diff --git a/frontend/src/components/layout/RightSidebar.tsx b/frontend/src/components/layout/RightSidebar.tsx index 6207b8a..79e0867 100644 --- a/frontend/src/components/layout/RightSidebar.tsx +++ b/frontend/src/components/layout/RightSidebar.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react' +import { useState, useMemo, useEffect } from 'react' import { X, Star, @@ -28,9 +28,23 @@ interface PhotoDetails { rating: number is_picked: boolean is_discarded: boolean + user_title: string | null + user_notes: string | null + color_label: string | null exif_json: string | null } +type ColorLabel = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple' + +const COLOR_LABEL_OPTIONS: { value: ColorLabel; className: string }[] = [ + { value: 'red', className: 'bg-red-500' }, + { value: 'orange', className: 'bg-orange-500' }, + { value: 'yellow', className: 'bg-yellow-400' }, + { value: 'green', className: 'bg-green-500' }, + { value: 'blue', className: 'bg-blue-500' }, + { value: 'purple', className: 'bg-purple-500' }, +] + interface ExifData { Make?: string Model?: string @@ -102,13 +116,17 @@ export function RightSidebar() { 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. + // Mutation for any patchable field on the active photo. Invalidates both + // the photo detail cache and the timeline list so the grid reflects the + // change too. const updateMutation = useMutation({ mutationFn: (data: { rating?: number is_picked?: boolean is_discarded?: boolean + user_title?: string | null + user_notes?: string | null + color_label?: string | null }) => photosApi.update(activePhotoId!, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['photo', activePhotoId] }) @@ -116,6 +134,35 @@ export function RightSidebar() { }, }) + // Local drafts for the editable text fields. These mirror the server value + // but stay independent while the user is typing, so we don't fight focus or + // clobber edits with stale refetches. + const [titleDraft, setTitleDraft] = useState('') + const [notesDraft, setNotesDraft] = useState('') + + useEffect(() => { + setTitleDraft(photo?.user_title ?? '') + setNotesDraft(photo?.user_notes ?? '') + }, [photo?.id, photo?.user_title, photo?.user_notes]) + + const commitTitle = () => { + const next = titleDraft.trim() + const current = photo?.user_title ?? '' + if (next === current) return + updateMutation.mutate({ user_title: next || null }) + } + + const commitNotes = () => { + const next = notesDraft + const current = photo?.user_notes ?? '' + if (next === current) return + updateMutation.mutate({ user_notes: next || null }) + } + + const setColor = (label: ColorLabel | null) => { + updateMutation.mutate({ color_label: label }) + } + const exif = useMemo(() => parseExif(photo?.exif_json ?? null), [photo?.exif_json]) if (selectedPhotos.length === 0) { @@ -133,6 +180,7 @@ export function RightSidebar() { const rating = photo?.rating ?? 0 const isPicked = photo?.is_picked ?? false const isDiscarded = photo?.is_discarded ?? false + const colorLabel = (photo?.color_label ?? null) as ColorLabel | null return (