fitaiProto/apps/mobile/src/contexts/NutritionContext.tsx
2026-03-19 03:37:15 +01:00

191 lines
5.0 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
} from "react";
import { useUser, useAuth } from "@clerk/clerk-expo";
import {
getNutritionByDate,
saveDailyNutrition,
addMealEntry,
getMealEntriesByDate,
type NutritionEntry,
type MealEntry,
} from "../api/nutrition";
import log from "../utils/logger";
interface NutritionContextValue {
nutrition: NutritionEntry | null;
meals: MealEntry[];
loading: boolean;
error: Error | null;
calorieGoal: number;
todayCalories: number;
percentage: number;
addMeal: (
data: Omit<MealEntry, "id" | "createdAt" | "dailyNutritionId">,
) => Promise<void>;
updateGoal: (goal: number) => void;
refetch: () => Promise<void>;
}
const NutritionContext = createContext<NutritionContextValue | undefined>(
undefined,
);
export function NutritionProvider({ children }: { children: React.ReactNode }) {
const { user } = useUser();
const { getToken } = useAuth();
const [nutrition, setNutrition] = useState<NutritionEntry | null>(null);
const [meals, setMeals] = useState<MealEntry[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [calorieGoal, setCalorieGoal] = useState(2000); // Default goal: 2000 cal
const today = new Date().toISOString().split("T")[0];
const fetchTodayNutrition = useCallback(async () => {
if (!user?.id) return;
try {
setLoading(true);
setError(null);
const token = await getToken();
// Fetch both nutrition data and meal entries
const [nutritionData, mealsData] = await Promise.all([
getNutritionByDate(today, token),
getMealEntriesByDate(today, token),
]);
if (nutritionData) {
setNutrition(nutritionData);
if (nutritionData.calorieGoal) {
setCalorieGoal(nutritionData.calorieGoal);
}
} else {
setNutrition(null);
}
setMeals(mealsData);
log.debug("Today's nutrition fetched", {
nutritionData,
mealsCount: mealsData.length,
});
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to fetch nutrition", error);
setError(error);
} finally {
setLoading(false);
}
}, [user?.id, getToken, today]);
useEffect(() => {
fetchTodayNutrition();
}, [fetchTodayNutrition]);
const addMeal = useCallback(
async (data: Omit<MealEntry, "id" | "createdAt" | "dailyNutritionId">) => {
if (!user?.id) return;
try {
const token = await getToken();
// Add the meal entry
const meal = await addMealEntry(data, token);
// Recalculate total calories
const currentTotal = nutrition?.totalCalories || 0;
const newTotal = currentTotal + data.calories;
// Update the daily nutrition
const updatedNutrition = await saveDailyNutrition(
{
date: today,
totalCalories: newTotal,
calorieGoal,
meals: [
...(nutrition?.meals || []),
{
type: data.mealType,
name: data.foodName,
calories: data.calories,
time: new Date().toISOString(),
},
],
},
token,
);
setNutrition(updatedNutrition);
setMeals([...meals, meal]);
log.debug("Meal added successfully", { meal, newTotal });
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to add meal", error);
setError(error);
throw error;
}
},
[user?.id, getToken, today, nutrition, calorieGoal, meals],
);
const updateGoal = useCallback(
async (goal: number) => {
setCalorieGoal(goal);
if (user?.id && nutrition) {
try {
const token = await getToken();
await saveDailyNutrition(
{
date: today,
totalCalories: nutrition.totalCalories,
calorieGoal: goal,
meals: nutrition.meals,
},
token,
);
} catch (err) {
log.error("Failed to update calorie goal", err);
}
}
},
[user?.id, nutrition, today, getToken],
);
const todayCalories = nutrition?.totalCalories || 0;
const percentage =
calorieGoal > 0 ? Math.round((todayCalories / calorieGoal) * 100) : 0;
const value: NutritionContextValue = {
nutrition,
meals,
loading,
error,
calorieGoal,
todayCalories,
percentage,
addMeal,
updateGoal,
refetch: fetchTodayNutrition,
};
return (
<NutritionContext.Provider value={value}>
{children}
</NutritionContext.Provider>
);
}
export function useNutrition() {
const context = useContext(NutritionContext);
if (context === undefined) {
throw new Error("useNutrition must be used within a NutritionProvider");
}
return context;
}