Skip to content

Commit 29de82b

Browse files
authored
Merge pull request #1709 from Skords-01/devin/1777905144-fizruk-datastate-adoption
2 parents 571ee6c + 1d69985 commit 29de82b

1 file changed

Lines changed: 72 additions & 45 deletions

File tree

apps/web/src/modules/fizruk/pages/Workouts.tsx

Lines changed: 72 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { PullToRefresh } from "@shared/components/ui/PullToRefresh";
99
import { Button } from "@shared/components/ui/Button";
1010
import { ConfirmDialog } from "@shared/components/ui/ConfirmDialog";
1111
import { Skeleton } from "@shared/components/ui/Skeleton";
12+
import {
13+
DataState,
14+
type DataStateQueryLike,
15+
} from "@shared/components/ui/DataState";
1216
import { useToast } from "@shared/hooks/useToast";
1317
import { showUndoToast } from "@shared/lib/ui/undoToast";
1418
import { WorkoutTemplatesSection } from "../components/WorkoutTemplatesSection";
@@ -36,6 +40,7 @@ import { useWorkouts } from "../hooks/useWorkouts";
3640
import { recoveryConflictsForExercise } from "@sergeant/fizruk-domain";
3741
import type { RawExerciseDef } from "@sergeant/fizruk-domain/data";
3842
import type {
43+
Workout,
3944
WorkoutFinishSummary,
4045
WorkoutGroup,
4146
WorkoutItem,
@@ -446,6 +451,33 @@ export function Workouts() {
446451
// re-render automatically once the engine writes new state.
447452
const handlePullRefresh = useCallback(() => requestCloudPull(2500), []);
448453

454+
// DataState contract for the workout journal:
455+
// - `useWorkouts` flips `loaded` from false → true after the first
456+
// hydration tick. While `loaded === false` we feed `data: undefined`
457+
// so DataState renders the skeleton slot; from the second tick on
458+
// `data` is the real list (possibly empty), which lets the journal
459+
// render its own "порожньо" empty-state without flashing it during
460+
// mount.
461+
// - `isLoading` mirrors the inverted `loaded` flag so a future
462+
// stale-revalidate (cloud pull → re-merge) keeps the list visible.
463+
const journalQuery: DataStateQueryLike<readonly Workout[]> = {
464+
data: workoutsLoaded ? workouts : undefined,
465+
isLoading: !workoutsLoaded,
466+
};
467+
468+
const workoutsLoadingSkeleton = (
469+
<div
470+
className="space-y-3"
471+
role="status"
472+
aria-live="polite"
473+
aria-label="Завантажуємо тренування"
474+
>
475+
<Skeleton className="h-28 w-full" />
476+
<Skeleton className="h-20 w-full" />
477+
<Skeleton className="h-20 w-full" />
478+
</div>
479+
);
480+
449481
return (
450482
<PullToRefresh onRefresh={handlePullRefresh} variant="fizruk">
451483
<div className="max-w-4xl mx-auto px-4 pt-4 page-tabbar-pad">
@@ -511,51 +543,46 @@ export function Workouts() {
511543
/>
512544
) : null}
513545

514-
{view === "log" && !workoutsLoaded && (
515-
// First-paint placeholder while `useWorkouts` is still rehydrating
516-
// from `localStorage` (one tick on mount). Prevents the "порожньо"
517-
// empty-state from flashing before real data renders — matches the
518-
// Skeleton pattern already used in Finyk.
519-
<div
520-
className="space-y-3"
521-
role="status"
522-
aria-live="polite"
523-
aria-label="Завантажуємо тренування"
524-
>
525-
<Skeleton className="h-28 w-full" />
526-
<Skeleton className="h-20 w-full" />
527-
<Skeleton className="h-20 w-full" />
528-
</div>
529-
)}
530-
{view === "log" && workoutsLoaded && (
531-
<WorkoutJournalSection
532-
activeWorkout={activeWorkout}
533-
activeDuration={activeDuration}
534-
workouts={workouts}
535-
activeWorkoutId={activeWorkoutId}
536-
setActiveWorkoutId={setActiveWorkoutId}
537-
retroOpen={retroOpen}
538-
setRetroOpen={setRetroOpen}
539-
retroDate={retroDate}
540-
setRetroDate={setRetroDate}
541-
retroTime={retroTime}
542-
setRetroTime={setRetroTime}
543-
createWorkout={createWorkout}
544-
setMode={setView}
545-
musclesUk={musclesUk}
546-
recBy={rec.by}
547-
lastByExerciseId={lastByExerciseId}
548-
setRestTimer={setRestTimer}
549-
updateWorkout={updateWorkout}
550-
updateItem={updateItem}
551-
removeItem={removeItemWithUndo}
552-
setFinishFlash={setFinishFlash}
553-
endWorkout={endWorkout}
554-
summarizeWorkoutForFinish={summarizeWorkoutForFinish}
555-
submitRetroWorkout={submitRetroWorkout}
556-
deleteWorkout={deleteWorkout}
557-
restoreWorkout={restoreWorkout}
558-
/>
546+
{view === "log" && (
547+
// DataState contract: `data === undefined` triggers the skeleton
548+
// slot. `useWorkouts` flips `loaded` from false → true after one
549+
// tick on mount (it rehydrates from `localStorage` / SQLite),
550+
// so on first paint we feed `data: undefined` and from the
551+
// second tick onwards `data` is the real list — even when it
552+
// happens to be empty. This prevents the "порожньо" empty-state
553+
// inside `WorkoutJournalSection` from flashing during hydration.
554+
<DataState query={journalQuery} skeleton={workoutsLoadingSkeleton}>
555+
{() => (
556+
<WorkoutJournalSection
557+
activeWorkout={activeWorkout}
558+
activeDuration={activeDuration}
559+
workouts={workouts}
560+
activeWorkoutId={activeWorkoutId}
561+
setActiveWorkoutId={setActiveWorkoutId}
562+
retroOpen={retroOpen}
563+
setRetroOpen={setRetroOpen}
564+
retroDate={retroDate}
565+
setRetroDate={setRetroDate}
566+
retroTime={retroTime}
567+
setRetroTime={setRetroTime}
568+
createWorkout={createWorkout}
569+
setMode={setView}
570+
musclesUk={musclesUk}
571+
recBy={rec.by}
572+
lastByExerciseId={lastByExerciseId}
573+
setRestTimer={setRestTimer}
574+
updateWorkout={updateWorkout}
575+
updateItem={updateItem}
576+
removeItem={removeItemWithUndo}
577+
setFinishFlash={setFinishFlash}
578+
endWorkout={endWorkout}
579+
summarizeWorkoutForFinish={summarizeWorkoutForFinish}
580+
submitRetroWorkout={submitRetroWorkout}
581+
deleteWorkout={deleteWorkout}
582+
restoreWorkout={restoreWorkout}
583+
/>
584+
)}
585+
</DataState>
559586
)}
560587

561588
{view === "templates" && (

0 commit comments

Comments
 (0)