191 lines
5.0 KiB
TypeScript
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;
|
|
}
|