Skip to content

Commit 148a34d

Browse files
committed
Fix rendering flickering, add keepPreviousData, resolve 38 ESLint warnings
1 parent 2ced3b8 commit 148a34d

File tree

10 files changed

+209
-153
lines changed

10 files changed

+209
-153
lines changed

frontend/src/components/ui/version-info.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import { cn } from "../../lib/utils";
22

33
type Environment = "production" | "staging" | "development";
44

5-
const environment = (import.meta.env.VITE_APP_ENVIRONMENT ||
6-
"development") as Environment;
7-
const version = import.meta.env.VITE_APP_VERSION || "dev";
5+
const environment: Environment =
6+
(import.meta.env.VITE_APP_ENVIRONMENT as Environment | undefined) ??
7+
"development";
8+
const version: string =
9+
(import.meta.env.VITE_APP_VERSION as string | undefined) ?? "dev";
810

911
const envStyles: Record<Environment, string> = {
1012
production: "bg-green-600 text-white",

frontend/src/features/dashboard/DashboardLayout.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback, useRef, useEffect } from "react";
1+
import { useState, useCallback, useRef, useEffect, Suspense } from "react";
22
import { NavLink, Outlet } from "react-router-dom";
33
import { useQueryClient } from "@tanstack/react-query";
44
import { toast } from "sonner";
@@ -19,6 +19,7 @@ import {
1919
Dumbbell,
2020
} from "lucide-react";
2121
import { cn } from "../../lib/utils";
22+
import { Spinner } from "../../components/ui/spinner";
2223
import { VersionInfo } from "../../components/ui/version-info";
2324
import { formatCombinedReport } from "../../lib/report-formatter";
2425
import { MAX_BASELINE_DAYS } from "../../lib/metrics";
@@ -204,9 +205,15 @@ export function DashboardLayout() {
204205
</header>
205206

206207
<main className="container py-8 px-4 sm:px-6 lg:px-8">
207-
<div className="animate-fade-in">
208+
<Suspense
209+
fallback={
210+
<div className="flex items-center justify-center h-96">
211+
<Spinner size="lg" />
212+
</div>
213+
}
214+
>
208215
<Outlet />
209-
</div>
216+
</Suspense>
210217
</main>
211218

212219
<footer

frontend/src/features/dashboard/DashboardOverview.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
Scale,
3030
Footprints,
3131
Flame,
32+
Loader2,
3233
type LucideIcon,
3334
} from "lucide-react";
3435
import {
@@ -118,7 +119,10 @@ export function DashboardOverview() {
118119
[startDate, endDate],
119120
);
120121

121-
const { data, isLoading, error } = useHealthDataRange(startDate, endDate);
122+
const { data, isLoading, isFetching, error } = useHealthDataRange(
123+
startDate,
124+
endDate,
125+
);
122126
const { data: syncStatus } = useSyncStatus();
123127
const { isSyncing } = useAutoSync();
124128

@@ -238,14 +242,20 @@ export function DashboardOverview() {
238242
</div>
239243
);
240244
}
241-
return lastSync ? (
245+
return (
242246
<div className="flex items-center gap-2 text-sm text-muted-foreground">
243-
<RefreshCw className="h-3.5 w-3.5" />
244-
<span>
245-
Last sync: {format(new Date(toTimeMs(lastSync)), "PPp")}
246-
</span>
247+
{isFetching ? (
248+
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
249+
) : (
250+
lastSync && <RefreshCw className="h-3.5 w-3.5" />
251+
)}
252+
{lastSync && (
253+
<span>
254+
Last sync: {format(new Date(toTimeMs(lastSync)), "PPp")}
255+
</span>
256+
)}
247257
</div>
248-
) : null;
258+
);
249259
})()}
250260
</div>
251261

frontend/src/features/dashboard/StatisticsPage.tsx

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
Dumbbell,
6868
Beaker,
6969
BarChart3,
70+
Loader2,
7071
type LucideIcon,
7172
} from "lucide-react";
7273
import { cn } from "../../lib/utils";
@@ -132,6 +133,47 @@ function getStressTrendColor(value: number | null): string {
132133
return "text-blue-500";
133134
}
134135

