notification system implemented
need refinements
This commit is contained in:
1 parent
612259f020
commit
1143f8ca02
33 files changed
+2221
-222
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user