feat: structure 2

This commit is contained in:
2026-04-07 00:15:00 +02:00
parent 46a0d7aba8
commit 6d1b227fb9
15 changed files with 7433 additions and 94 deletions

View File

@@ -0,0 +1,302 @@
import { useState } from 'react'
import {
X,
Star,
MapPin,
Camera,
Aperture,
Info,
ChevronDown,
ChevronRight,
Check,
Plus
} from 'lucide-react'
import clsx from 'clsx'
import { usePhotoStore } from '../../store/photoStore'
import { format } from 'date-fns'
export function RightSidebar() {
const { selectedPhotos, clearSelection } = usePhotoStore()
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(['basic', 'camera', 'location', 'tags'])
)
const [rating, setRating] = useState(0)
const [flagStatus, setFlagStatus] = useState<'none' | 'pick' | 'reject'>('none')
const toggleSection = (section: string) => {
const newExpanded = new Set(expandedSections)
if (newExpanded.has(section)) {
newExpanded.delete(section)
} else {
newExpanded.add(section)
}
setExpandedSections(newExpanded)
}
// Mock photo data - in real app, fetch based on selectedPhotos
const mockPhoto = selectedPhotos.length > 0 ? {
filename: 'IMG_1234.jpg',
size: '3.2 MB',
dimensions: '4032 × 3024',
dateTaken: new Date('2024-01-15T14:30:00'),
camera: 'Canon EOS R5',
lens: 'RF 24-70mm F2.8L IS USM',
iso: 400,
aperture: 'f/2.8',
shutterSpeed: '1/250',
focalLength: '50mm',
location: 'San Francisco, CA',
tags: ['landscape', 'sunset', 'golden hour'],
} : null
if (selectedPhotos.length === 0) {
return (
<div className="flex h-full items-center justify-center p-4 text-center">
<div className="text-text-muted">
<Info className="mx-auto mb-2 h-8 w-8" />
<p className="text-sm">Select photos to view details</p>
</div>
</div>
)
}
const multipleSelected = selectedPhotos.length > 1
return (
<div className="flex h-full flex-col bg-surface">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h2 className="text-sm font-semibold text-text">
{multipleSelected
? `${selectedPhotos.length} Photos Selected`
: 'Photo Details'}
</h2>
<button
onClick={clearSelection}
className="rounded p-1 text-text-muted hover:bg-surface-2 hover:text-text"
>
<X className="h-4 w-4" />
</button>
</div>
{/* Quick Actions */}
<div className="border-b border-border p-4">
{/* Rating Stars */}
<div className="mb-3">
<label className="mb-1 block text-xs text-text-muted">Rating</label>
<div className="flex gap-1">
{[1, 2, 3, 4, 5].map((value) => (
<button
key={value}
onClick={() => setRating(rating === value ? 0 : value)}
className="p-0.5"
>
<Star
className={clsx(
'h-5 w-5 transition-colors',
value <= rating
? 'fill-star text-star'
: 'text-text-muted hover:text-star'
)}
/>
</button>
))}
</div>
</div>
{/* Flag Status */}
<div>
<label className="mb-1 block text-xs text-text-muted">Flag</label>
<div className="flex gap-2">
<button
onClick={() => setFlagStatus(flagStatus === 'pick' ? 'none' : 'pick')}
className={clsx(
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
flagStatus === 'pick'
? 'bg-pick/20 text-pick'
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'
)}
>
<Check className="h-3 w-3" />
Pick
</button>
<button
onClick={() => setFlagStatus(flagStatus === 'reject' ? 'none' : 'reject')}
className={clsx(
'flex items-center gap-1 rounded px-2 py-1 text-sm transition-colors',
flagStatus === 'reject'
? 'bg-reject/20 text-reject'
: 'bg-surface-2 text-text-muted hover:bg-surface-offset'
)}
>
<X className="h-3 w-3" />
Reject
</button>
</div>
</div>
</div>
{/* Metadata Sections */}
<div className="flex-1 overflow-y-auto">
{mockPhoto && (
<>
{/* Basic Info */}
<div className="border-b border-border">
<button
onClick={() => toggleSection('basic')}
className="flex w-full items-center justify-between px-4 py-2 text-sm hover:bg-surface-2"
>
<span className="font-medium text-text">Basic Info</span>
{expandedSections.has('basic') ? (
<ChevronDown className="h-4 w-4 text-text-muted" />
) : (
<ChevronRight className="h-4 w-4 text-text-muted" />
)}
</button>
{expandedSections.has('basic') && (
<div className="px-4 pb-3 text-xs">
<div className="grid grid-cols-2 gap-2">
<div>
<span className="text-text-muted">Filename:</span>
<p className="text-text">{mockPhoto.filename}</p>
</div>
<div>
<span className="text-text-muted">Size:</span>
<p className="text-text">{mockPhoto.size}</p>
</div>
<div>
<span className="text-text-muted">Dimensions:</span>
<p className="text-text">{mockPhoto.dimensions}</p>
</div>
<div>
<span className="text-text-muted">Date Taken:</span>
<p className="text-text">
{format(mockPhoto.dateTaken, 'MMM d, yyyy')}
</p>
</div>
</div>
</div>
)}
</div>
{/* Camera Info */}
<div className="border-b border-border">
<button
onClick={() => toggleSection('camera')}
className="flex w-full items-center justify-between px-4 py-2 text-sm hover:bg-surface-2"
>
<span className="font-medium text-text">Camera</span>
{expandedSections.has('camera') ? (
<ChevronDown className="h-4 w-4 text-text-muted" />
) : (
<ChevronRight className="h-4 w-4 text-text-muted" />
)}
</button>
{expandedSections.has('camera') && (
<div className="px-4 pb-3 text-xs">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Camera className="h-3 w-3 text-text-muted" />
<span className="text-text">{mockPhoto.camera}</span>
</div>
<div className="flex items-center gap-2">
<Aperture className="h-3 w-3 text-text-muted" />
<span className="text-text">{mockPhoto.lens}</span>
</div>
<div className="grid grid-cols-2 gap-2 mt-2">
<div>
<span className="text-text-muted">ISO:</span>
<span className="ml-1 text-text">{mockPhoto.iso}</span>
</div>
<div>
<span className="text-text-muted">Aperture:</span>
<span className="ml-1 text-text">{mockPhoto.aperture}</span>
</div>
<div>
<span className="text-text-muted">Shutter:</span>
<span className="ml-1 text-text">{mockPhoto.shutterSpeed}</span>
</div>
<div>
<span className="text-text-muted">Focal:</span>
<span className="ml-1 text-text">{mockPhoto.focalLength}</span>
</div>
</div>
</div>
</div>
)}
</div>
{/* Location */}
<div className="border-b border-border">
<button
onClick={() => toggleSection('location')}
className="flex w-full items-center justify-between px-4 py-2 text-sm hover:bg-surface-2"
>
<span className="font-medium text-text">Location</span>
{expandedSections.has('location') ? (
<ChevronDown className="h-4 w-4 text-text-muted" />
) : (
<ChevronRight className="h-4 w-4 text-text-muted" />
)}
</button>
{expandedSections.has('location') && (
<div className="px-4 pb-3">
<div className="flex items-center gap-2 text-xs">
<MapPin className="h-3 w-3 text-text-muted" />
<span className="text-text">{mockPhoto.location}</span>
</div>
</div>
)}
</div>
{/* Tags */}
<div className="border-b border-border">
<button
onClick={() => toggleSection('tags')}
className="flex w-full items-center justify-between px-4 py-2 text-sm hover:bg-surface-2"
>
<span className="font-medium text-text">Tags</span>
{expandedSections.has('tags') ? (
<ChevronDown className="h-4 w-4 text-text-muted" />
) : (
<ChevronRight className="h-4 w-4 text-text-muted" />
)}
</button>
{expandedSections.has('tags') && (
<div className="px-4 pb-3">
<div className="flex flex-wrap gap-1">
{mockPhoto.tags.map((tag) => (
<span
key={tag}
className="rounded bg-surface-2 px-2 py-0.5 text-xs text-text"
>
{tag}
</span>
))}
<button className="rounded bg-surface-2 px-2 py-0.5 text-xs text-text-muted hover:bg-surface-offset hover:text-text">
<Plus className="h-3 w-3" />
</button>
</div>
</div>
)}
</div>
</>
)}
</div>
{/* Footer Actions */}
{multipleSelected && (
<div className="border-t border-border p-3">
<div className="space-y-2">
<button className="w-full rounded bg-surface-2 px-3 py-1.5 text-sm text-text hover:bg-surface-offset">
Add to Heap
</button>
<button className="w-full rounded bg-surface-2 px-3 py-1.5 text-sm text-text hover:bg-surface-offset">
Export Selected
</button>
</div>
</div>
)}
</div>
)
}