136+
function getCorrelationColor(value: number | null): string {
137+
if (value === null) return "text-muted-foreground";
138+
if (value < -0.3) return "text-green-500";
139+
if (value > 0) return "text-red-500";
140+
return "text-muted-foreground";
141+
}
142+
143+
function getHrvSdColor(value: number | null): string {
144+
if (value === null) return "";
145+
if (value < 0.1) return "text-green-500";
146+
if (value > 0.15) return "text-red-500";
147+
return "";
148+
}
149+
150+
function getTsbColor(value: number | null): string {
151+
if (value === null) return "";
152+
if (value > 0) return "text-green-500";
153+
if (value < -10) return "text-red-500";
154+
return "";
155+
}
156+
157+
function getAllostaticScoreColor(value: number | null): string {
158+
if (value === null) return "text-yellow-500";
159+
if (value < 20) return "text-green-500";
160+
if (value > 40) return "text-red-500";
161+
return "text-yellow-500";
162+
}
163+
164+
function getDysregulationRateColor(rate: number): string {
165+
if (rate > 0.3) return "text-red-500";
166+
if (rate > 0.15) return "text-yellow-500";
167+
return "text-green-500";
168+
}
169+
170+
function getCrossCorrelationColor(value: number | null): string {
171+
if (value === null) return "";
172+
if (value > 0.3) return "text-green-500";
173+
if (value < -0.3) return "text-red-500";
174+
return "";
175+
}
176+
135177
interface DataQualityBadgeProps {
136178
quality: DataQuality;
137179
}
@@ -346,10 +388,14 @@ export function StatisticsPage() {
346388
trendWindow,
347389
useShiftedZScore,
348390
} = modeConfig;
349-
const { data, isLoading, error } = useHealthData(MAX_BASELINE_DAYS, true);
391+
const { data, isLoading, isFetching, error } = useHealthData(
392+
MAX_BASELINE_DAYS,
393+
true,
394+
);
350395
const {
351396
data: analyticsData,
352397
isLoading: analyticsLoading,
398+
isFetching: analyticsFetching,
353399
error: analyticsError,
354400
} = useAnalytics(mode);
355401
const advancedInsights = analyticsData?.advanced_insights;
@@ -541,7 +587,12 @@ export function StatisticsPage() {
541587
Personal baseline deviations and trends
542588
</p>
543589
</div>
544-
<ModeSelector mode={mode} setMode={setMode} />
590+
<div className="flex items-center gap-3">
591+
{(isFetching || analyticsFetching) && (
592+
<Loader2 className="h-4 w-4 animate-spin text-primary" />
593+
)}
594+
<ModeSelector mode={mode} setMode={setMode} />
595+
</div>
545596
</div>
546597

547598
<Card className="border-2">
@@ -1248,13 +1299,7 @@ export function StatisticsPage() {
12481299
<span
12491300
className={cn(
12501301
"font-mono text-sm",
1251-
correlationMetrics.hrvRhrCorrelation !== null &&
1252-
correlationMetrics.hrvRhrCorrelation < -0.3
1253-
? "text-green-500"
1254-
: correlationMetrics.hrvRhrCorrelation !== null &&
1255-
correlationMetrics.hrvRhrCorrelation > 0
1256-
? "text-red-500"
1257-
: "text-muted-foreground",
1302+
getCorrelationColor(correlationMetrics.hrvRhrCorrelation),
12581303
)}
12591304
>
12601305
{correlationMetrics.hrvRhrCorrelation !== null
@@ -1729,16 +1774,9 @@ export function StatisticsPage() {
17291774
<span
17301775
className={cn(
17311776
"font-mono text-sm",
1732-
advancedInsights.hrv_advanced.ln_rmssd_sd_7d !==
1733-
null &&
1734-
advancedInsights.hrv_advanced.ln_rmssd_sd_7d < 0.1
1735-
? "text-green-500"
1736-
: advancedInsights.hrv_advanced.ln_rmssd_sd_7d !==
1737-
null &&
1738-
advancedInsights.hrv_advanced.ln_rmssd_sd_7d >
1739-
0.15
1740-
? "text-red-500"
1741-
: "",
1777+
getHrvSdColor(
1778+
advancedInsights.hrv_advanced.ln_rmssd_sd_7d,
1779+
),
17421780
)}
17431781
>
17441782
{advancedInsights.hrv_advanced.ln_rmssd_sd_7d?.toFixed(
@@ -1920,13 +1958,7 @@ export function StatisticsPage() {
19201958
{advancedInsights.fitness.atl?.toFixed(1) ?? "—"} /{" "}
19211959
<span
19221960
className={cn(
1923-
advancedInsights.fitness.tsb !== null &&
1924-
advancedInsights.fitness.tsb > 0
1925-
? "text-green-500"
1926-
: advancedInsights.fitness.tsb !== null &&
1927-
advancedInsights.fitness.tsb < -10
1928-
? "text-red-500"
1929-
: "",
1961+
getTsbColor(advancedInsights.fitness.tsb),
19301962
)}
19311963
>
19321964
{advancedInsights.fitness.tsb?.toFixed(1) ?? "—"}
@@ -1987,17 +2019,9 @@ export function StatisticsPage() {
19872019
<span
19882020
className={cn(
19892021
"text-2xl font-bold",
1990-
advancedInsights.allostatic_load.composite_score !==
1991-
null &&
1992-
advancedInsights.allostatic_load.composite_score <
1993-
20
1994-
? "text-green-500"
1995-
: advancedInsights.allostatic_load
1996-
.composite_score !== null &&
1997-
advancedInsights.allostatic_load
1998-
.composite_score > 40
1999-
? "text-red-500"
2000-
: "text-yellow-500",
2022+
getAllostaticScoreColor(
2023+
advancedInsights.allostatic_load.composite_score,
2024+
),
20012025
)}
20022026
>
20032027
{advancedInsights.allostatic_load.composite_score?.toFixed(
@@ -2021,11 +2045,7 @@ export function StatisticsPage() {
20212045
<span
20222046
className={cn(
20232047
"font-mono",
2024-
rate > 0.3
2025-
? "text-red-500"
2026-
: rate > 0.15
2027-
? "text-yellow-500"
2028-
: "text-green-500",
2048+
getDysregulationRateColor(rate),
20292049
)}
20302050
>
20312051
{(rate * 100).toFixed(0)}%
@@ -2234,13 +2254,7 @@ export function StatisticsPage() {
22342254
<span
22352255
className={cn(
22362256
"font-mono font-medium",
2237-
pair.correlation !== null &&
2238-
pair.correlation > 0.3
2239-
? "text-green-500"
2240-
: pair.correlation !== null &&
2241-
pair.correlation < -0.3
2242-
? "text-red-500"
2243-
: "",
2257+
getCrossCorrelationColor(pair.correlation),
22442258
)}
22452259
>
22462260
{pair.correlation?.toFixed(2) ?? "—"}

frontend/src/features/dashboard/TrainingsPage.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import {
1010
} from "../../components/ui/card";
1111
import { LoadingState } from "../../components/ui/loading-state";
1212
import { ErrorCard } from "../../components/ui/error-card";
13-
import { Dumbbell, Calendar, Flame, Activity, Heart } from "lucide-react";
13+
import {
14+
Dumbbell,
15+
Calendar,
16+
Flame,
17+
Activity,
18+
Heart,
19+
Loader2,
20+
} from "lucide-react";
1421
import { format, parseISO } from "date-fns";
1522
import type {
1623
WorkoutExerciseDetail,
@@ -431,8 +438,14 @@ function DailyTrainingCard({ day }: { day: DailyTrainings }) {
431438
export function TrainingsPage() {
432439
const [periodDays, setPeriodDays] = useState<PeriodDays>(30);
433440

434-
const { data: workouts, isLoading, error } = useDetailedWorkouts(periodDays);
435-
const { data: healthData } = useHealthData(periodDays);
441+
const {
442+
data: workouts,
443+
isLoading,
444+
isFetching,
445+
error,
446+
} = useDetailedWorkouts(periodDays);
447+
const { data: healthData, isFetching: healthFetching } =
448+
useHealthData(periodDays);
436449

437450
const dailyTrainings = useMemo(() => {
438451
return groupAllTrainingsByDate(
@@ -458,7 +471,9 @@ export function TrainingsPage() {
458471
Detailed workout log from Hevy, Garmin, and Whoop
459472
</p>
460473
</div>
461-
<PeriodSelector period={periodDays} setPeriod={setPeriodDays} />
474+
<div className="flex items-center gap-3">
475+
<PeriodSelector period={periodDays} setPeriod={setPeriodDays} />
476+
</div>
462477
</div>
463478
<LoadingState message="Loading workouts..." />
464479
</div>
@@ -474,7 +489,12 @@ export function TrainingsPage() {
474489
Detailed workout log from Hevy, Garmin, and Whoop
475490
</p>
476491
</div>
477-
<PeriodSelector period={periodDays} setPeriod={setPeriodDays} />
492+
<div className="flex items-center gap-3">
493+
{(isFetching || healthFetching) && (
494+
<Loader2 className="h-4 w-4 animate-spin text-primary" />
495+
)}
496+
<PeriodSelector period={periodDays} setPeriod={setPeriodDays} />
497+
</div>
478498
</div>
479499

480500
{dailyTrainings.length > 0 ? (

frontend/src/hooks/useAnalytics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useQuery } from "@tanstack/react-query";
1+
import { useQuery, keepPreviousData } from "@tanstack/react-query";
22
import { api } from "../lib/api";
33
import { healthKeys } from "../lib/query-keys";
44
import { HEALTH_DATA_STALE_TIME } from "../lib/constants";
@@ -9,5 +9,6 @@ export function useAnalytics(mode: string = "recent") {
99
queryFn: () => api.analytics.get(mode),
1010
staleTime: HEALTH_DATA_STALE_TIME,
1111
refetchOnWindowFocus: true,
12+
placeholderData: keepPreviousData,
1213
});
1314
}
Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { useQuery } from "@tanstack/react-query";
1+
import { useQuery, keepPreviousData } from "@tanstack/react-query";
22
import { api } from "../lib/api";
33
import { format, subDays } from "date-fns";
44
import { healthKeys } from "../lib/query-keys";
5-
import {
6-
HEALTH_DATA_STALE_TIME,
7-
SYNC_REFETCH_INTERVAL,
8-
} from "../lib/constants";
5+
import { HEALTH_DATA_STALE_TIME } from "../lib/constants";
96

107
export function useDetailedWorkouts(days: number = 90) {
118
const today = new Date();
@@ -16,7 +13,7 @@ export function useDetailedWorkouts(days: number = 90) {
1613
queryKey: healthKeys.detailedWorkouts(startDate, endDate),
1714
queryFn: () => api.data.getDetailedWorkouts(startDate, endDate),
1815
staleTime: HEALTH_DATA_STALE_TIME,
19-
refetchInterval: SYNC_REFETCH_INTERVAL,
2016
refetchOnWindowFocus: true,
17+
placeholderData: keepPreviousData,
2118
});
2219
}

0 commit comments

Comments
 (0)