feat: "On this day" memories — photos from previous years
Add a Memories view that surfaces photos taken on the current date in previous years (like Google Photos / Immich). Only uses EXIF-sourced dates to avoid false matches from filesystem timestamps. - Backend: GET /api/v1/photos/memories returns groups by year, up to 12 photos each, filtered to non-discarded/non-hidden EXIF dates - Frontend: MemoriesView with year-grouped thumbnail grid - Sidebar: new "Memories" nav item with clock icon Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
User as UserIcon,
|
||||
LogOut,
|
||||
Shield,
|
||||
Clock,
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import { sourceFolders, photos as photosApi, type FolderTreeNode } from '../../services/api'
|
||||
@@ -407,6 +408,7 @@ export function LeftSidebar({ onCollapse }: LeftSidebarProps) {
|
||||
{ id: 'people', label: 'People', icon: <Users className="h-4 w-4" />, count: peopleTotalCount },
|
||||
{ id: 'colors', label: 'Colors', icon: <Palette className="h-4 w-4" />, count: stats?.colored ?? 0 },
|
||||
{ id: 'map', label: 'Map', icon: <MapPin className="h-4 w-4" />, count: stats?.with_gps ?? 0 },
|
||||
{ id: 'memories', label: 'Memories', icon: <Clock className="h-4 w-4" /> },
|
||||
{ id: 'duplicates', label: 'Duplicates', icon: <Copy className="h-4 w-4" />, count: stats?.duplicates ?? 0 },
|
||||
{ id: 'discarded', label: 'Discarded', icon: <Trash2 className="h-4 w-4" />, count: stats?.discarded ?? 0 },
|
||||
],
|
||||
|
||||
70
frontend/src/components/memories/MemoriesView.tsx
Normal file
70
frontend/src/components/memories/MemoriesView.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { photos } from '../../services/api'
|
||||
import type { MemoryGroup } from '../../services/api'
|
||||
|
||||
export function MemoriesView() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['memories'],
|
||||
queryFn: () => photos.memories(),
|
||||
staleTime: 60_000 * 30, // 30 min — date doesn't change often
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64 text-neutral-500">
|
||||
Loading memories...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const memories = data?.memories ?? []
|
||||
|
||||
if (memories.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-neutral-500 gap-2">
|
||||
<p className="text-lg font-medium">No memories for today</p>
|
||||
<p className="text-sm">Photos taken on this date in previous years will appear here.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-8 max-w-5xl mx-auto">
|
||||
<h2 className="text-xl font-semibold text-neutral-200">
|
||||
On This Day — {data?.date}
|
||||
</h2>
|
||||
|
||||
{memories.map((group: MemoryGroup) => (
|
||||
<section key={group.year} className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-neutral-400 uppercase tracking-wide">
|
||||
{group.year} · {group.years_ago} year{group.years_ago !== 1 ? 's' : ''} ago
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2">
|
||||
{group.photos.map((photo) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="aspect-square rounded-lg overflow-hidden bg-neutral-800 relative group"
|
||||
>
|
||||
{photo.thumb_small ? (
|
||||
<img
|
||||
src={`/api/v1/photos/${photo.id}/thumb/small`}
|
||||
alt={photo.filename}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-neutral-600 text-xs">
|
||||
No thumb
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<p className="text-xs text-white truncate">{photo.filename}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user