mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
refactor(insights): extract useInsightStatus hook for sub-page status queries
Five insights sub-pages (blutdruck / bmi / gewicht / puls / stimmung) each declared the same 13-line useQuery block plus an identical StatusData interface. The hook routes through the existing queryKeys factory so the cache keys stop drifting on the next typo. Medikamente keeps its bespoke query because the compliance-status payload carries a different shape (per-medication breakdown). Closes W10 simplifier S12.
This commit is contained in:
1 parent
7d625343f9
commit
3b5f06d6b3
6 files changed
+100
-100
No files matched your search
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import { HeartPulse } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightStatus } from "@/hooks/use-insight-status";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
@@ -31,31 +31,13 @@ const HealthChart = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface BloodPressureStatusData {
|
||||
hasProvider: boolean;
|
||||
text: string | null;
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export default function InsightsBlutdruckPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
const { data: status, isLoading: isStatusLoading } = useQuery({
|
||||
queryKey: ["insights", "blood-pressure-status", locale],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/insights/blood-pressure-status?locale=${locale}`,
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = await res.json();
|
||||
return json.data as BloodPressureStatusData;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const { data: status, isLoading: isStatusLoading } =
|
||||
useInsightStatus("blood-pressure");
|
||||
|
||||
const bpTargets =
|
||||
user?.dateOfBirth != null ? getBpTargets(new Date(user.dateOfBirth)) : null;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { Ruler } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightStatus } from "@/hooks/use-insight-status";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -30,13 +30,6 @@ const HealthChart = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface BmiStatusData {
|
||||
hasProvider: boolean;
|
||||
text: string | null;
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
const BMI_BANDS = [
|
||||
{ min: 0, max: 17, color: "#ff5555", opacity: 0.16 },
|
||||
{ min: 17, max: 18.5, color: "#ffb86c", opacity: 0.18 },
|
||||
@@ -47,20 +40,10 @@ const BMI_BANDS = [
|
||||
|
||||
export default function InsightsBmiPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
const { data: status, isLoading: isStatusLoading } = useQuery({
|
||||
queryKey: ["insights", "bmi-status", locale],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/insights/bmi-status?locale=${locale}`);
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = await res.json();
|
||||
return json.data as BmiStatusData;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const { data: status, isLoading: isStatusLoading } = useInsightStatus("bmi");
|
||||
|
||||
if (!user?.heightCm) {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import { Scale } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightStatus } from "@/hooks/use-insight-status";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
@@ -28,29 +28,13 @@ const HealthChart = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface WeightStatusData {
|
||||
hasProvider: boolean;
|
||||
text: string | null;
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export default function InsightsGewichtPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
const { data: status, isLoading: isStatusLoading } = useQuery({
|
||||
queryKey: ["insights", "weight-status", locale],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/insights/weight-status?locale=${locale}`);
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = await res.json();
|
||||
return json.data as WeightStatusData;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const { data: status, isLoading: isStatusLoading } =
|
||||
useInsightStatus("weight");
|
||||
|
||||
const weightBands = user?.heightCm
|
||||
? buildWeightBandsFromHeight(user.heightCm, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import dynamic from "next/dynamic";
|
||||
import { Heart } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightStatus } from "@/hooks/use-insight-status";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { InsightStatusCard } from "@/components/insights/insight-status-card";
|
||||
@@ -34,33 +35,17 @@ const HealthChart = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface PulseStatusData {
|
||||
hasProvider: boolean;
|
||||
text: string | null;
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface AnalyticsData {
|
||||
summaries: Record<string, DataSummary>;
|
||||
}
|
||||
|
||||
export default function InsightsPulsPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
const { data: status, isLoading: isStatusLoading } = useQuery({
|
||||
queryKey: ["insights", "pulse-status", locale],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/insights/pulse-status?locale=${locale}`);
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = await res.json();
|
||||
return json.data as PulseStatusData;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const { data: status, isLoading: isStatusLoading } =
|
||||
useInsightStatus("pulse");
|
||||
|
||||
// v1.4.25 W16a — VO2 max chart-row consumes the same `/api/analytics`
|
||||
// bundle the mother page reads. Sharing the cache key keeps the
|
||||
|
||||
@@ -6,6 +6,7 @@ import Link from "next/link";
|
||||
import { Smile } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useInsightStatus } from "@/hooks/use-insight-status";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -31,33 +32,16 @@ const MoodChart = dynamic(
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
interface MoodStatusData {
|
||||
hasProvider: boolean;
|
||||
text: string | null;
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
interface ComprehensiveMoodData {
|
||||
moodSummary: { count: number } | null;
|
||||
}
|
||||
|
||||
export default function InsightsStimmungPage() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
const { data: status, isLoading: isStatusLoading } = useQuery({
|
||||
queryKey: ["insights", "mood-status", locale],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/insights/mood-status?locale=${locale}`);
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = await res.json();
|
||||
return json.data as MoodStatusData;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
const { data: status, isLoading: isStatusLoading } = useInsightStatus("mood");
|
||||
|
||||
// Reuse the mother-page comprehensive query — TanStack Query
|
||||
// dedups so this is a free cache read for the common case.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/**
|
||||
* Shape returned by every `/api/insights/<metric>-status` endpoint.
|
||||
*
|
||||
* - `hasProvider` — whether the user has configured an AI provider; the
|
||||
* sub-page hides the status card entirely when false.
|
||||
* - `text` — the rendered status string; null when the provider has
|
||||
* not yet produced one.
|
||||
* - `cached` — whether the payload came from the per-locale cache or a
|
||||
* fresh provider round-trip.
|
||||
* - `updatedAt` — ISO timestamp of the cached payload's creation, or
|
||||
* null when the route has never produced a value.
|
||||
*/
|
||||
export interface InsightStatusData {
|
||||
hasProvider: boolean;
|
||||
text: string | null;
|
||||
cached: boolean;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metric slugs the sub-pages render. Each slug is paired with the
|
||||
* Insights-status endpoint and the matching `queryKeys.*` factory so
|
||||
* the cache keys stay aligned across the whole app — a hard-coded
|
||||
* `["insights", "weight-status", locale]` array typoed once is exactly
|
||||
* the class of bug `queryKeys` was introduced to defend against.
|
||||
*/
|
||||
type InsightStatusMetric =
|
||||
| "blood-pressure"
|
||||
| "weight"
|
||||
| "pulse"
|
||||
| "bmi"
|
||||
| "mood"
|
||||
| "medication-compliance";
|
||||
|
||||
const QUERY_KEY_FACTORY: Record<
|
||||
InsightStatusMetric,
|
||||
(locale: string) => readonly unknown[]
|
||||
> = {
|
||||
"blood-pressure": queryKeys.insightsBpStatus,
|
||||
weight: queryKeys.insightsWeightStatus,
|
||||
pulse: queryKeys.insightsPulseStatus,
|
||||
bmi: queryKeys.insightsBmiStatus,
|
||||
mood: queryKeys.insightsMoodStatus,
|
||||
"medication-compliance": queryKeys.insightsMedicationComplianceStatus,
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared loader for the insight-status payload backing
|
||||
* `/insights/<metric>` sub-pages. Five sub-pages previously copy-pasted
|
||||
* the same 13-line useQuery block plus an identical `XxxStatusData`
|
||||
* interface; the hook collapses them onto one helper plus the existing
|
||||
* `queryKeys` factory so the next "add a metric" PR touches one map
|
||||
* entry instead of an entire page module.
|
||||
*
|
||||
* Returns the `useQuery` result narrowed to the `InsightStatusData`
|
||||
* payload — callers stay free to use `data` / `isLoading` / refetch
|
||||
* helpers without an extra unwrap.
|
||||
*/
|
||||
export function useInsightStatus(metric: InsightStatusMetric) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { locale } = useTranslations();
|
||||
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEY_FACTORY[metric](locale),
|
||||
queryFn: async (): Promise<InsightStatusData> => {
|
||||
const res = await fetch(`/api/insights/${metric}-status?locale=${locale}`);
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = (await res.json()) as { data: InsightStatusData };
|
||||
return json.data;
|
||||
},
|
||||
enabled: isAuthenticated,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user