notification system implemented

need refinements
This commit is contained in:
dimitar committed 2026-03-11 06:07:16 +01:00
1 parent 612259f020
commit 1143f8ca02
33 files changed
+2221 -222

No files matched your search

+10
View File
@@ -13,6 +13,7 @@ import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { attendanceApi, Attendance } from "../../api/attendance";
import { AttendanceCalendar } from "../../components/AttendanceCalendar";
import { useStatistics } from "../../contexts/StatisticsContext";
import { theme } from "../../styles/theme";
import { Animated } from "react-native";
import { getErrorMessage } from "../../utils/error-helpers";
@@ -20,6 +21,7 @@ import log from "../../utils/logger";
export default function AttendanceScreen() {
const { getToken, userId } = useAuth();
const { clearCache: clearStatisticsCache } = useStatistics();
const [loading, setLoading] = useState(true);
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null);
const [history, setHistory] = useState<Attendance[]>([]);
@@ -80,6 +82,10 @@ export default function AttendanceScreen() {
if (!token) return;
await attendanceApi.checkIn("gym", token);
// Clear statistics cache to force refresh on home screen
clearStatisticsCache();
fetchAttendance();
Alert.alert("Success", "Checked in successfully!");
} catch (error: unknown) {
@@ -94,6 +100,10 @@ export default function AttendanceScreen() {
if (!token) return;
await attendanceApi.checkOut(token);
// Clear statistics cache to force refresh on home screen
clearStatisticsCache();
fetchAttendance();
Alert.alert("Success", "Checked out successfully!");
} catch (error: unknown) {
+79 -5
View File
@@ -9,6 +9,7 @@ import {
import { useUser } from "@clerk/clerk-expo";
import { LinearGradient } from "expo-linear-gradient";
import { useState, useCallback, useEffect } from "react";
import { useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { theme } from "../../styles/theme";
import { ActivityWidget } from "../../components/ActivityWidget";
@@ -16,12 +17,18 @@ import { QuickActionGrid } from "../../components/QuickActionGrid";
import { TrackMealModal } from "../../components/TrackMealModal";
import { AddWaterModal } from "../../components/AddWaterModal";
import { HydrationWidget } from "../../components/HydrationWidget";
import { NutritionWidget } from "../../components/NutritionWidget";
import { WeeklyProgressWidget } from "../../components/WeeklyProgressWidget";
import { ScanFoodModal } from "../../components/ScanFoodModal";
import { useStatistics } from "../../contexts/StatisticsContext";
import { Ionicons } from "@expo/vector-icons";
const CALORIE_GOAL = 2000; // kcal
const WATER_GOAL = 2000; // ml
export default function HomeScreen() {
const { user } = useUser();
const { refetchStatistics, forceRefresh } = useStatistics();
const [refreshing, setRefreshing] = useState(false);
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
@@ -29,12 +36,19 @@ export default function HomeScreen() {
const [calories, setCalories] = useState(0);
const [waterIntake, setWaterIntake] = useState(0);
const onRefresh = useCallback(() => {
// Refetch statistics when screen comes into focus
useFocusEffect(
useCallback(() => {
refetchStatistics();
}, [refetchStatistics]),
);
const onRefresh = useCallback(async () => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}, []);
// Force refetch statistics bypassing cache
await forceRefresh();
setRefreshing(false);
}, [forceRefresh]);
const getGreeting = () => {
const hour = new Date().getHours();
@@ -75,8 +89,48 @@ export default function HomeScreen() {
setWaterIntake(0);
const today = new Date().toDateString();
await AsyncStorage.setItem("lastResetDate", today);
await AsyncStorage.removeItem(`calories_${today}`);
await AsyncStorage.removeItem(`water_${today}`);
};
// Load persisted data on mount
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();
}, []);
// Persist calories to AsyncStorage whenever it changes
useEffect(() => {
const persistCalories = async () => {
const today = new Date().toDateString();
await AsyncStorage.setItem(`calories_${today}`, calories.toString());
};
persistCalories();
}, [calories]);
// Persist water intake to AsyncStorage whenever it changes
useEffect(() => {
const persistWater = async () => {
const today = new Date().toDateString();
await AsyncStorage.setItem(`water_${today}`, waterIntake.toString());
};
persistWater();
}, [waterIntake]);
// Check for midnight reset
useEffect(() => {
const checkAndResetIfNeeded = async () => {
@@ -146,6 +200,26 @@ export default function HomeScreen() {
{/* Activity Widget */}
<ActivityWidget calories={calories} />
{/* Quick Action Grid */}
<QuickActionGrid
onTrackMealPress={() => setTrackMealModalVisible(true)}
onAddWaterPress={() => setAddWaterModalVisible(true)}
onScanFoodPress={() => setScanFoodModalVisible(true)}
onLogWorkoutPress={() => {
// TODO: Implement workout logging
console.log("Log workout tapped");
}}
/>
{/* Nutrition Widget */}
<NutritionWidget current={calories} goal={CALORIE_GOAL} />
{/* Hydration Widget */}
<HydrationWidget current={waterIntake} goal={WATER_GOAL} />
{/* Weekly Progress Widget */}
<WeeklyProgressWidget />
<TrackMealModal
visible={trackMealModalVisible}
onClose={() => setTrackMealModalVisible(false)}
+50 -6
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useRef } from "react";
import {
View,
Text,
@@ -15,6 +15,8 @@ import { useUser } from "@clerk/clerk-expo";
import { useFocusEffect } from "expo-router";
import { theme } from "../../styles/theme";
import { useRecommendations } from "../../contexts/RecommendationsContext";
import { useNotifications } from "../../contexts/NotificationsContext";
import { NotificationsModal } from "../../components/NotificationsModal";
import type { Recommendation } from "../../api/recommendations";
import log from "../../utils/logger";
@@ -26,8 +28,20 @@ export default function RecommendationsScreen() {
refetchRecommendations,
generateNewRecommendation,
} = useRecommendations();
const { unreadCount, refetchNotifications } = useNotifications();
const [generating, setGenerating] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [notificationsVisible, setNotificationsVisible] = useState(false);
const handleOpenNotifications = () => {
log.debug("Opening notifications modal", { unreadCount });
setNotificationsVisible(true);
};
const handleCloseNotifications = () => {
log.debug("Closing notifications modal");
setNotificationsVisible(false);
};
// Filter to show only approved recommendations for regular users
const recommendations = allRecommendations.filter(
@@ -37,12 +51,13 @@ export default function RecommendationsScreen() {
useFocusEffect(
useCallback(() => {
refetchRecommendations();
}, [refetchRecommendations]),
refetchNotifications();
}, [refetchRecommendations, refetchNotifications]),
);
const onRefresh = async () => {
setRefreshing(true);
await refetchRecommendations();
await Promise.all([refetchRecommendations(), refetchNotifications()]);
setRefreshing(false);
};
@@ -61,7 +76,7 @@ export default function RecommendationsScreen() {
setGenerating(true);
await generateNewRecommendation({
userId: user.id,
modelProvider: "openai",
modelProvider: "deepseek",
useExternalModel: true,
});
Alert.alert(
@@ -94,6 +109,10 @@ export default function RecommendationsScreen() {
return (
<View style={styles.container}>
<NotificationsModal
visible={notificationsVisible}
onClose={handleCloseNotifications}
/>
<ScrollView
contentContainerStyle={styles.scrollContent}
refreshControl={
@@ -117,9 +136,18 @@ export default function RecommendationsScreen() {
Personalized fitness & nutrition plans
</Text>
</View>
<View style={styles.iconContainer}>
<TouchableOpacity
style={styles.iconContainer}
onPress={handleOpenNotifications}
activeOpacity={0.7}
>
<Ionicons name="sparkles" size={32} color="#fff" />
</View>
{unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{unreadCount}</Text>
</View>
)}
</TouchableOpacity>
</LinearGradient>
{/* Generate Button */}
@@ -314,6 +342,22 @@ const styles = StyleSheet.create({
justifyContent: "center",
alignItems: "center",
},
badge: {
position: "absolute",
top: -4,
right: -4,
backgroundColor: theme.colors.danger,
borderRadius: 10,
width: 20,
height: 20,
justifyContent: "center",
alignItems: "center",
},
badgeText: {
color: "#fff",
fontSize: 12,
fontWeight: "bold",
},
actionContainer: {
paddingHorizontal: 20,
marginBottom: 20,
+27 -11
View File
@@ -7,8 +7,26 @@ import { validateEnv } from "../utils/env";
import { StatisticsProvider } from "../contexts/StatisticsContext";
import { FitnessGoalsProvider } from "../contexts/FitnessGoalsContext";
import { RecommendationsProvider } from "../contexts/RecommendationsContext";
import { NotificationsProvider } from "../contexts/NotificationsContext";
import log from "../utils/logger";
// Wrapper to use notification permissions hook after ClerkLoaded
function AppContent() {
// Import here to avoid hook execution before Clerk is loaded
const {
useNotificationPermissions,
} = require("../hooks/useNotificationPermissions");
useNotificationPermissions();
return (
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="welcome" options={{ headerShown: false }} />
</Stack>
);
}
// Validate environment variables on app startup
try {
const env = validateEnv();
@@ -153,17 +171,15 @@ export default function RootLayout() {
return (
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<StatisticsProvider>
<FitnessGoalsProvider>
<RecommendationsProvider>
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="welcome" options={{ headerShown: false }} />
</Stack>
</RecommendationsProvider>
</FitnessGoalsProvider>
</StatisticsProvider>
<NotificationsProvider>
<StatisticsProvider>
<FitnessGoalsProvider>
<RecommendationsProvider>
<AppContent />
</RecommendationsProvider>
</FitnessGoalsProvider>
</StatisticsProvider>
</NotificationsProvider>
</ClerkLoaded>
</ClerkProvider>
);