user report gtound work

This commit is contained in:
dimitar committed 2026-03-19 03:37:15 +01:00
1 parent 1be2de05fa
commit 74f0d0dbed
42 files changed
+8127 -5

No files matched your search

+79
View File
@@ -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 || [];
}
+2
View File
@@ -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";
+146
View File
@@ -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}` } : {},
});
}
+13
View File
@@ -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;
}