feat: feeding heatmap, diaper streak, quick note, handoff summary, weight percentile alert, growth curve labels
- stats: 7×24 feed density heatmap (Mon–Sun × 24h, indigo intensity, dark mode) - stats: diaper streak card — current consecutive days ≥4 couches + best streak for period - dashboard: QuickNoteWidget — inline journal note (sky card) without leaving dashboard - dashboard: HandoffWidget — localStorage "since last visit" digest (feeds/diapers/sleep) shown after 2h gap - growth: replace S0/S1/S2 x-axis with human labels (Naiss., 1sem, 2sem, 1m, 2m … 1 an) - growth: tooltip shows age in full words (e.g. "3 mois 1 sem.") - cron: POST /api/cron/weight-percentile-alert — alerts when baby weight exits configurable WHO band (weight_percentile_min / weight_percentile_max SystemConfig keys, deduped per measurement) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,24 @@ function buildSimpleChartData(logs: GrowthLog[], baby: BabyShape, field: "height
|
||||
|
||||
const inputCls = "w-full px-3 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100";
|
||||
|
||||
function fmtAgeWeeks(weeks: number): string {
|
||||
if (weeks === 0) return "Naiss.";
|
||||
if (weeks < 4) return `${weeks}sem`;
|
||||
const months = Math.round(weeks / 4.345);
|
||||
if (months >= 12) return "1 an";
|
||||
return `${months}m`;
|
||||
}
|
||||
|
||||
function fmtAgeLabel(weeks: number): string {
|
||||
if (weeks === 0) return "Naissance";
|
||||
if (weeks < 4) return `${weeks} semaine${weeks > 1 ? "s" : ""}`;
|
||||
const months = Math.floor(weeks / 4.345);
|
||||
const remWeeks = Math.round(weeks - months * 4.345);
|
||||
if (months >= 12) return "1 an";
|
||||
if (remWeeks > 0) return `${months} mois ${remWeeks} sem.`;
|
||||
return `${months} mois`;
|
||||
}
|
||||
|
||||
function WeightTooltip(props: Record<string, unknown> & { style: React.CSSProperties }) {
|
||||
const { active, payload, label, style } = props as {
|
||||
active?: boolean;
|
||||
@@ -113,7 +131,7 @@ function WeightTooltip(props: Record<string, unknown> & { style: React.CSSProper
|
||||
if (!baby) return null;
|
||||
return (
|
||||
<div style={style} className="px-3 py-2 rounded-lg text-xs">
|
||||
<p className="text-slate-400 mb-1">Semaine {label}</p>
|
||||
<p className="text-slate-400 mb-1">{fmtAgeLabel(label ?? 0)}</p>
|
||||
<p className="font-semibold text-indigo-500">Bébé : {baby.value.toFixed(3)} kg</p>
|
||||
</div>
|
||||
);
|
||||
@@ -341,7 +359,7 @@ export default function GrowthPage() {
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={weightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
|
||||
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={(v) => `S${v}`} axisLine={false} tickLine={false} />
|
||||
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} />
|
||||
<Tooltip content={(props) => <WeightTooltip {...props} style={tooltipStyle} />} />
|
||||
{WHO_CURVE_KEYS.map((key, i) => (
|
||||
@@ -361,9 +379,9 @@ export default function GrowthPage() {
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<LineChart data={heightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
|
||||
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={(v) => `S${v}`} axisLine={false} tickLine={false} />
|
||||
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
|
||||
<Tooltip formatter={(val) => [`${val} cm`, "Taille"]} contentStyle={tooltipStyle} />
|
||||
<Tooltip formatter={(val) => [`${val} cm`, "Taille"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
|
||||
<Line dataKey="value" stroke="#0ea5e9" strokeWidth={2.5} dot={{ r: 4, fill: "#0ea5e9", strokeWidth: 2, stroke: "#fff" }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -377,9 +395,9 @@ export default function GrowthPage() {
|
||||
<ResponsiveContainer width="100%" height={160}>
|
||||
<LineChart data={headData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
|
||||
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={(v) => `S${v}`} axisLine={false} tickLine={false} />
|
||||
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
|
||||
<Tooltip formatter={(val) => [`${val} cm`, "PC"]} contentStyle={tooltipStyle} />
|
||||
<Tooltip formatter={(val) => [`${val} cm`, "PC"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
|
||||
<Line dataKey="value" stroke="#10b981" strokeWidth={2.5} dot={{ r: 4, fill: "#10b981", strokeWidth: 2, stroke: "#fff" }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
Reference in New Issue
Block a user