feat: UI/UX improvements pass + fix photo 404 in Docker

Photos:
- Serve uploads via /api/photos/[filename] from UPLOAD_DIR env var
- Default UPLOAD_DIR=/data/uploads (mount as Docker volume for persistence)
- Upload route creates dir on demand, no root-permission issue
- Dockerfile creates /data/uploads owned by nextjs user

Growth:
- Show chart with single measurement (was hidden until 2+ points)
- PC label expanded to "PC — Périmètre crânien (cm)"
- WHO percentile ⓘ tooltip explaining P50/P3-P97
- Date field defaults to today in add-measurement form

Stats:
- Heatmap legend shows actual counts (0 1 2 3 4+) not just Moins/Plus
- Sleep timeline labeled "Fenêtre 19h – 11h"

Timeline:
- Filter type persisted in URL param (?type=...) — survives navigation
- Inline note save shows ✓ flash after blur
- Photo thumbnails open in new tab

Photos:
- Lightbox wraps: next on last → first, prev on first → last

Search:
- Result count header "X événements · Y notes"
- Event results link to /timeline?date=...&type=...
- Note results link to /notes?date=...

Notes:
- Unsaved indicator (amber dot) while textarea dirty, green ✓ after save

Milestones:
- Sort by date ascending within each month group

Settings:
- Pushover key field has eye toggle (show/hide)
- API key delete shows confirmation with key name
- All sections wrapped in collapsible SectionCard accordion

Dashboard:
- Empty state callout for new users with no events today

Event modal:
- Milk lot selection shows "Sélectionnez un lot — il sera marqué comme utilisé"
- Photo upload validates 5MB client-side before sending
- Timer resume shows "Reprise du chronomètre" when loaded from localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 20:44:45 +02:00
parent f7ab467385
commit 8f68eebeb0
14 changed files with 258 additions and 60 deletions
+4 -6
View File
@@ -58,11 +58,11 @@ export default function PhotosPage() {
}
function prev() {
setLightbox((lb) => lb && lb.index > 0 ? { ...lb, index: lb.index - 1 } : lb);
setLightbox((lb) => lb ? { ...lb, index: (lb.index - 1 + lb.events.length) % lb.events.length } : lb);
}
function next() {
setLightbox((lb) => lb && lb.index < lb.events.length - 1 ? { ...lb, index: lb.index + 1 } : lb);
setLightbox((lb) => lb ? { ...lb, index: (lb.index + 1) % lb.events.length } : lb);
}
const current = lightbox ? lightbox.events[lightbox.index] : null;
@@ -150,16 +150,14 @@ export default function PhotosPage() {
<div className="flex items-center justify-between px-4 py-4 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
<button
onClick={prev}
disabled={lightbox.index === 0}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition disabled:opacity-30"
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition"
>
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<span className="text-white/60 text-xs">{lightbox.index + 1} / {lightbox.events.length}</span>
<button
onClick={next}
disabled={lightbox.index === lightbox.events.length - 1}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition disabled:opacity-30"
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 transition"
>
<ChevronRight className="w-5 h-5 text-white" />
</button>