mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
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:
1 parent
25f41f88fe
commit
a8149e0bdf
35 files changed
+347
-191
No files matched your search
@@ -77,6 +77,19 @@ function getSavedLocale(): Locale | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Persist a locale choice everywhere the next render / request reads
|
||||
// it: localStorage (client preference), cookie (SSR handoff — layout +
|
||||
// metadata render in the user's language), <html lang>. Called ONLY
|
||||
// alongside the `setActive` flip so a failed bundle load never leaves
|
||||
// the persisted locale pointing at strings the UI isn't showing.
|
||||
function persistLocale(newLocale: Locale) {
|
||||
localStorage.setItem("healthlog-locale", newLocale);
|
||||
// 1-year expiry, Lax samesite, not HttpOnly so the client continues
|
||||
// to own it.
|
||||
document.cookie = `healthlog-locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
||||
document.documentElement.lang = newLocale;
|
||||
}
|
||||
|
||||
export function I18nProvider({
|
||||
children,
|
||||
initialLocale,
|
||||
@@ -142,17 +155,12 @@ export function I18nProvider({
|
||||
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
if (!(locales as readonly string[]).includes(newLocale)) return;
|
||||
localStorage.setItem("healthlog-locale", newLocale);
|
||||
// Also mirror to cookie so SSR (layout, metadata) renders in the
|
||||
// user's language. 1-year expiry, Lax samesite, not HttpOnly so the
|
||||
// client continues to own it.
|
||||
document.cookie = `healthlog-locale=${newLocale}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
||||
document.documentElement.lang = newLocale;
|
||||
|
||||
const cached = getCachedMessages(newLocale);
|
||||
if (cached) {
|
||||
requestedLocaleRef.current = null;
|
||||
setPendingLocale(null);
|
||||
persistLocale(newLocale);
|
||||
setActive({ locale: newLocale, messages: cached });
|
||||
return;
|
||||
}
|
||||
@@ -164,11 +172,16 @@ export function I18nProvider({
|
||||
if (requestedLocaleRef.current !== newLocale) return;
|
||||
requestedLocaleRef.current = null;
|
||||
setPendingLocale(null);
|
||||
// Persist atomically with the flip — never before the bundle is
|
||||
// in hand, so cookie / <html lang> / localStorage can't drift
|
||||
// onto a locale the UI failed to load.
|
||||
persistLocale(newLocale);
|
||||
setActive({ locale: newLocale, messages: loaded });
|
||||
})
|
||||
.catch(() => {
|
||||
// Bundle fetch failed (offline mid-session, …) — stay on the
|
||||
// current locale rather than rendering the new one in EN.
|
||||
// current locale rather than rendering the new one in EN, and
|
||||
// leave cookie / <html lang> / localStorage untouched.
|
||||
if (requestedLocaleRef.current !== newLocale) return;
|
||||
requestedLocaleRef.current = null;
|
||||
setPendingLocale(null);
|
||||
|
||||
@@ -90,6 +90,32 @@ export function getDisplayTransform(
|
||||
return perType[preference] ?? perType.metric;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.16.4 — count metrics are integers by definition; any fractional
|
||||
* part on a stored sample is source noise (HK chunk splitting, device
|
||||
* estimation), so the raw-value surfaces render them with 0 decimals
|
||||
* ("9.689 steps", never "9.688,595 steps"). Every other type keeps the
|
||||
* formatter's default precision (≤3 digits, trailing zeros dropped).
|
||||
*/
|
||||
const INTEGER_DISPLAY_TYPES: ReadonlySet<string> = new Set([
|
||||
"ACTIVITY_STEPS",
|
||||
"FLIGHTS_CLIMBED",
|
||||
"FALL_COUNT",
|
||||
// kcal + daylight minutes — whole units are the meaningful display
|
||||
// grain for these day-cumulative counts.
|
||||
"ACTIVE_ENERGY_BURNED",
|
||||
"TIME_IN_DAYLIGHT",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Display fraction digits for a RAW (untransformed) value of `type`.
|
||||
* `0` for integer count metrics, `undefined` (= formatter default)
|
||||
* otherwise — pass the result straight into `Formatters.number`.
|
||||
*/
|
||||
export function rawDisplayFractionDigits(type: string): number | undefined {
|
||||
return INTEGER_DISPLAY_TYPES.has(type) ? 0 : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a transform's factor to a raw canonical value. Pure helper —
|
||||
* keeps the multiply in one place so the chart, list cell, and tooltip
|
||||
|
||||
Reference in new issue
Block a user