From 3c3dfb6cd6adfe510a6220f51b7e33348e323f4d Mon Sep 17 00:00:00 2001 From: echo Date: Tue, 31 Mar 2026 16:17:08 +0200 Subject: [PATCH] hydration calories persistance --- apps/mobile/src/app/(tabs)/index.tsx | 192 +++++++++++++++++++++------ 1 file changed, 148 insertions(+), 44 deletions(-) diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 9ae79de..cc1cdb1 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -8,6 +8,7 @@ import { Animated, TouchableOpacity, Alert, + AppState, } from "react-native"; import { useUser } from "@clerk/clerk-expo"; import { useState, useCallback, useEffect, useRef, useMemo } from "react"; @@ -43,6 +44,22 @@ const CALORIE_GOAL = 2000; const WATER_GOAL = 2000; const WORKOUT_GOAL = 3; const MOTIVATION_KEY_PREFIX = "home-motivation"; +const HOME_METRICS_KEY_PREFIX = "home-metrics"; + +const getLocalDateKey = () => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + +const getMillisecondsUntilNextMidnight = () => { + const now = new Date(); + const nextMidnight = new Date(now); + nextMidnight.setHours(24, 0, 0, 0); + return Math.max(1000, nextMidnight.getTime() - now.getTime()); +}; const getRandomMotivation = () => { const messages = [ @@ -74,12 +91,102 @@ export default function HomeScreen() { const caloriesBounce = useRef(new Animated.Value(1)).current; const waterBounce = useRef(new Animated.Value(1)).current; + const caloriesRef = useRef(0); + const waterRef = useRef(0); + const currentDateRef = useRef(getLocalDateKey()); + const midnightResetTimerRef = useRef | null>( + null, + ); + + useEffect(() => { + caloriesRef.current = calories; + }, [calories]); + + useEffect(() => { + waterRef.current = waterIntake; + }, [waterIntake]); + + const getMetricsStorageKey = useCallback( + () => `${HOME_METRICS_KEY_PREFIX}_${user?.id || "guest"}`, + [user?.id], + ); + + const persistDailyMetrics = useCallback( + async (nextCalories: number, nextWaterIntake: number, dateKey?: string) => { + if (!user?.id) return; + + const targetDate = dateKey || currentDateRef.current; + await AsyncStorage.setItem( + getMetricsStorageKey(), + JSON.stringify({ + date: targetDate, + calories: nextCalories, + waterIntake: nextWaterIntake, + }), + ); + }, + [getMetricsStorageKey, user?.id], + ); + + const reconcileDailyMetrics = useCallback(async () => { + const today = getLocalDateKey(); + currentDateRef.current = today; + + if (!user?.id) { + setCalories(0); + setWaterIntake(0); + return; + } + + const stored = await AsyncStorage.getItem(getMetricsStorageKey()); + if (!stored) { + setCalories(0); + setWaterIntake(0); + await persistDailyMetrics(0, 0, today); + return; + } + + try { + const parsed = JSON.parse(stored) as { + date?: string; + calories?: number; + waterIntake?: number; + }; + + if (parsed.date === today) { + const nextCalories = Number(parsed.calories) || 0; + const nextWater = Number(parsed.waterIntake) || 0; + setCalories(nextCalories); + setWaterIntake(nextWater); + } else { + setCalories(0); + setWaterIntake(0); + await persistDailyMetrics(0, 0, today); + } + } catch { + setCalories(0); + setWaterIntake(0); + await persistDailyMetrics(0, 0, today); + } + }, [getMetricsStorageKey, persistDailyMetrics, user?.id]); + + const scheduleMidnightReset = useCallback(() => { + if (midnightResetTimerRef.current) { + clearTimeout(midnightResetTimerRef.current); + } + + midnightResetTimerRef.current = setTimeout(() => { + void reconcileDailyMetrics(); + scheduleMidnightReset(); + }, getMillisecondsUntilNextMidnight() + 50); + }, [reconcileDailyMetrics]); useFocusEffect( useCallback(() => { + void reconcileDailyMetrics(); refetchStatistics(); refetchGoals(); - }, [refetchStatistics, refetchGoals]), + }, [reconcileDailyMetrics, refetchStatistics, refetchGoals]), ); const onRefresh = useCallback(async () => { @@ -100,7 +207,11 @@ export default function HomeScreen() { name: string; calories: number; }) => { - setCalories((prev) => prev + meal.calories); + setCalories((prev) => { + const next = prev + meal.calories; + void persistDailyMetrics(next, waterRef.current); + return next; + }); setTrackMealModalVisible(false); Animated.sequence([ Animated.timing(caloriesBounce, { @@ -117,7 +228,11 @@ export default function HomeScreen() { }; const handleAddWater = (amount: number) => { - setWaterIntake((prev) => prev + amount); + setWaterIntake((prev) => { + const next = prev + amount; + void persistDailyMetrics(caloriesRef.current, next); + return next; + }); setAddWaterModalVisible(false); Animated.sequence([ Animated.timing(waterBounce, { @@ -133,20 +248,23 @@ export default function HomeScreen() { ]).start(); }; - const handleResetCalories = () => setCalories(0); - const handleResetWater = () => setWaterIntake(0); - const handleAddScannedFood = (scannedCalories: number) => { - setCalories((prev) => prev + scannedCalories); - setScanFoodModalVisible(false); + const handleResetCalories = () => { + setCalories(0); + void persistDailyMetrics(0, waterRef.current); }; - const resetAllCounters = async () => { - setCalories(0); + const handleResetWater = () => { setWaterIntake(0); - const today = new Date().toDateString(); - await AsyncStorage.setItem("lastResetDate", today); - await AsyncStorage.removeItem(`calories_${today}`); - await AsyncStorage.removeItem(`water_${today}`); + void persistDailyMetrics(caloriesRef.current, 0); + }; + + const handleAddScannedFood = (scannedCalories: number) => { + setCalories((prev) => { + const next = prev + scannedCalories; + void persistDailyMetrics(next, waterRef.current); + return next; + }); + setScanFoodModalVisible(false); }; useEffect(() => { @@ -183,42 +301,28 @@ export default function HomeScreen() { }, [user?.id]); useEffect(() => { - const loadPersistedData = async () => { - const today = new Date().toDateString(); - const storedCalories = await AsyncStorage.getItem(`calories_${today}`); - const storedWater = await AsyncStorage.getItem(`water_${today}`); - if (storedCalories) setCalories(parseInt(storedCalories, 10)); - if (storedWater) setWaterIntake(parseInt(storedWater, 10)); - }; - loadPersistedData(); - }, []); + void reconcileDailyMetrics(); + }, [reconcileDailyMetrics]); useEffect(() => { - const persistCalories = async () => { - const today = new Date().toDateString(); - await AsyncStorage.setItem(`calories_${today}`, calories.toString()); - }; - persistCalories(); - }, [calories]); + const appStateSubscription = AppState.addEventListener( + "change", + (state) => { + if (state === "active") { + void reconcileDailyMetrics(); + } + }, + ); - useEffect(() => { - const persistWater = async () => { - const today = new Date().toDateString(); - await AsyncStorage.setItem(`water_${today}`, waterIntake.toString()); - }; - persistWater(); - }, [waterIntake]); + scheduleMidnightReset(); - useEffect(() => { - const checkAndResetIfNeeded = async () => { - const lastResetDate = await AsyncStorage.getItem("lastResetDate"); - const today = new Date().toDateString(); - if (lastResetDate !== today) { - await resetAllCounters(); + return () => { + appStateSubscription.remove(); + if (midnightResetTimerRef.current) { + clearTimeout(midnightResetTimerRef.current); } }; - checkAndResetIfNeeded(); - }, []); + }, [reconcileDailyMetrics, scheduleMidnightReset]); const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0; const currentStreak = statistics?.attendance.currentStreak || 0;