fitaiProto/apps/mobile/src/contexts/FitnessGoalsContext.tsx

216 lines
5.8 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
} from "react";
import { useUser, useAuth } from "@clerk/clerk-expo";
import {
fitnessGoalsService,
type FitnessGoal,
type CreateGoalData,
} from "../services/fitnessGoals";
import log from "../utils/logger";
interface FitnessGoalsContextValue {
goals: FitnessGoal[];
loading: boolean;
error: Error | null;
refetchGoals: () => Promise<void>;
createGoal: (goalData: CreateGoalData) => Promise<FitnessGoal>;
updateGoal: (
id: string,
updates: Partial<FitnessGoal>,
) => Promise<FitnessGoal>;
completeGoal: (id: string) => Promise<FitnessGoal>;
deleteGoal: (id: string) => Promise<void>;
clearCache: () => void;
}
const FitnessGoalsContext = createContext<FitnessGoalsContextValue | undefined>(
undefined,
);
export function FitnessGoalsProvider({
children,
}: {
children: React.ReactNode;
}) {
const { user } = useUser();
const { getToken } = useAuth();
const [goals, setGoals] = useState<FitnessGoal[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
const fetchInProgress = useRef(false);
// Cache goals for 30 seconds to avoid duplicate calls
const CACHE_DURATION = 30000; // 30 seconds
const refetchGoals = useCallback(async () => {
if (!user?.id) return;
// Prevent concurrent fetches
if (fetchInProgress.current) {
log.debug("Fetch already in progress, skipping duplicate call");
return;
}
// Check if we have recent cached data
const now = Date.now();
if (goals.length > 0 && now - lastFetchTime < CACHE_DURATION) {
log.debug("Using cached fitness goals", {
count: goals.length,
age: now - lastFetchTime,
cacheRemaining: CACHE_DURATION - (now - lastFetchTime),
});
return;
}
try {
fetchInProgress.current = true;
setLoading(true);
setError(null);
log.debug("Fetching fresh fitness goals", { userId: user.id });
const token = await getToken();
const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
setGoals(loadedGoals);
setLastFetchTime(now);
log.debug("Fitness goals fetched and cached", {
count: loadedGoals.length,
});
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to fetch fitness goals", error);
setError(error);
} finally {
setLoading(false);
fetchInProgress.current = false;
}
}, [user?.id, getToken, goals.length, lastFetchTime]);
const createGoal = useCallback(
async (goalData: CreateGoalData): Promise<FitnessGoal> => {
if (!user?.id) throw new Error("User not authenticated");
const token = await getToken();
const newGoal = await fitnessGoalsService.createGoal(goalData, token);
// Optimistically update cache
setGoals((prev) => [...prev, newGoal]);
setLastFetchTime(Date.now());
log.debug("Goal created and added to cache", { goalId: newGoal.id });
return newGoal;
},
[user?.id, getToken],
);
const updateGoal = useCallback(
async (id: string, updates: Partial<FitnessGoal>): Promise<FitnessGoal> => {
if (!user?.id) throw new Error("User not authenticated");
const token = await getToken();
const updatedGoal = await fitnessGoalsService.updateGoal(
id,
updates,
token,
);
// Optimistically update cache
setGoals((prev) =>
prev.map((goal) => (goal.id === id ? updatedGoal : goal)),
);
setLastFetchTime(Date.now());
log.debug("Goal updated in cache", { goalId: id });
return updatedGoal;
},
[user?.id, getToken],
);
const completeGoal = useCallback(
async (id: string): Promise<FitnessGoal> => {
if (!user?.id) throw new Error("User not authenticated");
const token = await getToken();
const completedGoal = await fitnessGoalsService.completeGoal(id, token);
// Optimistically update cache
setGoals((prev) =>
prev.map((goal) => (goal.id === id ? completedGoal : goal)),
);
setLastFetchTime(Date.now());
log.debug("Goal completed in cache", { goalId: id });
return completedGoal;
},
[user?.id, getToken],
);
const deleteGoal = useCallback(
async (id: string): Promise<void> => {
if (!user?.id) throw new Error("User not authenticated");
const token = await getToken();
await fitnessGoalsService.deleteGoal(id, token);
// Optimistically update cache
setGoals((prev) => prev.filter((goal) => goal.id !== id));
setLastFetchTime(Date.now());
log.debug("Goal deleted from cache", { goalId: id });
},
[user?.id, getToken],
);
const clearCache = useCallback(() => {
setGoals([]);
setLoading(false);
setLastFetchTime(0);
setError(null);
fetchInProgress.current = false;
log.debug("Fitness goals cache cleared");
}, []);
useEffect(() => {
clearCache();
if (user?.id) {
log.debug("Fitness goals cache reset for user", { userId: user.id });
} else {
log.debug("Fitness goals cache reset on sign-out");
}
}, [user?.id, clearCache]);
return (
<FitnessGoalsContext.Provider
value={{
goals,
loading,
error,
refetchGoals,
createGoal,
updateGoal,
completeGoal,
deleteGoal,
clearCache,
}}
>
{children}
</FitnessGoalsContext.Provider>
);
}
export function useFitnessGoals() {
const context = useContext(FitnessGoalsContext);
if (context === undefined) {
throw new Error(
"useFitnessGoals must be used within a FitnessGoalsProvider",
);
}
return context;
}