diff --git a/frontend/src/components/memories/MemoriesView.tsx b/frontend/src/components/memories/MemoriesView.tsx index f429176..6ba0581 100644 --- a/frontend/src/components/memories/MemoriesView.tsx +++ b/frontend/src/components/memories/MemoriesView.tsx @@ -1,70 +1,162 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react' import { useQuery } from '@tanstack/react-query' -import { photos, photos as photosApi } from '../../services/api' -import type { MemoryGroup } from '../../services/api' +import { Loader2 } from 'lucide-react' +import { + photos, + type MemoriesResponse, + type MemoryPhoto, +} from '../../services/api' +import type { Photo } from '../../types/photo' +import { usePhotoStore } from '../../store/photoStore' +import { useFilterStore } from '../../store/filterStore' +import { PhotoThumbnail } from '../timeline/PhotoThumbnail' +const CELL_SIZE = 180 +const GAP = 4 + +/** + * "On this day" view. Same visual + interaction grid as Timeline — + * PhotoThumbnail cells wired up to the shared photo store, so selection, + * heap membership, preview (double-click / Enter), and drag-to-heap all + * work identically. Just grouped by year instead of month, and sourced + * from the /memories endpoint. + */ export function MemoriesView() { - const { data, isLoading } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: ['memories'], - queryFn: () => photos.memories(), - staleTime: 60_000 * 30, // 30 min — date doesn't change often + queryFn: photos.memories, + staleTime: 60_000 * 30, }) + const memories = data?.memories ?? [] + + // Flat ordered sequence of ids across every year section — fed to + // the store so preview arrow nav walks the same order the user sees. + const visibleSequence = useMemo( + () => memories.flatMap((g) => g.photos.map((p) => p.id)), + [memories], + ) + const setVisiblePhotoIds = usePhotoStore((s) => s.setVisiblePhotoIds) + useEffect(() => { + setVisiblePhotoIds(visibleSequence) + }, [visibleSequence, setVisiblePhotoIds]) + + const selectedPhotos = usePhotoStore((s) => s.selectedPhotos) + const selectPhoto = usePhotoStore((s) => s.selectPhoto) + const togglePhotoSelection = usePhotoStore((s) => s.togglePhotoSelection) + const selectRange = usePhotoStore((s) => s.selectRange) + const openPreview = usePhotoStore((s) => s.openPreview) + const searchQuery = useFilterStore((s) => s.q) + + const visibleSequenceRef = useRef([]) + visibleSequenceRef.current = visibleSequence + + const handleCellClick = useCallback( + (photo: Photo, e: React.MouseEvent) => { + if (e.shiftKey) selectRange(photo.id) + else if (e.ctrlKey || e.metaKey) togglePhotoSelection(photo.id) + else selectPhoto(photo.id) + }, + [selectRange, togglePhotoSelection, selectPhoto], + ) + const handleCellDoubleClick = useCallback( + (photo: Photo) => { + openPreview(photo.id, visibleSequenceRef.current) + }, + [openPreview], + ) + if (isLoading) { return ( -
- Loading memories... +
+ + Loading memories…
) } - const memories = data?.memories ?? [] - if (memories.length === 0) { return ( -
-

No memories for today

-

Photos taken on this date in previous years will appear here.

+
+
+ No memories for today +
+

+ Photos taken on this date in previous years will appear here. +

) } return ( -
-

- On This Day — {data?.date} +
+

+ On this day · {data?.date}

- {memories.map((group: MemoryGroup) => ( -
-

- {group.year} · {group.years_ago} year{group.years_ago !== 1 ? 's' : ''} ago -

-
- {group.photos.map((photo) => ( -
- {photo.thumb_small ? ( - {photo.filename} - ) : ( -
- No thumb -
- )} -
-

{photo.filename}

-
-
- ))} -
-
- ))} +
+ {memories.map((group) => ( +
+
+

+ {group.year} +

+ + {group.years_ago} year{group.years_ago === 1 ? '' : 's'} ago + +
+
+ {group.photos.map((m) => ( + + ))} +
+
+ ))} +
) } + +/** Widen the slim MemoryPhoto payload to the full Photo shape the + * thumbnail expects. Fields the memories endpoint doesn't return + * (is_duplicate, file_hash, ...) get sensible defaults — memories + * never surface discarded/duplicate photos server-side. */ +function memoryToPhoto(m: MemoryPhoto): Photo { + return { + id: m.id, + filepath: m.filename, + filename: m.filename, + media_type: m.media_type, + width: m.width, + height: m.height, + taken_at: m.taken_at, + rating: m.rating, + is_discarded: false, + is_duplicate: false, + file_hash: '', + folder_id: null, + added_at: null, + thumb_small: m.thumb_small ?? undefined, + thumb_medium: m.thumb_medium ?? undefined, + } +}