+
+ {/* Header row */}
+
+
+
+ {dateLabel}
+
+
+ {noPhotos
+ ? 'No photos from this day in your library yet'
+ : `${allPhotos.length} photo${allPhotos.length !== 1 ? 's' : ''} across ${groups.length} year${groups.length !== 1 ? 's' : ''}`}
+
+
+ {!noPhotos && (
+
+ )}
+
+
+ {/* Empty state */}
+ {noPhotos && (
+
+
📷
+
Nothing from {dateLabel} yet.
+
Keep shooting — they'll appear here on this date next year.
+
+ )}
+
+ {/* Year sections */}
+ {groups.map(({ year, photos: yPhotos }) => (
+
+
+ {year}
+ {yearsAgo(year)}
+ · {yPhotos.length} photo{yPhotos.length !== 1 ? 's' : ''}
+
+
+ {yPhotos.map(photo => {
+ const idx = allPhotos.indexOf(photo);
+ return (
+
startSlideshow(idx)}
+ style={{
+ width: 168, height: 126, borderRadius: 5,
+ overflow: 'hidden', cursor: 'pointer',
+ flexShrink: 0, background: '#111',
+ position: 'relative',
+ }}
+ onMouseEnter={e => { e.currentTarget.style.opacity = '0.78'; }}
+ onMouseLeave={e => { e.currentTarget.style.opacity = '1'; }}
+ >
+ {photo.thumb_url
+ ?

+ :
▪
+ }
+
+ );
+ })}
+
+
+ ))}
+
+
+ {/* ── Slideshow overlay ── */}
+ {slideshow && allPhotos.length > 0 && (() => {
+ const photo = allPhotos[ssIndex];
+ const year = new Date(photo.date_taken).getFullYear();
+ return (
+
+
+ {/* Photo area */}
+
+

+
+ {/* Year badge */}
+
+
{year}
+
{yearsAgo(year)}
+
+
+ {/* Prev */}
+
+
+ {/* Next */}
+
+
+
+ {/* Bottom bar */}
+
+ {/* Progress dots */}
+
+ {allPhotos.map((_, i) => (
+
{ setOpacity(0); setTimeout(() => { setSsIndex(i); setOpacity(1); }, 330); }}
+ style={{
+ width: i === ssIndex ? 20 : 6, height: 6, borderRadius: 3,
+ background: i === ssIndex ? '#4d9eff' : '#2a2a2a',
+ cursor: 'pointer',
+ transition: 'width 0.25s, background 0.25s',
+ flexShrink: 0,
+ }}
+ />
+ ))}
+
+
+ {/* Counter */}
+
+ {ssIndex + 1} / {allPhotos.length}
+
+
+ {/* Mute */}
+
+
+ {/* Exit */}
+
+
+
+ );
+ })()}
+ >
+ );
+}
+
+// ── Styles ─────────────────────────────────────────────────────────────────────
+
+function navBtn(side) {
+ return {
+ position: 'absolute', top: '50%', transform: 'translateY(-50%)',
+ [side]: 14,
+ background: 'rgba(0,0,0,0.45)',
+ border: 'none', borderRadius: 8,
+ color: '#fff', fontSize: 36, lineHeight: 1,
+ width: 44, height: 68,
+ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
+ opacity: 0.5, transition: 'opacity 0.18s',
+ };
+}
+
+const ctrlBtn = {
+ background: 'transparent',
+ border: '1px solid #222', borderRadius: 6,
+ color: '#888', cursor: 'pointer',
+ padding: '5px 11px', fontSize: 14,
+ flexShrink: 0,
+};
diff --git a/server/index.js b/server/index.js
index 3cb944a..eced631 100644
--- a/server/index.js
+++ b/server/index.js
@@ -501,6 +501,22 @@ app.post('/api/admin/clear-dismissals', (_req, res) => {
res.sendStatus(204);
});
+// ── Memories (photos on this day) ─────────────────────────────────────────────
+
+app.get('/api/memories/today', (req, res) => {
+ // Client sends its local date as YYYY-MM-DD so Docker's UTC clock doesn't cause off-by-one
+ const date = req.query.date ?? new Date().toISOString().slice(0, 10);
+ const monthDay = date.slice(5); // "MM-DD"
+ const rows = db.raw.prepare(`
+ SELECT ${SELECT_COLS}
+ FROM photos p
+ WHERE p.date_taken IS NOT NULL
+ AND strftime('%m-%d', p.date_taken) = ?
+ ORDER BY p.date_taken ASC
+ `).all(monthDay);
+ res.json({ photos: rows.map(mapPhoto) });
+});
+
// ── Filesystem browser ────────────────────────────────────────────────────────
app.get('/api/fs/browse', (req, res) => {