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>
This commit is contained in:
1 parent
03e6d6b628
commit
e0f4e8ff0d
32 files changed
+4845
No files matched your search
@@ -0,0 +1,48 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user