diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts index 4cdd97e..39c9e69 100644 --- a/frontend/src/hooks/useKeyboardShortcuts.ts +++ b/frontend/src/hooks/useKeyboardShortcuts.ts @@ -1,5 +1,7 @@ import { useHotkeys } from 'react-hotkeys-hook' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { usePhotoStore } from '../store/photoStore' +import { photos as photosApi } from '../services/api' interface KeyboardShortcutsProps { onToggleLeftSidebar: () => void @@ -8,17 +10,50 @@ interface KeyboardShortcutsProps { getFirstPhotoId?: () => string | null } +interface PhotoUpdate { + rating?: number + is_picked?: boolean + is_rejected?: boolean + color_label?: string | null +} + +// Spec §6.4 number-key color labels. +const COLOR_LABELS: Record = { + '6': 'red', + '7': 'orange', + '8': 'yellow', + '9': 'green', +} + export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { const { onToggleLeftSidebar, onToggleRightSidebar, getFirstPhotoId } = props const viewMode = usePhotoStore((s) => s.viewMode) - const activePhotoId = usePhotoStore((s) => s.activePhotoId) const openLoupe = usePhotoStore((s) => s.openLoupe) const closeLoupe = usePhotoStore((s) => s.closeLoupe) const isGrid = viewMode === 'grid' const isLoupe = viewMode === 'loupe' + // Photo mutation shared by every culling shortcut. Reads the active photo + // id from the store at fire time so the closure stays fresh without forcing + // hotkey re-binding on every selection change. + const queryClient = useQueryClient() + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: PhotoUpdate }) => + photosApi.update(id, data), + onSuccess: (_data, vars) => { + queryClient.invalidateQueries({ queryKey: ['photo', vars.id] }) + queryClient.invalidateQueries({ queryKey: ['photos'] }) + }, + }) + + const updateActive = (data: PhotoUpdate) => { + const id = usePhotoStore.getState().activePhotoId + if (!id) return + updateMutation.mutate({ id, data }) + } + // Toggle sidebars (allowed in both modes; right sidebar is hidden in loupe // by App-level CSS so toggling it is effectively grid-only.) useHotkeys('tab', (e) => { @@ -31,15 +66,14 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { onToggleRightSidebar() }) - // Grid view: G always returns to grid (closes loupe if open). + // G always returns to grid (closes loupe if open). useHotkeys('g', () => { closeLoupe() }) - // Loupe view: E toggles loupe (open from grid, close from loupe). - // Enter also opens loupe from grid. + // E toggles loupe (open from grid, close from loupe). Enter opens from grid. const openLoupeFromGrid = () => { - const id = activePhotoId ?? getFirstPhotoId?.() ?? null + const id = usePhotoStore.getState().activePhotoId ?? getFirstPhotoId?.() ?? null if (id) openLoupe(id) } @@ -52,7 +86,7 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { openLoupeFromGrid() } }, - [isLoupe, activePhotoId, getFirstPhotoId] + [isLoupe, getFirstPhotoId] ) useHotkeys( @@ -64,33 +98,37 @@ export function useKeyboardShortcuts(props: KeyboardShortcutsProps) { } }, { enabled: isGrid }, - [isGrid, activePhotoId, getFirstPhotoId] + [isGrid, getFirstPhotoId] ) - // Rating shortcuts (grid only — stubs from a different phase) - useHotkeys( - '1,2,3,4,5', - (_e, handler) => { - const rating = parseInt(handler.keys![0]) - console.log('Set rating:', rating) - }, - { enabled: isGrid } - ) + // ── Culling shortcuts (work in both grid and loupe) ────────────────────── + + // Star rating: 1-5 set, 0 clears. + useHotkeys('1,2,3,4,5', (_e, handler) => { + const rating = parseInt(handler.keys![0]) + if (Number.isFinite(rating)) updateActive({ rating }) + }) useHotkeys('0', () => { - console.log('Remove rating') - }, { enabled: isGrid }) + updateActive({ rating: 0 }) + }) - // Flag shortcuts (grid only) + // Pick / reject / unflag. useHotkeys('p', () => { - console.log('Pick photo') - }, { enabled: isGrid }) + updateActive({ is_picked: true, is_rejected: false }) + }) useHotkeys('x', () => { - console.log('Reject photo') - }, { enabled: isGrid }) + updateActive({ is_rejected: true, is_picked: false }) + }) useHotkeys('u', () => { - console.log('Unflag photo') - }, { enabled: isGrid }) + updateActive({ is_picked: false, is_rejected: false }) + }) + + // Color labels 6-9 (red/orange/yellow/green per spec §6.4). + useHotkeys('6,7,8,9', (_e, handler) => { + const label = COLOR_LABELS[handler.keys![0]] + if (label) updateActive({ color_label: label }) + }) }