user report gtound work
This commit is contained in:
1 parent
1be2de05fa
commit
74f0d0dbed
42 files changed
+8127
-5
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import { apiClient } from "./client";
|
||||
import { API_ENDPOINTS } from "../config/api";
|
||||
|
||||
/**
|
||||
* Hydration data types
|
||||
*/
|
||||
export interface HydrationEntry {
|
||||
id?: string;
|
||||
date: string; // YYYY-MM-DD
|
||||
totalWater: number; // ml
|
||||
waterGoal: number; // ml
|
||||
entries?: Array<{
|
||||
amount: number; // ml
|
||||
time: string;
|
||||
}>;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update daily hydration
|
||||
*/
|
||||
export async function saveDailyHydration(
|
||||
data: {
|
||||
date: string;
|
||||
totalWater?: number;
|
||||
waterGoal?: number;
|
||||
entries?: HydrationEntry["entries"];
|
||||
},
|
||||
token?: string | null,
|
||||
): Promise<HydrationEntry> {
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
data: { hydration: HydrationEntry };
|
||||
}>(API_ENDPOINTS.HYDRATION.BASE, data, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data.hydration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hydration for a specific date
|
||||
*/
|
||||
export async function getHydrationByDate(
|
||||
date: string,
|
||||
token?: string | null,
|
||||
): Promise<HydrationEntry | null> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: HydrationEntry | null;
|
||||
}>(API_ENDPOINTS.HYDRATION.GET_BY_DATE(date), {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hydration for a date range
|
||||
*/
|
||||
export async function getHydrationByRange(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
token?: string | null,
|
||||
): Promise<HydrationEntry[]> {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: HydrationEntry[];
|
||||
}>(API_ENDPOINTS.HYDRATION.GET_RANGE(startDate, endDate), {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data || [];
|
||||
}
|
||||
@@ -10,4 +10,6 @@ export * from "./statistics";
|
||||
export * from "./fitnessProfile";
|
||||
export * from "./attendance";
|
||||
export * from "./recommendations";
|
||||
export * from "./nutrition";
|
||||
export * from "./hydration";
|
||||
export * from "./client";
|
||||
@@ -0,0 +1,146 @@
|
||||
import { apiClient } from "./client";
|
||||
import { API_ENDPOINTS } from "../config/api";
|
||||
|
||||
/**
|
||||
* Nutrition data types
|
||||
*/
|
||||
export interface NutritionEntry {
|
||||
id?: string;
|
||||
date: string; // YYYY-MM-DD
|
||||
totalCalories: number;
|
||||
calorieGoal: number;
|
||||
meals?: Array<{
|
||||
type: "breakfast" | "lunch" | "dinner" | "snack";
|
||||
name: string;
|
||||
calories: number;
|
||||
time?: string;
|
||||
}>;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface MealEntry {
|
||||
id?: string;
|
||||
dailyNutritionId?: string;
|
||||
mealType: "breakfast" | "lunch" | "dinner" | "snack";
|
||||
foodName: string;
|
||||
calories: number;
|
||||
protein?: number;
|
||||
carbs?: number;
|
||||
fats?: number;
|
||||
timestamp?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update daily nutrition
|
||||
*/
|
||||
export async function saveDailyNutrition(
|
||||
data: {
|
||||
date: string;
|
||||
totalCalories?: number;
|
||||
calorieGoal?: number;
|
||||
meals?: NutritionEntry["meals"];
|
||||
},
|
||||
token?: string | null,
|
||||
): Promise<NutritionEntry> {
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
data: { nutrition: NutritionEntry };
|
||||
}>(API_ENDPOINTS.NUTRITION.BASE, data, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data.nutrition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nutrition for a specific date
|
||||
*/
|
||||
export async function getNutritionByDate(
|
||||
date: string,
|
||||
token?: string | null,
|
||||
): Promise<NutritionEntry | null> {
|
||||
try {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: NutritionEntry | null;
|
||||
}>(API_ENDPOINTS.NUTRITION.GET_BY_DATE(date), {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data;
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nutrition for a date range
|
||||
*/
|
||||
export async function getNutritionByRange(
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
token?: string | null,
|
||||
): Promise<NutritionEntry[]> {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: NutritionEntry[];
|
||||
}>(API_ENDPOINTS.NUTRITION.GET_RANGE(startDate, endDate), {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a meal entry
|
||||
*/
|
||||
export async function addMealEntry(
|
||||
data: {
|
||||
mealType: MealEntry["mealType"];
|
||||
foodName: string;
|
||||
calories: number;
|
||||
protein?: number;
|
||||
carbs?: number;
|
||||
fats?: number;
|
||||
dailyNutritionId?: string;
|
||||
},
|
||||
token?: string | null,
|
||||
): Promise<MealEntry> {
|
||||
const response = await apiClient.post<{
|
||||
success: boolean;
|
||||
data: MealEntry;
|
||||
}>(API_ENDPOINTS.NUTRITION.MEALS, data, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meal entries for a specific date
|
||||
*/
|
||||
export async function getMealEntriesByDate(
|
||||
date: string,
|
||||
token?: string | null,
|
||||
): Promise<MealEntry[]> {
|
||||
const response = await apiClient.get<{
|
||||
success: boolean;
|
||||
data: MealEntry[];
|
||||
}>(`${API_ENDPOINTS.NUTRITION.MEALS}?date=${date}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
return response.data.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a meal entry
|
||||
*/
|
||||
export async function deleteMealEntry(
|
||||
id: string,
|
||||
token?: string | null,
|
||||
): Promise<void> {
|
||||
await apiClient.delete(`${API_ENDPOINTS.NUTRITION.MEALS}?id=${id}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
}
|
||||
@@ -44,6 +44,19 @@ export const API_ENDPOINTS = {
|
||||
HISTORY: "/api/attendance/history",
|
||||
},
|
||||
RECOMMENDATIONS: "/api/recommendations",
|
||||
NUTRITION: {
|
||||
BASE: "/api/nutrition",
|
||||
MEALS: "/api/nutrition/meals",
|
||||
GET_BY_DATE: (date: string) => `/api/nutrition?date=${date}`,
|
||||
GET_RANGE: (startDate: string, endDate: string) =>
|
||||
`/api/nutrition?startDate=${startDate}&endDate=${endDate}`,
|
||||
},
|
||||
HYDRATION: {
|
||||
BASE: "/api/hydration",
|
||||
GET_BY_DATE: (date: string) => `/api/hydration?date=${date}`,
|
||||
GET_RANGE: (startDate: string, endDate: string) =>
|
||||
`/api/hydration?startDate=${startDate}&endDate=${endDate}`,
|
||||
},
|
||||
FITNESS_GOALS: {
|
||||
LIST: "/api/fitness-goals",
|
||||
CREATE: "/api/fitness-goals",
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
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]);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user