feat: daily digest, photo timeline, milk expiry cron, batch delete, duration chart
- /api/cron/daily-digest: last feed/diaper, 24h sleep total, next med due - /photos: photo grid grouped by day, full-screen lightbox with prev/next - /api/cron/milk-expiry: push alert for lots expiring within 24h - Timeline: select mode with checkboxes, confirm + bulk DELETE - Stats: avg NAP/NIGHT_SLEEP duration trend dual-line chart (30d) - Nav: Photos link added under "Vie du bébé" drawer group Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { EventIcon } from "@/components/event-icon";
|
||||
import { useBaby } from "@/contexts/baby-context";
|
||||
import { format, isToday, isYesterday } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { Trash2, StickyNote, Pencil } from "lucide-react";
|
||||
import { Trash2, StickyNote, Pencil, CheckSquare, Square, XCircle } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { EventModal } from "@/components/event-modal";
|
||||
|
||||
@@ -71,6 +71,8 @@ export default function TimelinePage() {
|
||||
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
||||
const [filterType, setFilterType] = useState<EventType | "">("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
|
||||
const { data: eventsData, isLoading } = useQuery({
|
||||
queryKey: ["events", selectedBaby?.id, filterType, page],
|
||||
@@ -94,6 +96,22 @@ export default function TimelinePage() {
|
||||
qc.invalidateQueries({ queryKey: ["events"] });
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
if (selected.size === 0) return;
|
||||
await Promise.all([...selected].map((id) => fetch(`/api/events/${id}`, { method: "DELETE" })));
|
||||
setSelected(new Set());
|
||||
setSelectMode(false);
|
||||
qc.invalidateQueries({ queryKey: ["events"] });
|
||||
}
|
||||
|
||||
function toggleSelect(id: string) {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id); else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async function saveNotes(id: string, notes: string) {
|
||||
await fetch(`/api/events/${id}`, {
|
||||
method: "PATCH",
|
||||
@@ -112,9 +130,22 @@ export default function TimelinePage() {
|
||||
return (
|
||||
<div className="p-4 md:p-8 pb-24 md:pb-8">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-4">
|
||||
Journal{selectedBaby ? ` — ${selectedBaby.name}` : ""}
|
||||
</h1>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
||||
Journal{selectedBaby ? ` — ${selectedBaby.name}` : ""}
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => { setSelectMode((v) => !v); setSelected(new Set()); }}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium border transition ${
|
||||
selectMode
|
||||
? "bg-indigo-600 text-white border-indigo-600"
|
||||
: "bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-600"
|
||||
}`}
|
||||
>
|
||||
<CheckSquare className="w-3.5 h-3.5" />
|
||||
Sélectionner
|
||||
</button>
|
||||
</div>
|
||||
{/* Filter chips */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-1 no-scrollbar">
|
||||
<button
|
||||
@@ -140,6 +171,31 @@ export default function TimelinePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectMode && (
|
||||
<div className="flex items-center justify-between mb-4 px-4 py-2.5 bg-indigo-50 dark:bg-indigo-950/50 border border-indigo-200 dark:border-indigo-800 rounded-xl">
|
||||
<span className="text-sm text-indigo-700 dark:text-indigo-300 font-medium">
|
||||
{selected.size} sélectionné{selected.size > 1 ? "s" : ""}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => { setSelectMode(false); setSelected(new Set()); }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800"
|
||||
>
|
||||
<XCircle className="w-3.5 h-3.5" />
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { if (confirm(`Supprimer ${selected.size} événement${selected.size > 1 ? "s" : ""} ?`)) deleteSelected(); }}
|
||||
disabled={selected.size === 0}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg bg-red-600 text-white font-medium disabled:opacity-40"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Supprimer ({selected.size})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-indigo-600 border-t-transparent rounded-full animate-spin" />
|
||||
@@ -161,12 +217,21 @@ export default function TimelinePage() {
|
||||
const isExpanded = expandedId === ev.id;
|
||||
const summary = getEventSummary(ev);
|
||||
|
||||
const isSelected = selected.has(ev.id);
|
||||
|
||||
return (
|
||||
<div key={ev.id}>
|
||||
<div key={ev.id} className={isSelected ? "bg-indigo-50 dark:bg-indigo-950/30" : ""}>
|
||||
<button
|
||||
className="w-full text-left flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition"
|
||||
onClick={() => setExpandedId(isExpanded ? null : ev.id)}
|
||||
onClick={() => selectMode ? toggleSelect(ev.id) : setExpandedId(isExpanded ? null : ev.id)}
|
||||
>
|
||||
{selectMode && (
|
||||
<div className="flex-shrink-0">
|
||||
{isSelected
|
||||
? <CheckSquare className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
|
||||
: <Square className="w-4 h-4 text-slate-300 dark:text-slate-600" />}
|
||||
</div>
|
||||
)}
|
||||
<div className={`w-8 h-8 rounded-lg ${cfg.bgClass} flex items-center justify-center flex-shrink-0`}>
|
||||
<EventIcon name={cfg.icon} className={`w-4 h-4 ${cfg.colorClass}`} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user