diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx index 8cf23f3c3..fab01da16 100644 --- a/src/app/achievements/page.tsx +++ b/src/app/achievements/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; import { Activity, AlertTriangle, @@ -36,20 +35,13 @@ import { EmptyState } from "@/components/ui/empty-state"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { formatDate } from "@/lib/format"; +import { useAchievementsQuery } from "@/lib/queries/use-achievements-query"; import { ACHIEVEMENT_CATEGORY_ORDER, type AchievementCategory, - type AchievementMetrics, type AchievementProgress, - type AchievementSummary, } from "@/lib/gamification/achievements"; -interface AchievementsData { - summary: AchievementSummary; - achievements: AchievementProgress[]; - metrics: AchievementMetrics; -} - const iconMap: Record = { Activity, Flame, @@ -271,16 +263,11 @@ export default function AchievementsPage() { const { isAuthenticated, isLoading: authLoading } = useAuth(); const { t } = useTranslations(); - const { data, isLoading } = useQuery({ - queryKey: ["gamification", "achievements"], - queryFn: async () => { - const res = await fetch("/api/gamification/achievements"); - if (!res.ok) throw new Error("Failed to load achievements"); - const json = await res.json(); - return json.data as AchievementsData; - }, - enabled: isAuthenticated, - }); + // v1.4.34 IW-F-Perf — mother page rides the shared cache slot + // alongside `` and + // `` so the three consumers never trigger + // more than one network call on a cold dashboard mount. + const { data, isLoading } = useAchievementsQuery(); if (authLoading || isLoading) { return ( diff --git a/src/components/gamification/achievement-unlock-notifier.tsx b/src/components/gamification/achievement-unlock-notifier.tsx index 4756f9f5f..cc78ea943 100644 --- a/src/components/gamification/achievement-unlock-notifier.tsx +++ b/src/components/gamification/achievement-unlock-notifier.tsx @@ -1,18 +1,13 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; import { Sparkles, Trophy } from "lucide-react"; import { useEffect, useMemo, useRef } from "react"; import { toast } from "sonner"; import { useTranslations } from "@/lib/i18n/context"; -import type { AchievementProgress } from "@/lib/gamification/achievements"; +import { useAchievementsQuery } from "@/lib/queries/use-achievements-query"; const STORAGE_KEY_PREFIX = "healthlog-achievements-seen"; -interface AchievementsPayload { - achievements: AchievementProgress[]; -} - interface AchievementUnlockNotifierProps { userId: string; } @@ -66,23 +61,16 @@ export function AchievementUnlockNotifier({ const localStateReadyRef = useRef(false); const hasSnapshotRef = useRef(false); - const { data } = useQuery({ - queryKey: ["gamification", "achievements", "unlock-notifier", userId], - queryFn: async () => { - try { - const response = await fetch("/api/gamification/achievements"); - if (!response.ok) { - return { achievements: [] } satisfies AchievementsPayload; - } - const json = await response.json(); - return (json.data ?? { achievements: [] }) as AchievementsPayload; - } catch { - return { achievements: [] } satisfies AchievementsPayload; - } - }, - staleTime: 60 * 1000, + // v1.4.34 IW-F-Perf — the notifier and `` + // both ride the shared `["gamification", "achievements"]` cache cell. + // Before, the notifier carried a per-user discriminator on its key, + // which made TanStack treat it as a fresh cell and fire a second + // network call alongside the dashboard tile. The shared hook owns the + // refetch cadence; the notifier supplies the polling interval so + // long-open tabs still surface new unlocks without forcing the card + // to re-render on the same cadence. + const { data } = useAchievementsQuery({ refetchInterval: 2 * 60 * 1000, - refetchOnWindowFocus: false, }); useEffect(() => { diff --git a/src/components/gamification/recent-achievements-card.tsx b/src/components/gamification/recent-achievements-card.tsx index 1d116945a..32dda5106 100644 --- a/src/components/gamification/recent-achievements-card.tsx +++ b/src/components/gamification/recent-achievements-card.tsx @@ -1,6 +1,5 @@ "use client"; -import { useQuery } from "@tanstack/react-query"; import { Activity, AlertTriangle, @@ -20,14 +19,10 @@ import { type LucideIcon, } from "lucide-react"; import Link from "next/link"; -import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; import { formatDate } from "@/lib/format"; -import type { - AchievementProgress, - AchievementSummary, - AchievementMetrics, -} from "@/lib/gamification/achievements"; +import { useAchievementsQuery } from "@/lib/queries/use-achievements-query"; +import type { AchievementProgress } from "@/lib/gamification/achievements"; const iconMap: Record = { Activity, @@ -46,12 +41,6 @@ const iconMap: Record = { SkipForward, }; -interface AchievementsData { - summary: AchievementSummary; - achievements: AchievementProgress[]; - metrics: AchievementMetrics; -} - /** * Pure helper — pick the N most-recently unlocked achievements. * Achievements without a `completedAt` (legacy unlocks pre-persistence) @@ -84,23 +73,15 @@ const RECENT_LIMIT = 3; * (`achievements` widget id). * * Reuses the same `/api/gamification/achievements` endpoint as the - * dedicated page; TanStack Query dedupes the request when both this - * card and the page render in the same session. + * dedicated page and the unlock notifier; v1.4.34 IW-F-Perf folds all + * three consumers onto the shared `useAchievementsQuery()` hook so a + * single cache slot and a single network call back this dashboard + * tile, the mother page, and the toast notifier. */ export function RecentAchievementsCard() { - const { isAuthenticated } = useAuth(); const { t } = useTranslations(); - const { data } = useQuery({ - queryKey: ["gamification", "achievements"], - queryFn: async () => { - const res = await fetch("/api/gamification/achievements"); - if (!res.ok) throw new Error("Failed to load achievements"); - const json = await res.json(); - return json.data as AchievementsData; - }, - enabled: isAuthenticated, - }); + const { data } = useAchievementsQuery(); const recent = pickRecentUnlocks(data?.achievements ?? [], RECENT_LIMIT);