feat: finish remaining improvements — search filters, stats zero-fill, notes nav warning
- search: date range (7j/30j/90j/Tout) + type (Repas/Sommeil/Couche/Santé) filter chips - stats: missing-day gaps filled with 0 — lines no longer break on no-data days - notes: beforeunload handler warns before closing tab with unsaved changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,14 @@ export default function NotesPage() {
|
|||||||
setDirty(false);
|
setDirty(false);
|
||||||
}, [currentNote?.id, dateStr, isDayLoading]);
|
}, [currentNote?.id, dateStr, isDayLoading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: BeforeUnloadEvent) => {
|
||||||
|
if (dirty) { e.preventDefault(); e.returnValue = ""; }
|
||||||
|
};
|
||||||
|
window.addEventListener("beforeunload", handler);
|
||||||
|
return () => window.removeEventListener("beforeunload", handler);
|
||||||
|
}, [dirty]);
|
||||||
|
|
||||||
function goToPrevDay() {
|
function goToPrevDay() {
|
||||||
setCurrentDate((d) => startOfDay(subDays(d, 1)));
|
setCurrentDate((d) => startOfDay(subDays(d, 1)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export default function SearchPage() {
|
|||||||
const { selectedBaby } = useBaby();
|
const { selectedBaby } = useBaby();
|
||||||
const [q, setQ] = useState("");
|
const [q, setQ] = useState("");
|
||||||
const [debouncedQ, setDebouncedQ] = useState("");
|
const [debouncedQ, setDebouncedQ] = useState("");
|
||||||
|
const [filterDays, setFilterDays] = useState<number | null>(null);
|
||||||
|
const [filterType, setFilterType] = useState<string | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => { inputRef.current?.focus(); }, []);
|
useEffect(() => { inputRef.current?.focus(); }, []);
|
||||||
@@ -77,8 +79,26 @@ export default function SearchPage() {
|
|||||||
placeholderData: { events: [], notes: [] },
|
placeholderData: { events: [], notes: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = data?.events ?? [];
|
const typeMap: Record<string, string[]> = {
|
||||||
const notes = data?.notes ?? [];
|
"Repas": ["BREASTFEED", "BOTTLE"],
|
||||||
|
"Sommeil": ["NAP", "NIGHT_SLEEP"],
|
||||||
|
"Couche": ["DIAPER_WET", "DIAPER_STOOL"],
|
||||||
|
"Santé": ["TEMPERATURE", "MEDICATION", "SYMPTOM"],
|
||||||
|
};
|
||||||
|
const cutoff = filterDays ? new Date(Date.now() - filterDays * 86400000) : null;
|
||||||
|
|
||||||
|
const filteredEvents = (data?.events ?? []).filter(ev => {
|
||||||
|
if (cutoff && new Date(ev.startedAt) < cutoff) return false;
|
||||||
|
if (filterType && !typeMap[filterType]?.includes(ev.type)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const filteredNotes = (data?.notes ?? []).filter(note => {
|
||||||
|
if (cutoff && new Date(note.date) < cutoff) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = filteredEvents;
|
||||||
|
const notes = filteredNotes;
|
||||||
const hasResults = events.length > 0 || notes.length > 0;
|
const hasResults = events.length > 0 || notes.length > 0;
|
||||||
const searched = debouncedQ.length >= 2;
|
const searched = debouncedQ.length >= 2;
|
||||||
|
|
||||||
@@ -101,6 +121,29 @@ export default function SearchPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{q.length >= 2 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2 mb-3">
|
||||||
|
{([["7j", 7], ["30j", 30], ["90j", 90], ["Tout", null]] as [string, number|null][]).map(([label, days]) => (
|
||||||
|
<button key={label}
|
||||||
|
onClick={() => setFilterDays(days)}
|
||||||
|
className={`px-2.5 py-1 rounded-full text-xs font-medium transition ${filterDays === days ? "bg-indigo-600 text-white" : "bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"}`}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<span className="text-slate-300 dark:text-slate-600 self-center">·</span>
|
||||||
|
{["Repas", "Sommeil", "Couche", "Santé"].map((label) => {
|
||||||
|
const active = filterType === label;
|
||||||
|
return (
|
||||||
|
<button key={label}
|
||||||
|
onClick={() => setFilterType(active ? null : label)}
|
||||||
|
className={`px-2.5 py-1 rounded-full text-xs font-medium transition ${active ? "bg-slate-700 text-white dark:bg-slate-200 dark:text-slate-900" : "bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300"}`}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!searched && (
|
{!searched && (
|
||||||
<p className="text-sm text-slate-400 dark:text-slate-500 text-center py-12">Saisissez au moins 2 caractères</p>
|
<p className="text-sm text-slate-400 dark:text-slate-500 text-center py-12">Saisissez au moins 2 caractères</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -109,12 +109,12 @@ function buildDurationTrend(events: Event[], days: number) {
|
|||||||
e.endedAt &&
|
e.endedAt &&
|
||||||
format(new Date(e.startedAt), "yyyy-MM-dd") === key
|
format(new Date(e.startedAt), "yyyy-MM-dd") === key
|
||||||
);
|
);
|
||||||
if (dayEvents.length === 0) return { label, nap: null, night: null };
|
if (dayEvents.length === 0) return { label, nap: 0, night: 0 };
|
||||||
const napEvents = dayEvents.filter((e) => e.type === "NAP");
|
const napEvents = dayEvents.filter((e) => e.type === "NAP");
|
||||||
const nightEvents = dayEvents.filter((e) => e.type === "NIGHT_SLEEP");
|
const nightEvents = dayEvents.filter((e) => e.type === "NIGHT_SLEEP");
|
||||||
const avg = (evs: Event[]) =>
|
const avg = (evs: Event[]) =>
|
||||||
evs.length === 0
|
evs.length === 0
|
||||||
? null
|
? 0
|
||||||
: Math.round(
|
: Math.round(
|
||||||
evs.reduce(
|
evs.reduce(
|
||||||
(acc, e) => acc + (new Date(e.endedAt!).getTime() - new Date(e.startedAt).getTime()) / 60000,
|
(acc, e) => acc + (new Date(e.endedAt!).getTime() - new Date(e.startedAt).getTime()) / 60000,
|
||||||
|
|||||||
Reference in New Issue
Block a user