Files
shpm/client/src/components/Timeline.jsx
T
nmemmertandClaude Sonnet 4.6 e0f4e8ff0d feat: initial Luma implementation
Full self-hosted photo manager with:
- Ingestion worker (chokidar + sharp + exifr, dHash perceptual hashing)
- SQLite schema with WAL mode (photos, tags, collections, duplicate_pairs)
- Express API with keyset pagination, tag/collection/search/date filters
- React + Vite UI: timeline, lightbox, detail panel, tag/collection editors
- Duplicate detection (Union-Find grouping) with review + dismiss UI
- Map view (Leaflet + OpenStreetMap, marker clustering)
- Docker multi-stage build + Compose with inline env config
- GitHub Actions: CI build check + ghcr.io image publish on main/tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-17 16:33:55 -04:00

49 lines
1.1 KiB
React

import MonthGroup from './MonthGroup.jsx';
function groupByMonth(photos) {
const groups = [];
let current = null;
for (const photo of photos) {
const key = photo.date_taken ? photo.date_taken.slice(0, 7) : 'undated';
if (!current || current.key !== key) {
current = { key, label: monthLabel(photo.date_taken), photos: [] };
groups.push(current);
}
current.photos.push(photo);
}
return groups;
}
function monthLabel(dateTaken) {
if (!dateTaken) return 'Undated';
return new Date(dateTaken).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
}
export default function Timeline({ photos, onSelect }) {
if (!photos.length) return null;
const groups = groupByMonth(photos);
let globalIdx = 0;
return (
<main style={{ padding: '0 3px' }}>
{groups.map(group => {
const startIdx = globalIdx;
globalIdx += group.photos.length;
return (
<MonthGroup
key={group.key}
label={group.label}
photos={group.photos}
startIdx={startIdx}
onSelect={onSelect}
/>
);
})}
</main>
);
}