mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
refactor(gamification): collapse achievement consumers onto shared hook
v1.4.34 IW-F-Perf — migrate `<RecentAchievementsCard>`, the `/achievements` mother page, and `<AchievementUnlockNotifier>` from ad-hoc `useQuery` blocks onto `useAchievementsQuery()`. Pre-change the dashboard cold mount fired `/api/gamification/achievements` twice — once for the card, once for the notifier — because the notifier carried a per-user discriminator on its queryKey and TanStack treated it as a fresh cache cell. The shared hook routes all three consumers through the same `queryKeys.gamificationAchievements()` slot, so a single network call now backs every surface. The notifier keeps its 2-minute refetch cadence; long-open tabs still surface new-unlock toasts and the card picks up the same payload from the shared cache without issuing a parallel request.
This commit is contained in:
1 parent
ca7ca0e7d6
commit
3b77b6a073
3 files changed
+23
-67
No files matched your search
@@ -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<string, LucideIcon> = {
|
||||
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 `<RecentAchievementsCard>` and
|
||||
// `<AchievementUnlockNotifier>` so the three consumers never trigger
|
||||
// more than one network call on a cold dashboard mount.
|
||||
const { data, isLoading } = useAchievementsQuery();
|
||||
|
||||
if (authLoading || isLoading) {
|
||||
return (
|
||||
|
||||
@@ -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 `<RecentAchievementsCard>`
|
||||
// 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(() => {
|
||||
|
||||
@@ -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<string, LucideIcon> = {
|
||||
Activity,
|
||||
@@ -46,12 +41,6 @@ const iconMap: Record<string, LucideIcon> = {
|
||||
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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user