diff --git a/messages/de.json b/messages/de.json index e245834e0..0db2acf0a 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1809,7 +1809,8 @@ "label": "Nicht der richtige Messwert?", "placeholder": "Verfolgen anhand…", "apply": "Übernehmen", - "reset": "Auf Standard zurücksetzen" + "reset": "Auf Standard zurücksetzen", + "error": "Diese Änderung konnte nicht gespeichert werden. Bitte erneut versuchen." }, "beforeAfter": { "summary": "Ø {before} davor → Ø {after} seitdem – eine Veränderung von {delta}.", diff --git a/messages/en.json b/messages/en.json index 0ff68d971..85ae8a4ea 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1809,7 +1809,8 @@ "label": "Not the right measurement?", "placeholder": "Track against…", "apply": "Apply", - "reset": "Reset to default" + "reset": "Reset to default", + "error": "Could not save that change. Try again." }, "beforeAfter": { "summary": "Ø {before} before → Ø {after} since — a change of {delta}.", diff --git a/messages/es.json b/messages/es.json index 19dfcd437..1f8693a3c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1809,7 +1809,8 @@ "label": "¿No es la medición correcta?", "placeholder": "Seguir según…", "apply": "Aplicar", - "reset": "Restablecer al valor predeterminado" + "reset": "Restablecer al valor predeterminado", + "error": "No se pudo guardar ese cambio. Inténtalo de nuevo." }, "beforeAfter": { "summary": "Ø {before} antes → Ø {after} desde entonces: un cambio de {delta}.", diff --git a/messages/fr.json b/messages/fr.json index ef2e1927e..943c80cc9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1809,7 +1809,8 @@ "label": "Ce n'est pas la bonne mesure ?", "placeholder": "Suivre selon…", "apply": "Appliquer", - "reset": "Réinitialiser par défaut" + "reset": "Réinitialiser par défaut", + "error": "Impossible d'enregistrer cette modification. Réessaie." }, "beforeAfter": { "summary": "Ø {before} avant → Ø {after} depuis — un écart de {delta}.", diff --git a/messages/it.json b/messages/it.json index 7e453fc2c..6e947266a 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1809,7 +1809,8 @@ "label": "Non è la misurazione giusta?", "placeholder": "Monitora in base a…", "apply": "Applica", - "reset": "Ripristina predefinito" + "reset": "Ripristina predefinito", + "error": "Impossibile salvare questa modifica. Riprova." }, "beforeAfter": { "summary": "Ø {before} prima → Ø {after} da allora — una variazione di {delta}.", diff --git a/messages/pl.json b/messages/pl.json index 15458dbe6..e5fa6918b 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1809,7 +1809,8 @@ "label": "To nie ten pomiar?", "placeholder": "Śledź według…", "apply": "Zastosuj", - "reset": "Przywróć domyślne" + "reset": "Przywróć domyślne", + "error": "Nie udało się zapisać tej zmiany. Spróbuj ponownie." }, "beforeAfter": { "summary": "Ø {before} przedtem → Ø {after} od tego czasu — zmiana o {delta}.", diff --git a/src/app/insights/pulse/page.tsx b/src/app/insights/pulse/page.tsx index 4ab7decbc..90367402a 100644 --- a/src/app/insights/pulse/page.tsx +++ b/src/app/insights/pulse/page.tsx @@ -228,7 +228,7 @@ export default function InsightsPulsPage() { -
+
{/* diff --git a/src/components/insights/intraday-pulse-chart.tsx b/src/components/insights/intraday-pulse-chart.tsx index e9a29f0d1..4d2c8a69e 100644 --- a/src/components/insights/intraday-pulse-chart.tsx +++ b/src/components/insights/intraday-pulse-chart.tsx @@ -23,6 +23,7 @@ import { userDayKey, shiftDateKey } from "@/lib/tz/format"; import { TileHeader } from "@/components/insights/tile-header"; import { Button } from "@/components/ui/button"; import { QueryErrorCard } from "@/components/ui/query-error-card"; +import { Skeleton } from "@/components/ui/skeleton"; import { BUCKET_MINUTES, type IntradayHrBucket, @@ -185,7 +186,7 @@ export function IntradayPulseChart({ // hue, per the plan's §3.6 hue-family call); the chart inside stays // untouched. The same hue marks the `tension_window` rail card, so // the two S11 surfaces read as one family. - className="bg-card metric-accent space-y-1.5 rounded-xl border p-4" + className="bg-card metric-accent space-y-1.5 rounded-xl border p-4 md:p-6" style={{ "--tile-hue": "var(--tile-stress)" } as React.CSSProperties} > + ) : isError ? ( refetch()} diff --git a/src/components/insights/nutrients/caffeine-card.tsx b/src/components/insights/nutrients/caffeine-card.tsx index e89733df1..49e799711 100644 --- a/src/components/insights/nutrients/caffeine-card.tsx +++ b/src/components/insights/nutrients/caffeine-card.tsx @@ -33,6 +33,13 @@ export function CaffeineCard() { apiGet( `/api/nutrients/daily?nutrient=caffeine&days=${WINDOW_DAYS}`, ), + // v1.30 UI audit (M7) — this card unmounts entirely on a no-data window + // (`hasData` gate below), so a self-hoster who never logs caffeine saw + // the 280px skeleton collapse on every `/insights/nutrients` visit. A + // 60s `staleTime` (the "route swing within a minute is a free cache + // hit" convention used across the insights queries) keeps the flash to + // once per session rather than once per mount. + staleTime: 60_000, }); if (isLoading) { diff --git a/src/components/insights/workout-detail.tsx b/src/components/insights/workout-detail.tsx index 6bc216c64..74e8169c0 100644 --- a/src/components/insights/workout-detail.tsx +++ b/src/components/insights/workout-detail.tsx @@ -124,26 +124,33 @@ export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) { return (
- {renderSportIconBadge(workout.sportType)} -
-

- {workout.source} -

-

- {sportName} -

-

- {formatDateRange( - workout.startedAt, - workout.endedAt, - locale, - timeFormat, - )} -

+
+ {renderSportIconBadge(workout.sportType)} +
+

+ {workout.source} +

+

+ {sportName} +

+

+ {formatDateRange( + workout.startedAt, + workout.endedAt, + locale, + timeFormat, + )} +

+
-
+
{formatDuration(workout.durationSec)} @@ -180,7 +187,7 @@ function StatTile({ icon, label, value, hint }: StatTileProps) { {value} {hint ? ( - {hint} + {hint} ) : null}
); diff --git a/src/components/medications/detail/efficacy/efficacy-tab.tsx b/src/components/medications/detail/efficacy/efficacy-tab.tsx index f5ded3a59..8d0ac882b 100644 --- a/src/components/medications/detail/efficacy/efficacy-tab.tsx +++ b/src/components/medications/detail/efficacy/efficacy-tab.tsx @@ -13,12 +13,14 @@ import { useMemo, useState } from "react"; import dynamic from "next/dynamic"; import { Activity, TrendingUp } from "lucide-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; import { MedicationDetailSection } from "@/components/medications/medication-detail-section"; import { SegmentedToggle } from "@/components/medications/detail/segmented-toggle"; import { TileHeader } from "@/components/insights/tile-header"; import { Glp1PlateauNote } from "@/components/insights/glp1-plateau-note"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { @@ -29,9 +31,10 @@ import { SelectValue, } from "@/components/ui/select"; import { ChartSkeleton } from "@/components/charts/chart-skeleton"; +import { Skeleton } from "@/components/ui/skeleton"; import { queryKeys } from "@/lib/query-keys"; import { apiGet, apiPut } from "@/lib/api/api-fetch"; -import { useTranslations } from "@/lib/i18n/context"; +import { useTranslations, useFormatters } from "@/lib/i18n/context"; import { useAuth } from "@/hooks/use-auth"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; import type { @@ -39,12 +42,18 @@ import type { EfficacyTargetView, } from "@/lib/medications/efficacy/build-efficacy"; +// v1.30 UI audit (M8) — `EfficacyChart` mounts inside `TargetCard`'s own +// `` as a bare 220px `ResponsiveContainer` with no chrome of its own. +// The default `` fallback is a rounded-xl bordered card with +// faux header + a 240/280px band — a card nested in a card, then a ~100px +// height snap when the real chart mounts. A chrome-less skeleton sized to +// the actual chart avoids both. const EfficacyChart = dynamic( () => import("@/components/charts/chart-runtime").then((mod) => ({ default: mod.EfficacyChart, })), - { ssr: false, loading: () => }, + { ssr: false, loading: () => }, ); // v1.28.20 — the estimated active-substance curve (Wirkstoffverlauf) also @@ -223,6 +232,7 @@ function TargetCard({ timezone: string; }) { const { t } = useTranslations(); + const fmt = useFormatters(); const [scaleMode, setScaleMode] = useState<"absolute" | "percent">( "absolute", ); @@ -236,9 +246,9 @@ function TargetCard({ title={target.label} right={ target.primary ? ( - + {t("medications.efficacy.primaryBadge")} - + ) : undefined } /> @@ -294,13 +304,15 @@ function TargetCard({ - {target.levelShift?.present && target.levelShift.nearStart ? ( + {target.levelShift?.present && + target.levelShift.nearStart && + target.levelShift.at ? (

{t("medications.efficacy.levelShift", { - date: formatShort(target.levelShift.at), + date: fmt.date(target.levelShift.at), })}

) : null} @@ -414,6 +426,12 @@ function RetargetControl({ await apiPut(`/api/medications/${medicationId}/efficacy/target`, body); onChanged(); if (clear) setValue(""); + } catch { + // v1.30 UI audit (M9) — the save previously failed silently: the + // buttons re-enabled with no toast, no inline alert, and the + // rejection escaped as an unhandled promise. The user could not + // tell the override was not saved. + toast.error(t("medications.efficacy.retarget.error")); } finally { setBusy(false); } @@ -465,9 +483,3 @@ function RetargetControl({
); } - -function formatShort(iso?: string): string { - if (!iso) return ""; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10); -}