diff --git a/frontend/src/components/layout/RightSidebar.tsx b/frontend/src/components/layout/RightSidebar.tsx index 02a8bd9..34fb6ae 100644 --- a/frontend/src/components/layout/RightSidebar.tsx +++ b/frontend/src/components/layout/RightSidebar.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { +import { useState, useMemo } from 'react' +import { X, Star, MapPin, @@ -9,46 +9,114 @@ import { ChevronDown, ChevronRight, Check, - Plus } from 'lucide-react' import clsx from 'clsx' -import { usePhotoStore } from '../../store/photoStore' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { format } from 'date-fns' +import { usePhotoStore } from '../../store/photoStore' +import { photos as photosApi } from '../../services/api' + +interface PhotoDetails { + id: string + filename: string + filepath: string + width: number | null + height: number | null + file_size: number | null + taken_at: string | null + rating: number + is_picked: boolean + is_rejected: boolean + exif_json: string | null +} + +interface ExifData { + Make?: string + Model?: string + LensModel?: string + Lens?: string + ISO?: number | string + FNumber?: number | string + ApertureValue?: number | string + ExposureTime?: string + ShutterSpeedValue?: string + FocalLength?: string + FocalLengthIn35mmFormat?: string + GPSLatitude?: number | string + GPSLongitude?: number | string + [key: string]: unknown +} + +function formatFileSize(bytes: number | null): string { + if (bytes == null) return '—' + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB` + return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB` +} + +function formatExifValue(v: unknown): string { + if (v == null || v === '') return '—' + return String(v) +} + +function pickFirst(exif: ExifData, ...keys: string[]): string { + for (const k of keys) { + const v = exif[k] + if (v != null && v !== '') return String(v) + } + return '—' +} + +function parseExif(json: string | null): ExifData { + if (!json) return {} + try { + const parsed = JSON.parse(json) + return typeof parsed === 'object' && parsed !== null ? (parsed as ExifData) : {} + } catch { + return {} + } +} export function RightSidebar() { - const { selectedPhotos, clearSelection } = usePhotoStore() + const { selectedPhotos, activePhotoId, clearSelection } = usePhotoStore() + const queryClient = useQueryClient() + const [expandedSections, setExpandedSections] = useState>( - new Set(['basic', 'camera', 'location', 'tags']) + new Set(['basic', 'camera', 'location']) ) - 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) - } + 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 - + + // Fetch the active photo's full record (with EXIF) on demand. + const { data: photo } = useQuery({ + queryKey: ['photo', activePhotoId], + queryFn: () => photosApi.get(activePhotoId!), + enabled: !!activePhotoId, + staleTime: 60_000, + }) + + // Mutations for rating / pick / reject. Optimistic-ish: invalidate the + // photo query and the timeline list query so the grid re-renders too. + const updateMutation = useMutation({ + mutationFn: (data: { + rating?: number + is_picked?: boolean + is_rejected?: boolean + }) => photosApi.update(activePhotoId!, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['photo', activePhotoId] }) + queryClient.invalidateQueries({ queryKey: ['photos'] }) + }, + }) + + const exif = useMemo(() => parseExif(photo?.exif_json ?? null), [photo?.exif_json]) + if (selectedPhotos.length === 0) { return (
@@ -59,232 +127,205 @@ export function RightSidebar() {
) } - + const multipleSelected = selectedPhotos.length > 1 - + const rating = photo?.rating ?? 0 + const isPicked = photo?.is_picked ?? false + const isRejected = photo?.is_rejected ?? false + return (
{/* Header */}

- {multipleSelected - ? `${selectedPhotos.length} Photos Selected` + {multipleSelected + ? `${selectedPhotos.length} Photos Selected` : 'Photo Details'}

- - {/* Quick Actions */} -
- {/* Rating Stars */} -
- -
- {[1, 2, 3, 4, 5].map((value) => ( + + {/* Quick Actions — operate on the active photo */} + {photo && !multipleSelected && ( +
+
+ +
+ {[1, 2, 3, 4, 5].map((value) => ( + + ))} +
+
+ +
+ +
- ))} + +
- - {/* Flag Status */} -
- -
- - -
-
-
- - {/* Metadata Sections */} + )} + + {/* Metadata */}
- {mockPhoto && ( + {photo && !multipleSelected && ( <> {/* Basic Info */} -
- - {expandedSections.has('basic') && ( -
-
-
- Filename: -

{mockPhoto.filename}

-
-
- Size: -

{mockPhoto.size}

-
-
- Dimensions: -

{mockPhoto.dimensions}

-
-
- Date Taken: -

- {format(mockPhoto.dateTaken, 'MMM d, yyyy')} -

-
-
+
toggleSection('basic')} + > +
+ + + + +
+
+ + {/* Camera */} +
toggleSection('camera')} + > +
+
+ + + {pickFirst(exif, 'Make', 'Model') === '—' + ? '—' + : `${formatExifValue(exif.Make)} ${formatExifValue(exif.Model)}`.trim()} +
- )} -
- - {/* Camera Info */} -
- - {expandedSections.has('camera') && ( -
-
-
- - {mockPhoto.camera} -
-
- - {mockPhoto.lens} -
-
-
- ISO: - {mockPhoto.iso} -
-
- Aperture: - {mockPhoto.aperture} -
-
- Shutter: - {mockPhoto.shutterSpeed} -
-
- Focal: - {mockPhoto.focalLength} -
-
-
+
+ + + {pickFirst(exif, 'LensModel', 'Lens')} +
- )} -
- +
+ + + + +
+
+
+ {/* Location */} -
- - {expandedSections.has('location') && ( -
-
- - {mockPhoto.location} -
+
toggleSection('location')} + > + {exif.GPSLatitude && exif.GPSLongitude ? ( +
+ + + {String(exif.GPSLatitude)}, {String(exif.GPSLongitude)} +
+ ) : ( +
No GPS data
)} -
- - {/* Tags */} -
- - {expandedSections.has('tags') && ( -
-
- {mockPhoto.tags.map((tag) => ( - - {tag} - - ))} - -
-
- )} -
+ )} + + {!photo && !multipleSelected && ( +
Loading…
+ )}
- - {/* Footer Actions */} + + {/* Footer Actions for multi-select */} {multipleSelected && (
@@ -299,4 +340,42 @@ export function RightSidebar() { )}
) -} \ No newline at end of file +} + +function Section({ + title, + expanded, + onToggle, + children, +}: { + title: string + expanded: boolean + onToggle: () => void + children: React.ReactNode +}) { + return ( +
+ + {expanded &&
{children}
} +
+ ) +} + +function Field({ label, value }: { label: string; value: string }) { + return ( +
+ {label}: +

{value}

+
+ ) +}