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

186 lines
4.6 KiB
TypeScript

import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
} from "react";
import { useUser, useAuth } from "@clerk/clerk-expo";
import {
getHydrationByDate,
saveDailyHydration,
type HydrationEntry,
} from "../api/hydration";
import log from "../utils/logger";
interface HydrationContextValue {
hydration: HydrationEntry | null;
loading: boolean;
error: Error | null;
waterGoal: number;
todayTotal: number;
percentage: number;
addWater: (amount: number) => Promise<void>;
resetToday: () => Promise<void>;
setWaterGoal: (goal: number) => void;
refetch: () => Promise<void>;
}
const HydrationContext = createContext<HydrationContextValue | undefined>(
undefined,
);
export function HydrationProvider({ children }: { children: React.ReactNode }) {
const { user } = useUser();
const { getToken } = useAuth();
const [hydration, setHydration] = useState<HydrationEntry | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [waterGoal, setWaterGoal] = useState(2000); // Default goal: 2000ml
const today = new Date().toISOString().split("T")[0];
const fetchTodayHydration = useCallback(async () => {
if (!user?.id) return;
try {
setLoading(true);
setError(null);
const token = await getToken();
const data = await getHydrationByDate(today, token);
if (data) {
setHydration(data);
if (data.waterGoal) {
setWaterGoal(data.waterGoal);
}
} else {
// No data for today yet
setHydration(null);
}
log.debug("Today's hydration fetched", { data });
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to fetch hydration", error);
setError(error);
} finally {
setLoading(false);
}
}, [user?.id, getToken, today]);
useEffect(() => {
fetchTodayHydration();
}, [fetchTodayHydration]);
useEffect(() => {
setHydration(null);
setError(null);
setLoading(false);
setWaterGoal(2000);
if (user?.id) {
log.debug("Hydration state reset for user", { userId: user.id });
} else {
log.debug("Hydration state reset on sign-out");
}
}, [user?.id]);
const addWater = useCallback(
async (amount: number) => {
if (!user?.id) return;
try {
const token = await getToken();
const currentTotal = hydration?.totalWater || 0;
const newTotal = currentTotal + amount;
const entry = await saveDailyHydration(
{
date: today,
totalWater: newTotal,
waterGoal,
entries: [
...(hydration?.entries || []),
{ amount, time: new Date().toISOString() },
],
},
token,
);
setHydration(entry);
log.debug("Water added successfully", { amount, newTotal });
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to add water", error);
setError(error);
throw error; // Re-throw so UI can handle it
}
},
[user?.id, getToken, today, hydration, waterGoal],
);
const resetToday = useCallback(async () => {
if (!user?.id) return;
try {
const token = await getToken();
await saveDailyHydration(
{
date: today,
totalWater: 0,
waterGoal,
entries: [],
},
token,
);
setHydration({
date: today,
totalWater: 0,
waterGoal,
entries: [],
});
log.debug("Hydration reset for today");
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
log.error("Failed to reset hydration", error);
setError(error);
throw error;
}
}, [user?.id, getToken, today, waterGoal]);
const todayTotal = hydration?.totalWater || 0;
const percentage =
waterGoal > 0 ? Math.round((todayTotal / waterGoal) * 100) : 0;
const value: HydrationContextValue = {
hydration,
loading,
error,
waterGoal,
todayTotal,
percentage,
addWater,
resetToday,
setWaterGoal,
refetch: fetchTodayHydration,
};
return (
<HydrationContext.Provider value={value}>
{children}
</HydrationContext.Provider>
);
}
export function useHydration() {
const context = useContext(HydrationContext);
if (context === undefined) {
throw new Error("useHydration must be used within a HydrationProvider");
}
return context;
}