mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(charts): persist the range-tab pick like the overlay toggles
rangePoints lived in plain useState(30), so every chart reset to the 30 d tab on mount while the sibling trend/comparison/target-band toggles on the same card persisted per chart. Add rangePoints to ChartOverlayPrefs (server-validated against the closed 0/7/30/90 set) and hydrate it once the prefs resolve; a range-tab tap now persists through the same PUT the toggles already use.
This commit is contained in:
1 parent
a66a346d0d
commit
3c468b5e00
4 files changed
+96
-1
No files matched your search
@@ -44,6 +44,12 @@ const prefsSchema = z.object({
|
||||
comparisonBaseline: z
|
||||
.enum(["none", "lastMonth", "lastYear"])
|
||||
.default("none"),
|
||||
// v1.30.1 — persisted range-tab selection (M2 QoL fix). Optional so
|
||||
// a client on an older cached bundle that never sends it doesn't
|
||||
// 422. Literal set mirrors `CHART_RANGE_POINTS` in dashboard-layout.
|
||||
rangePoints: z
|
||||
.union([z.literal(0), z.literal(7), z.literal(30), z.literal(90)])
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -632,6 +632,32 @@ export function HealthChart({
|
||||
? overlayPrefs.prefs.comparisonBaseline
|
||||
: compareBaseline;
|
||||
|
||||
// v1.30.1 M2 — hydrate the range tab from the persisted prefs once
|
||||
// they arrive, mirroring the overlay toggles above. A plain
|
||||
// `useState(30)` initializer can't see the persisted value because
|
||||
// the dashboard-layout query resolves after mount; `rangePoints` also
|
||||
// drives several derived memos below by identity, so it stays local
|
||||
// state (unlike the toggles, which read `overlayPrefs.prefs` live)
|
||||
// and is synced in exactly once. `windowOverride` charts (mini mode)
|
||||
// are pinned and never adopt a persisted range. Adjusted during
|
||||
// render (React's documented "adjust state during render" escape
|
||||
// hatch, not an effect — the lint rule that bans ref reads during
|
||||
// render is why this is a second piece of state rather than a ref)
|
||||
// so the very first render that sees the resolved prefs already
|
||||
// reflects them; `rangeHydrated` guards against re-running past the
|
||||
// first hit, so a later cache refetch can't stomp a range the user
|
||||
// already picked this session.
|
||||
const [rangeHydrated, setRangeHydrated] = useState(false);
|
||||
if (
|
||||
!windowOverride &&
|
||||
chartKey &&
|
||||
!rangeHydrated &&
|
||||
typeof overlayPrefs.prefs.rangePoints === "number"
|
||||
) {
|
||||
setRangeHydrated(true);
|
||||
setRangePoints(overlayPrefs.prefs.rangePoints);
|
||||
}
|
||||
|
||||
// v1.4.19 A2 — viewport-aware tick density. Recharts' default is one
|
||||
// tick per data point; the helper caps that at 4-10 visible labels
|
||||
// depending on viewport width so the X-axis reads consistently across
|
||||
@@ -1674,7 +1700,17 @@ export function HealthChart({
|
||||
// v1.12.8 — a range-tab change re-slices `chartData`; the
|
||||
// visible-range stats memo recomputes off the new slice and
|
||||
// the stat strip follows automatically.
|
||||
setRangeHydrated(true);
|
||||
setRangePoints(r.points);
|
||||
// v1.30.1 M2 — persist the pick per chart, same model
|
||||
// as the overlay toggles, so the range survives a
|
||||
// remount instead of resetting to 30 d every visit.
|
||||
if (chartKey && !windowOverride) {
|
||||
overlayPrefs.setPrefs({
|
||||
...overlayPrefs.prefs,
|
||||
rangePoints: r.points,
|
||||
});
|
||||
}
|
||||
}}
|
||||
title={t(r.titleKey)}
|
||||
data-slot="chart-range-tab"
|
||||
|
||||
@@ -323,6 +323,25 @@ export function MoodChart({
|
||||
const effectiveCompareBaseline =
|
||||
!mini && chartKey ? overlayPrefs.prefs.comparisonBaseline : compareBaseline;
|
||||
|
||||
// v1.30.1 M2 — hydrate the range tab from the persisted prefs once
|
||||
// they arrive; see health-chart.tsx for the full rationale (the
|
||||
// dashboard-layout query resolves after mount, and `rangePoints`
|
||||
// stays local state because several memos depend on it by identity).
|
||||
// Adjusted during render, not an effect — React's documented "adjust
|
||||
// state during render" pattern (a second state flag rather than a
|
||||
// ref, since the lint rule bans ref reads during render).
|
||||
const [rangeHydrated, setRangeHydrated] = useState(false);
|
||||
if (
|
||||
!mini &&
|
||||
!windowOverride &&
|
||||
chartKey &&
|
||||
!rangeHydrated &&
|
||||
typeof overlayPrefs.prefs.rangePoints === "number"
|
||||
) {
|
||||
setRangeHydrated(true);
|
||||
setRangePoints(overlayPrefs.prefs.rangePoints);
|
||||
}
|
||||
|
||||
// v1.4.19 A2 — viewport-aware tick density helper.
|
||||
const viewportWidth = useViewportWidth();
|
||||
|
||||
@@ -1047,7 +1066,18 @@ export function MoodChart({
|
||||
aria-pressed={rangePoints === r.points}
|
||||
size="sm"
|
||||
className="min-h-11 px-2 text-xs sm:px-3"
|
||||
onClick={() => setRangePoints(r.points)}
|
||||
onClick={() => {
|
||||
setRangeHydrated(true);
|
||||
setRangePoints(r.points);
|
||||
// v1.30.1 M2 — persist the pick per chart, same
|
||||
// model as the overlay toggles.
|
||||
if (chartKey && !windowOverride) {
|
||||
overlayPrefs.setPrefs({
|
||||
...overlayPrefs.prefs,
|
||||
rangePoints: r.points,
|
||||
});
|
||||
}
|
||||
}}
|
||||
title={t(r.titleKey)}
|
||||
data-slot="chart-range-tab"
|
||||
>
|
||||
|
||||
@@ -306,11 +306,24 @@ export const CHART_OVERLAY_KEYS = [
|
||||
] as const;
|
||||
export type ChartOverlayKey = (typeof CHART_OVERLAY_KEYS)[number];
|
||||
|
||||
/**
|
||||
* v1.30.1 — the range tabs (`7 / 30 / 90 / All`) every chart card
|
||||
* exposes. `0` means "All". Shared here so both the client hook and
|
||||
* the server route validate against the same closed set instead of
|
||||
* each chart component re-declaring it.
|
||||
*/
|
||||
export const CHART_RANGE_POINTS = [0, 7, 30, 90] as const;
|
||||
export type ChartRangePoints = (typeof CHART_RANGE_POINTS)[number];
|
||||
|
||||
export interface ChartOverlayPrefs {
|
||||
showTrendIndicator: boolean;
|
||||
showTrendArrow: boolean;
|
||||
showTargetRange: boolean;
|
||||
comparisonBaseline: ComparisonBaseline;
|
||||
// v1.30.1 — persisted range-tab selection. Undefined = never saved
|
||||
// for this chart yet, so the chart falls back to its own 30 d
|
||||
// default rather than forcing every unopened chart to look "saved".
|
||||
rangePoints?: ChartRangePoints;
|
||||
}
|
||||
|
||||
export const DEFAULT_CHART_OVERLAY_PREFS: ChartOverlayPrefs = {
|
||||
@@ -320,6 +333,13 @@ export const DEFAULT_CHART_OVERLAY_PREFS: ChartOverlayPrefs = {
|
||||
comparisonBaseline: "none",
|
||||
};
|
||||
|
||||
function isChartRangePoints(value: unknown): value is ChartRangePoints {
|
||||
return (
|
||||
typeof value === "number" &&
|
||||
(CHART_RANGE_POINTS as readonly number[]).includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
export type ChartOverlayPrefsMap = Partial<
|
||||
Record<ChartOverlayKey, ChartOverlayPrefs>
|
||||
>;
|
||||
@@ -338,6 +358,9 @@ function coerceChartOverlayPrefs(value: unknown): ChartOverlayPrefs {
|
||||
comparisonBaseline: isComparisonBaseline(candidate.comparisonBaseline)
|
||||
? candidate.comparisonBaseline
|
||||
: "none",
|
||||
rangePoints: isChartRangePoints(candidate.rangePoints)
|
||||
? candidate.rangePoints
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user