feat: By Date sidebar — year/month drilldown navigation

The "By Date" library node was decorative. Now it's a real
hierarchical navigator: expand to see year buckets (with photo
counts), expand a year to see its months, click any year or
month to filter the timeline to that date range.

Backend
- New GET /library/date_buckets aggregates non-discarded photos
  by year+month from Photo.taken_at via SQLite strftime, returning
  [{ year, count, months: [{ month, count }] }] sorted newest-
  first. NULL taken_at rows are excluded.

Frontend
- New library.dateBuckets() helper + DateBucketYear / Month types.
- New hooks/useDateBucketsQuery.ts with a 60s staleTime.
- LeftSidebar builds the By Date subtree dynamically from the
  query: each year is a tree node with month children. Year nodes
  use the Calendar icon, months render as their full English name.
- applyLibraryNode handles two new id prefixes:
  'date-year-{year}'  → setDateFrom YYYY-01-01, setDateTo YYYY-12-31
  'date-month-{Y}-{M}'→ setDateFrom YYYY-MM-01, setDateTo YYYY-MM-LL
  where LL is the last day of the month (computed via Date trick
  new Date(year, month, 0).getDate() — uses month-day=0 to roll
  back into the previous month's last day).
- isItemActive recognises when the current dateFrom/dateTo matches
  a year or month node so the sidebar selection highlight stays
  in sync with the filter store (also when filters are set
  externally via the filter bar or URL hydration).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:14:57 +02:00
parent 16481730b7
commit 7d33e1688a
4 changed files with 134 additions and 2 deletions

View File

@@ -113,6 +113,16 @@ export const photos = {
}
// Library API
export interface DateBucketMonth {
month: number
count: number
}
export interface DateBucketYear {
year: number
count: number
months: DateBucketMonth[]
}
export const library = {
scan: async () => {
const response = await api.post('/library/scan')
@@ -128,6 +138,12 @@ export const library = {
const response = await api.get('/library/stats')
return response.data
},
/** Year/month aggregation for the sidebar By Date drilldown. */
dateBuckets: async (): Promise<DateBucketYear[]> => {
const response = await api.get('/library/date_buckets')
return response.data
},
}
// Heaps API