mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
154 lines
4.1 KiB
TypeScript
154 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { queryKeys } from "@/lib/query-keys";
|
|
import { apiGet } from "@/lib/api/api-fetch";
|
|
|
|
/**
|
|
* v1.4.32 — TanStack Query wrapper for `GET /api/workouts`.
|
|
*
|
|
* Consumers:
|
|
* - `/insights/workouts` — the workout list page reads the most
|
|
* recent N workouts and renders them as one-line rows.
|
|
* - `<RecentWorkoutsTile>` — the dashboard tile reads the top 3.
|
|
*
|
|
* Cache key shape:
|
|
* - `["workouts", "recent", { limit, since }]` — keyed on the
|
|
* pagination + filter inputs so the recent-tile and the list page
|
|
* keep separate cache slots without colliding.
|
|
* - `["workouts", id]` — single-workout detail (the workout-detail
|
|
* page consumes this separately via `useWorkoutDetail`).
|
|
*/
|
|
|
|
export interface WorkoutListEntry {
|
|
id: string;
|
|
sportType: string;
|
|
startedAt: string;
|
|
endedAt: string;
|
|
durationSec: number;
|
|
distanceM: number | null;
|
|
activeEnergyKcal: number | null;
|
|
avgHr: number | null;
|
|
maxHr: number | null;
|
|
source: string;
|
|
externalId: string | null;
|
|
}
|
|
|
|
export interface WorkoutListMeta {
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
droppedDuplicates: number;
|
|
}
|
|
|
|
export interface WorkoutListPayload {
|
|
workouts: WorkoutListEntry[];
|
|
meta: WorkoutListMeta;
|
|
}
|
|
|
|
export interface UseWorkoutsOptions {
|
|
/** Page size — defaults to 50, capped at 200 by the server. */
|
|
limit?: number;
|
|
/** Page offset — defaults to 0. */
|
|
offset?: number;
|
|
/**
|
|
* ISO timestamp — only workouts with `startedAt >= since` are
|
|
* returned. The dashboard tile passes a 7-day window; the list page
|
|
* leaves it open so the user can scroll back through history.
|
|
*/
|
|
since?: string;
|
|
/** Optional sport-type filter. Pass through the canonical string. */
|
|
sportType?: string;
|
|
}
|
|
|
|
async function fetchWorkouts(
|
|
opts: UseWorkoutsOptions,
|
|
): Promise<WorkoutListPayload> {
|
|
const params = new URLSearchParams();
|
|
if (opts.limit != null) params.set("limit", String(opts.limit));
|
|
if (opts.offset != null) params.set("offset", String(opts.offset));
|
|
if (opts.since) params.set("since", opts.since);
|
|
if (opts.sportType) params.set("sportType", opts.sportType);
|
|
const query = params.toString();
|
|
const url = query ? `/api/workouts?${query}` : "/api/workouts";
|
|
|
|
return apiGet<WorkoutListPayload>(url);
|
|
}
|
|
|
|
export interface UseWorkoutsResult {
|
|
data: WorkoutListPayload | undefined;
|
|
isLoading: boolean;
|
|
isEmpty: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useWorkouts(opts: UseWorkoutsOptions = {}): UseWorkoutsResult {
|
|
const { isAuthenticated } = useAuth();
|
|
|
|
const query = useQuery({
|
|
queryKey: queryKeys.workoutsRecentList({
|
|
limit: opts.limit,
|
|
offset: opts.offset,
|
|
since: opts.since,
|
|
sportType: opts.sportType,
|
|
}),
|
|
queryFn: () => fetchWorkouts(opts),
|
|
enabled: isAuthenticated,
|
|
staleTime: 60 * 1000,
|
|
});
|
|
|
|
const isEmpty =
|
|
Boolean(isAuthenticated) &&
|
|
query.data !== undefined &&
|
|
query.data.workouts.length === 0;
|
|
|
|
return {
|
|
data: query.data,
|
|
isLoading: query.isLoading,
|
|
isEmpty,
|
|
error: query.error as Error | null,
|
|
};
|
|
}
|
|
|
|
export interface WorkoutDetailPayload extends WorkoutListEntry {
|
|
minHr: number | null;
|
|
stepCount: number | null;
|
|
elevationM: number | null;
|
|
pauseDurationSec: number | null;
|
|
metadata: unknown;
|
|
route: {
|
|
geometry: unknown;
|
|
sampleTimestamps: string[] | null;
|
|
} | null;
|
|
canonicalId: string;
|
|
}
|
|
|
|
async function fetchWorkoutDetail(id: string): Promise<WorkoutDetailPayload> {
|
|
return apiGet<WorkoutDetailPayload>(
|
|
`/api/workouts/${encodeURIComponent(id)}`,
|
|
);
|
|
}
|
|
|
|
export interface UseWorkoutDetailResult {
|
|
data: WorkoutDetailPayload | undefined;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useWorkoutDetail(id: string): UseWorkoutDetailResult {
|
|
const { isAuthenticated } = useAuth();
|
|
const query = useQuery({
|
|
queryKey: queryKeys.workoutDetail(id),
|
|
queryFn: () => fetchWorkoutDetail(id),
|
|
enabled: isAuthenticated && id.length > 0,
|
|
staleTime: 60 * 1000,
|
|
});
|
|
return {
|
|
data: query.data,
|
|
isLoading: query.isLoading,
|
|
error: query.error as Error | null,
|
|
};
|
|
}
|