fix(ux): honest deletes, stable hydration and tidy numbers after release review

Server and first client render now agree everywhere query state used to
leak into the chrome, checkboxes no longer nest inside buttons, delete
confirmations admit the undo they offer, raw step counts drop their
decimals, the truncated unit label wraps instead, streaks know their
singular, the medication header speaks the user's date format, a failed
coach turn keeps the user's message and says what went wrong, settings
status notes follow a language switch, the locale only flips once its
bundle actually loaded, and the last raw api fetches in hooks move onto
the shared client.
This commit is contained in:
Marc-André Bombeck committed 2026-06-11 03:30:18 +02:00
1 parent 25f41f88fe
commit a8149e0bdf
35 files changed
+347 -191

No files matched your search

@@ -104,8 +104,11 @@ describe("v1.4.43 W11 — dashboard tile-strip skeleton (wiring)", () => {
it("derives `primaryLoading` from the snapshot vs slim query", () => {
const src = readFileSync(PAGE_PATH, "utf8");
// v1.16.4 — `!mounted` pins SSR + the hydration render to the
// skeleton branch so a late-hydrating boundary can't disagree with
// the server HTML (React #418); see `useMounted`.
expect(src).toMatch(
/const\s+primaryLoading\s*=\s*snapshotEnabled\s*\?\s*snapshotQuery\.isLoading\s*:\s*analyticsSlimQuery\.isLoading;/,
/const\s+primaryLoading\s*=\s*!mounted\s*\|\|\s*\(snapshotEnabled\s*\?\s*snapshotQuery\.isLoading\s*:\s*analyticsSlimQuery\.isLoading\);/,
);
});
+3 -1
View File
@@ -218,7 +218,9 @@ export default function InsightsMedikamentePage() {
<span>{categoryLabel}</span>
{med.streak > 0 ? (
<Badge variant="outline" className="ml-auto shrink-0 text-xs">
{t("insights.dayStreak", { count: med.streak })}
{med.streak === 1
? t("insights.dayStreakOne", { count: med.streak })
: t("insights.dayStreak", { count: med.streak })}
</Badge>
) : null}
</div>
+6 -1
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useMounted } from "@/hooks/use-mounted";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import {
MeasurementForm,
@@ -18,6 +19,7 @@ import { useTranslations } from "@/lib/i18n/context";
export default function MeasurementsPage() {
const { isAuthenticated, isLoading } = useAuth();
const mounted = useMounted();
const router = useRouter();
const queryClient = useQueryClient();
const searchParams = useSearchParams();
@@ -73,7 +75,10 @@ export default function MeasurementsPage() {
}
}, [isLoading, isAuthenticated, router]);
if (isLoading) {
// v1.16.4 — `!mounted` keeps the hydration render identical to the
// SSR HTML when this boundary hydrates after `/api/auth/me` settled
// (React #418 family); see `useMounted`.
if (!mounted || isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
+13 -3
View File
@@ -3,6 +3,7 @@
import React, { Suspense, useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { useMounted } from "@/hooks/use-mounted";
import {
Activity,
Droplet,
@@ -128,6 +129,7 @@ export function resolveDashboardFirstPaintGate(input: {
export default function DashboardPage() {
const { isAuthenticated, user } = useAuth();
const mounted = useMounted();
const { t } = useTranslations();
const fmt = useFormatters();
const [quickEntryDialog, setQuickEntryDialog] =
@@ -1310,9 +1312,17 @@ export default function DashboardPage() {
// never fires under snapshot mode, so the empty-state branch
// below flashes for the whole snapshot fetch. Key the loading
// flag off whichever query is actually driving the tiles.
const primaryLoading = snapshotEnabled
? snapshotQuery.isLoading
: analyticsSlimQuery.isLoading;
// v1.16.4 — `!mounted` pins the SSR pass AND the hydration
// render to the skeleton branch. Server-side the data queries
// are `enabled: false` (no auth) → `isLoading: false`, which
// used to select the empty state; a late-hydrating boundary
// then saw `isAuthenticated` already resolved and rendered the
// skeleton instead — React #418. See `useMounted`.
const primaryLoading =
!mounted ||
(snapshotEnabled
? snapshotQuery.isLoading
: analyticsSlimQuery.isLoading);
const { showTileStripSkeleton, showEmptyState } =
resolveDashboardFirstPaintGate({
trendCardCount: trendCards.length,