track meal and add water implemented

This commit is contained in:
dimitar committed 2025-12-01 19:31:07 +01:00
1 parent b0d751540e
commit ec370a3c17
169 files changed
+17981 -5

No files matched your search

+93 -3
View File
@@ -1,15 +1,23 @@
import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native";
import { useUser } from "@clerk/clerk-expo";
import { LinearGradient } from "expo-linear-gradient";
import { useState, useCallback } from "react";
import { useState, useCallback, useEffect } from "react";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { theme } from "../../styles/theme";
import { ActivityWidget } from "../../components/ActivityWidget";
import { QuickActionGrid } from "../../components/QuickActionGrid";
import { TrackMealModal } from "../../components/TrackMealModal";
import { AddWaterModal } from "../../components/AddWaterModal";
import { HydrationWidget } from "../../components/HydrationWidget";
import { Ionicons } from "@expo/vector-icons";
export default function HomeScreen() {
const { user } = useUser();
const [refreshing, setRefreshing] = useState(false);
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
const [calories, setCalories] = useState(0);
const [waterIntake, setWaterIntake] = useState(0);
const onRefresh = useCallback(() => {
setRefreshing(true);
@@ -25,6 +33,65 @@ export default function HomeScreen() {
return "Good Evening";
};
const handleSaveMeal = (meal: { type: string; name: string; calories: number }) => {
setCalories(prev => prev + meal.calories);
setTrackMealModalVisible(false);
};
const handleAddWater = (amount: number) => {
setWaterIntake(prev => prev + amount);
setAddWaterModalVisible(false);
};
const handleResetCalories = () => {
setCalories(0);
};
const handleResetWater = () => {
setWaterIntake(0);
};
const resetAllCounters = async () => {
setCalories(0);
setWaterIntake(0);
const today = new Date().toDateString();
await AsyncStorage.setItem('lastResetDate', today);
};
// Check for midnight reset
useEffect(() => {
const checkAndResetIfNeeded = async () => {
const lastResetDate = await AsyncStorage.getItem('lastResetDate');
const today = new Date().toDateString();
if (lastResetDate !== today) {
await resetAllCounters();
}
};
checkAndResetIfNeeded();
// Calculate time until midnight
const now = new Date();
const midnight = new Date();
midnight.setHours(24, 0, 0, 0);
const timeUntilMidnight = midnight.getTime() - now.getTime();
// Set timer for midnight reset
const midnightTimer = setTimeout(async () => {
await resetAllCounters();
// Set up daily interval after first midnight
const dailyInterval = setInterval(async () => {
await resetAllCounters();
}, 24 * 60 * 60 * 1000); // 24 hours
return () => clearInterval(dailyInterval);
}, timeUntilMidnight);
return () => clearTimeout(midnightTimer);
}, []);
return (
<View style={styles.container}>
<ScrollView
@@ -53,12 +120,35 @@ export default function HomeScreen() {
{/* Activity Widget */}
<ActivityWidget
steps={8432}
calories={640}
calories={calories}
duration={45}
/>
{/* Hydration Widget */}
<HydrationWidget
current={waterIntake}
goal={2500}
/>
{/* Quick Actions */}
<QuickActionGrid />
<QuickActionGrid
onTrackMealPress={() => setTrackMealModalVisible(true)}
onAddWaterPress={() => setAddWaterModalVisible(true)}
/>
<TrackMealModal
visible={trackMealModalVisible}
onClose={() => setTrackMealModalVisible(false)}
onSave={handleSaveMeal}
onResetData={handleResetCalories}
/>
<AddWaterModal
visible={addWaterModalVisible}
onClose={() => setAddWaterModalVisible(false)}
onAdd={handleAddWater}
onResetData={handleResetWater}
/>
{/* Recent Activity Section */}
<View style={styles.section}>
@@ -0,0 +1,224 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, KeyboardAvoidingView, Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
interface AddWaterModalProps {
visible: boolean;
onClose: () => void;
onAdd: (amount: number) => void;
onResetData?: () => void;
}
const QUICK_AMOUNTS = [
{ label: 'Small Glass', amount: 250, icon: 'water-outline' },
{ label: 'Large Glass', amount: 500, icon: 'water' },
{ label: 'Bottle', amount: 750, icon: 'flask' },
];
export function AddWaterModal({ visible, onClose, onAdd, onResetData }: AddWaterModalProps) {
const [customAmount, setCustomAmount] = useState('');
useEffect(() => {
if (visible) {
setCustomAmount('');
}
}, [visible]);
const handleReset = () => {
setCustomAmount('');
if (onResetData) {
onResetData();
}
};
const handleAdd = (amount: number) => {
onAdd(amount);
setCustomAmount('');
};
const handleCustomAdd = () => {
const amount = parseInt(customAmount, 10);
if (amount > 0) {
handleAdd(amount);
}
};
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalContainer}
>
<View style={styles.contentContainer}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Ionicons name="close" size={24} color={theme.colors.gray500} />
</TouchableOpacity>
<Text style={styles.title}>Add Water</Text>
<TouchableOpacity onPress={handleReset}>
<Text style={styles.resetText}>Reset</Text>
</TouchableOpacity>
</View>
<Text style={styles.subtitle}>Quick Add</Text>
<View style={styles.quickAddContainer}>
{QUICK_AMOUNTS.map((item) => (
<TouchableOpacity
key={item.amount}
onPress={() => handleAdd(item.amount)}
style={styles.quickAddItem}
>
<View style={styles.quickAddIcon}>
<Ionicons name={item.icon as any} size={24} color={theme.colors.primary} />
</View>
<Text style={styles.quickAddLabel}>{item.label}</Text>
<Text style={styles.quickAddAmount}>{item.amount}ml</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.subtitle}>Custom Amount</Text>
<View style={styles.customInputContainer}>
<TextInput
style={styles.input}
placeholder="e.g., 300"
value={customAmount}
onChangeText={setCustomAmount}
keyboardType="number-pad"
placeholderTextColor={theme.colors.gray400}
/>
<Text style={styles.unit}>ml</Text>
</View>
<TouchableOpacity onPress={handleCustomAdd} style={styles.addButtonContainer}>
<LinearGradient
colors={theme.gradients.ocean}
style={styles.addButton}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.addButtonText}>Add Water</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</Modal>
);
}
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
contentContainer: {
backgroundColor: '#fff',
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
padding: 24,
paddingBottom: 40,
...theme.shadows.strong,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
title: {
fontSize: 24,
fontWeight: '700',
color: theme.colors.gray900,
},
closeButton: {
padding: 4,
},
resetText: {
fontSize: 16,
color: theme.colors.primary,
fontWeight: '600',
},
subtitle: {
fontSize: 16,
fontWeight: '600',
color: theme.colors.gray700,
marginBottom: 16,
},
quickAddContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 32,
gap: 12,
},
quickAddItem: {
flex: 1,
alignItems: 'center',
padding: 16,
backgroundColor: theme.colors.gray50,
borderRadius: 20,
borderWidth: 1,
borderColor: theme.colors.gray200,
},
quickAddIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: '#e0f2fe',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
quickAddLabel: {
fontSize: 12,
fontWeight: '600',
color: theme.colors.gray800,
marginBottom: 2,
textAlign: 'center',
},
quickAddAmount: {
fontSize: 12,
color: theme.colors.gray500,
},
customInputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 32,
},
input: {
flex: 1,
backgroundColor: theme.colors.gray50,
borderWidth: 1,
borderColor: theme.colors.gray200,
borderRadius: 16,
padding: 16,
fontSize: 16,
color: theme.colors.gray900,
marginRight: 12,
},
unit: {
fontSize: 16,
fontWeight: '600',
color: theme.colors.gray500,
},
addButtonContainer: {
marginTop: 8,
},
addButton: {
paddingVertical: 16,
borderRadius: 20,
alignItems: 'center',
},
addButtonText: {
color: '#fff',
fontSize: 18,
fontWeight: '700',
},
});
@@ -0,0 +1,120 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
interface HydrationWidgetProps {
current: number; // in ml
goal: number; // in ml
}
export function HydrationWidget({ current, goal }: HydrationWidgetProps) {
const percentage = Math.min(Math.max(current / goal, 0), 1);
return (
<View style={styles.container}>
<LinearGradient
colors={['#e0f2fe', '#dbeafe']} // Light blue background
style={[styles.card, theme.shadows.subtle]}
>
<View style={styles.content}>
<View style={styles.iconContainer}>
<LinearGradient
colors={theme.gradients.ocean}
style={styles.icon}
>
<Ionicons name="water" size={24} color="#fff" />
</LinearGradient>
</View>
<View style={styles.info}>
<Text style={styles.title}>Hydration</Text>
<Text style={styles.subtitle}>{current} / {goal} ml</Text>
</View>
<View style={styles.percentageContainer}>
<Text style={styles.percentage}>{Math.round(percentage * 100)}%</Text>
</View>
</View>
<View style={styles.progressContainer}>
<View style={styles.progressBarBg}>
<LinearGradient
colors={theme.gradients.ocean}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.progressBarFill, { width: `${percentage * 100}%` }]}
/>
</View>
</View>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
marginBottom: 24,
},
card: {
borderRadius: 24,
padding: 20,
borderWidth: 1,
borderColor: '#bfdbfe',
},
content: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
iconContainer: {
marginRight: 16,
},
icon: {
width: 48,
height: 48,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
info: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: '700',
color: theme.colors.gray900,
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: theme.colors.gray600,
fontWeight: '500',
},
percentageContainer: {
backgroundColor: '#fff',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
percentage: {
fontSize: 14,
fontWeight: '700',
color: theme.colors.primary,
},
progressContainer: {
height: 8,
},
progressBarBg: {
height: 8,
backgroundColor: 'rgba(255, 255, 255, 0.5)',
borderRadius: 4,
overflow: 'hidden',
},
progressBarFill: {
height: '100%',
borderRadius: 4,
},
});
@@ -36,7 +36,12 @@ function QuickActionItem({ icon, label, gradient, onPress }: QuickActionProps) {
);
}
export function QuickActionGrid() {
interface QuickActionGridProps {
onTrackMealPress?: () => void;
onAddWaterPress?: () => void;
}
export function QuickActionGrid({ onTrackMealPress, onAddWaterPress }: QuickActionGridProps) {
return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>Quick Actions</Text>
@@ -50,11 +55,13 @@ export function QuickActionGrid() {
icon="restaurant"
label="Track Meal"
gradient={theme.gradients.success}
onPress={onTrackMealPress}
/>
<QuickActionItem
icon="water"
label="Add Water"
gradient={theme.gradients.ocean}
onPress={onAddWaterPress}
/>
<QuickActionItem
icon="scan"
@@ -0,0 +1,244 @@
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Modal, TouchableOpacity, TextInput, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
interface TrackMealModalProps {
visible: boolean;
onClose: () => void;
onSave: (meal: { type: string; name: string; calories: number }) => void;
onResetData?: () => void;
}
const MEAL_TYPES = ['Breakfast', 'Lunch', 'Dinner', 'Snack'];
export function TrackMealModal({ visible, onClose, onSave, onResetData }: TrackMealModalProps) {
const [selectedType, setSelectedType] = useState('Breakfast');
const [foodName, setFoodName] = useState('');
const [calories, setCalories] = useState('');
useEffect(() => {
if (visible) {
setFoodName('');
setCalories('');
setSelectedType('Breakfast');
}
}, [visible]);
const handleReset = () => {
setFoodName('');
setCalories('');
setSelectedType('Breakfast');
if (onResetData) {
onResetData();
}
};
const handleSave = () => {
if (!foodName || !calories) return;
onSave({
type: selectedType,
name: foodName,
calories: parseInt(calories, 10)
});
// Reset form
setFoodName('');
setCalories('');
setSelectedType('Breakfast');
};
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.modalContainer}
>
<View style={styles.contentContainer}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<Ionicons name="close" size={24} color={theme.colors.gray500} />
</TouchableOpacity>
<Text style={styles.title}>Track Meal</Text>
<TouchableOpacity onPress={handleReset}>
<Text style={styles.resetText}>Reset</Text>
</TouchableOpacity>
</View>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Meal Type Selection */}
<Text style={styles.label}>Meal Type</Text>
<View style={styles.typeContainer}>
{MEAL_TYPES.map((type) => (
<TouchableOpacity
key={type}
onPress={() => setSelectedType(type)}
style={[
styles.typeButton,
selectedType === type && styles.typeButtonActive
]}
>
<Text style={[
styles.typeText,
selectedType === type && styles.typeTextActive
]}>{type}</Text>
</TouchableOpacity>
))}
</View>
{/* Food Name Input */}
<Text style={styles.label}>Food Name</Text>
<TextInput
style={styles.input}
placeholder="e.g., Oatmeal with Berries"
value={foodName}
onChangeText={setFoodName}
placeholderTextColor={theme.colors.gray400}
/>
{/* Calories Input */}
<Text style={styles.label}>Calories</Text>
<View style={styles.caloriesInputContainer}>
<TextInput
style={[styles.input, styles.caloriesInput]}
placeholder="0"
value={calories}
onChangeText={setCalories}
keyboardType="number-pad"
placeholderTextColor={theme.colors.gray400}
/>
<Text style={styles.caloriesUnit}>kcal</Text>
</View>
{/* Save Button */}
<TouchableOpacity onPress={handleSave} style={styles.saveButtonContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.saveButton}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.saveButtonText}>Save Meal</Text>
</LinearGradient>
</TouchableOpacity>
</ScrollView>
</View>
</KeyboardAvoidingView>
</Modal>
);
}
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
contentContainer: {
backgroundColor: '#fff',
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
padding: 24,
minHeight: '60%',
...theme.shadows.strong,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
title: {
fontSize: 24,
fontWeight: '700',
color: theme.colors.gray900,
},
closeButton: {
padding: 4,
},
resetText: {
fontSize: 16,
color: theme.colors.primary,
fontWeight: '600',
},
label: {
fontSize: 16,
fontWeight: '600',
color: theme.colors.gray700,
marginBottom: 12,
marginTop: 8,
},
typeContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 24,
},
typeButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: theme.colors.gray100,
borderWidth: 1,
borderColor: theme.colors.gray200,
},
typeButtonActive: {
backgroundColor: theme.colors.primaryLight,
borderColor: theme.colors.primary,
},
typeText: {
fontSize: 14,
fontWeight: '500',
color: theme.colors.gray600,
},
typeTextActive: {
color: '#fff',
fontWeight: '600',
},
input: {
backgroundColor: theme.colors.gray50,
borderWidth: 1,
borderColor: theme.colors.gray200,
borderRadius: 16,
padding: 16,
fontSize: 16,
color: theme.colors.gray900,
marginBottom: 24,
},
caloriesInputContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 32,
},
caloriesInput: {
flex: 1,
marginBottom: 0,
marginRight: 12,
},
caloriesUnit: {
fontSize: 16,
fontWeight: '600',
color: theme.colors.gray500,
},
saveButtonContainer: {
marginTop: 'auto',
marginBottom: 24,
},
saveButton: {
paddingVertical: 16,
borderRadius: 20,
alignItems: 'center',
},
saveButtonText: {
color: '#fff',
fontSize: 18,
fontWeight: '700',
},
});
+1 -1
View File
@@ -1,5 +1,5 @@
export const API_BASE_URL = __DEV__
? 'https://cd1b5e914b16.ngrok-free.app'
? 'https://694d46f62d87.ngrok-free.app'
: 'https://your-production-url.com'
export const API_ENDPOINTS = {