216 lines
5.8 KiB
TypeScript
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;
|
|
}
|