fitaiProto/apps/mobile/src/app/(tabs)/index.tsx
echo 5d6166df1b redesign take 2 complete
fix artefacts from previous dessign
2026-03-12 17:56:46 +01:00

779 lines
24 KiB
TypeScript

import {
View,
Text,
StyleSheet,
ScrollView,
RefreshControl,
Image,
Animated,
TouchableOpacity,
} from "react-native";
import { useUser } from "@clerk/clerk-expo";
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Ionicons } from "@expo/vector-icons";
import { useTheme } from "../../contexts/ThemeContext";
import { useStatistics } from "../../contexts/StatisticsContext";
import { useFitnessGoals } from "../../contexts/FitnessGoalsContext";
import { MinimalCard } from "../../components/MinimalCard";
import { SectionHeader } from "../../components/SectionHeader";
import { IconContainer } from "../../components/IconContainer";
import { ProgressBar } from "../../components/ProgressBar";
import { TrackMealModal } from "../../components/TrackMealModal";
import { AddWaterModal } from "../../components/AddWaterModal";
import { ScanFoodModal } from "../../components/ScanFoodModal";
import { ActivityRing } from "../../components/ActivityRing";
import {
checkInsToActivities,
completedGoalsToActivities,
newGoalsToActivities,
combineActivities,
getRecentActivities,
formatActivityTime,
formatDuration,
getActivityIcon,
getActivityEmoji,
ActivityType,
} from "../../utils/activityFeed";
const CALORIE_GOAL = 2000;
const WATER_GOAL = 2000;
const WORKOUT_GOAL = 3;
export default function HomeScreen() {
const { user } = useUser();
const { colors, typography } = useTheme();
const { refetchStatistics, forceRefresh, statistics, loading } =
useStatistics();
const { goals, refetchGoals } = useFitnessGoals();
const [refreshing, setRefreshing] = useState(false);
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
const [scanFoodModalVisible, setScanFoodModalVisible] = useState(false);
const [calories, setCalories] = useState(0);
const [waterIntake, setWaterIntake] = useState(0);
const caloriesBounce = useRef(new Animated.Value(1)).current;
const waterBounce = useRef(new Animated.Value(1)).current;
useFocusEffect(
useCallback(() => {
refetchStatistics();
refetchGoals();
}, [refetchStatistics, refetchGoals]),
);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await Promise.all([forceRefresh(), refetchGoals()]);
setRefreshing(false);
}, [forceRefresh, refetchGoals]);
const getGreeting = () => {
const hour = new Date().getHours();
if (hour < 12) return "Good Morning";
if (hour < 18) return "Good Afternoon";
return "Good Evening";
};
const getMotivationalMessage = () => {
const messages = [
"Let's crush it today! 💪",
"Ready to level up? 🔥",
"You've got this! ⚡",
"Time to shine! ✨",
"Let's make it happen! 🚀",
];
return messages[Math.floor(Math.random() * messages.length)];
};
const handleSaveMeal = (meal: {
type: string;
name: string;
calories: number;
}) => {
setCalories((prev) => prev + meal.calories);
setTrackMealModalVisible(false);
Animated.sequence([
Animated.timing(caloriesBounce, {
toValue: 1.15,
duration: 150,
useNativeDriver: true,
}),
Animated.timing(caloriesBounce, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}),
]).start();
};
const handleAddWater = (amount: number) => {
setWaterIntake((prev) => prev + amount);
setAddWaterModalVisible(false);
Animated.sequence([
Animated.timing(waterBounce, {
toValue: 1.15,
duration: 150,
useNativeDriver: true,
}),
Animated.timing(waterBounce, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}),
]).start();
};
const handleResetCalories = () => setCalories(0);
const handleResetWater = () => setWaterIntake(0);
const handleAddScannedFood = (scannedCalories: number) => {
setCalories((prev) => prev + scannedCalories);
setScanFoodModalVisible(false);
};
const resetAllCounters = async () => {
setCalories(0);
setWaterIntake(0);
const today = new Date().toDateString();
await AsyncStorage.setItem("lastResetDate", today);
await AsyncStorage.removeItem(`calories_${today}`);
await AsyncStorage.removeItem(`water_${today}`);
};
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();
}, []);
useEffect(() => {
const persistCalories = async () => {
const today = new Date().toDateString();
await AsyncStorage.setItem(`calories_${today}`, calories.toString());
};
persistCalories();
}, [calories]);
useEffect(() => {
const persistWater = async () => {
const today = new Date().toDateString();
await AsyncStorage.setItem(`water_${today}`, waterIntake.toString());
};
persistWater();
}, [waterIntake]);
useEffect(() => {
const checkAndResetIfNeeded = async () => {
const lastResetDate = await AsyncStorage.getItem("lastResetDate");
const today = new Date().toDateString();
if (lastResetDate !== today) {
await resetAllCounters();
}
};
checkAndResetIfNeeded();
}, []);
const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0;
const currentStreak = statistics?.attendance.currentStreak || 0;
const workoutsThisWeek = checkInsThisWeek;
const recentActivities = useMemo(() => {
if (!statistics || !goals) return [];
const checkInActivities = checkInsToActivities(statistics.attendance).slice(
0,
2,
);
const completedGoalActivities = completedGoalsToActivities(goals).slice(
0,
2,
);
const newGoalActivities = newGoalsToActivities(goals, 7);
const allActivities = combineActivities(
checkInActivities,
completedGoalActivities,
newGoalActivities,
);
return getRecentActivities(allActivities, 5);
}, [statistics, goals]);
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<ScrollView
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.primary}
/>
}
>
{/* Hero Header */}
<View style={styles.heroHeader}>
<View style={styles.heroTextContainer}>
<Text
style={[
typography.label,
{ color: colors.primary, marginBottom: 4 },
]}
>
{getGreeting()} 👋
</Text>
<Text
style={[
typography.h1,
{ color: colors.textPrimary, marginBottom: 4 },
]}
>
{user?.firstName || "Champion"}
</Text>
<Text style={[typography.body, { color: colors.textSecondary }]}>
{getMotivationalMessage()}
</Text>
</View>
<TouchableOpacity activeOpacity={0.8}>
{user?.imageUrl ? (
<Image
source={{ uri: user.imageUrl }}
style={styles.heroAvatar}
/>
) : (
<View
style={[
styles.heroPlaceholderAvatar,
{ backgroundColor: colors.primary },
]}
>
<Ionicons name="person" size={28} color={colors.white} />
</View>
)}
</TouchableOpacity>
</View>
{/* Activity Rings - Hero Section */}
<View style={styles.section}>
<MinimalCard variant="elevated" style={styles.ringsCard}>
<View style={styles.ringsContainer}>
<ActivityRing
progress={(calories / CALORIE_GOAL) * 100}
current={calories}
goal={CALORIE_GOAL}
label="CALORIES"
color={colors.calories}
size={95}
strokeWidth={9}
icon={
<Text
style={[
typography.statLarge,
{ color: colors.calories, fontSize: 24 },
]}
>
{calories}
</Text>
}
/>
<ActivityRing
progress={(waterIntake / WATER_GOAL) * 100}
current={waterIntake}
goal={WATER_GOAL}
label="WATER (ML)"
color={colors.water}
size={95}
strokeWidth={9}
icon={
<Text
style={[
typography.statLarge,
{ color: colors.water, fontSize: 24 },
]}
>
{waterIntake}
</Text>
}
/>
<ActivityRing
progress={(workoutsThisWeek / WORKOUT_GOAL) * 100}
current={workoutsThisWeek}
goal={WORKOUT_GOAL}
label="WORKOUTS"
color={colors.workouts}
size={95}
strokeWidth={9}
icon={
<Text
style={[
typography.statLarge,
{ color: colors.workouts, fontSize: 24 },
]}
>
{workoutsThisWeek}
</Text>
}
/>
</View>
{/* Streak Banner */}
{currentStreak >= 1 && (
<View
style={[
styles.streakBanner,
{ backgroundColor: `${colors.warning}15` },
]}
>
<Text style={{ fontSize: 20 }}>🔥</Text>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.warning, marginLeft: 8, flex: 1 },
]}
>
{currentStreak} Day Streak!
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Keep it going
</Text>
</View>
)}
</MinimalCard>
</View>
{/* Quick Actions */}
<View style={styles.section}>
<SectionHeader
title="Quick Actions"
subtitle="What do you want to do?"
/>
<View style={styles.quickActionsGrid}>
<TouchableOpacity
onPress={() => console.log("Log workout")}
activeOpacity={0.85}
style={[
styles.quickActionCard,
{ backgroundColor: colors.primary },
]}
>
<View
style={[
styles.quickActionIcon,
{ backgroundColor: "rgba(255,255,255,0.2)" },
]}
>
<Ionicons name="barbell" size={28} color={colors.white} />
</View>
<Text
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
>
Workout
</Text>
<Text
style={[
typography.caption,
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
]}
>
Log your session
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setTrackMealModalVisible(true)}
activeOpacity={0.85}
style={[
styles.quickActionCard,
{ backgroundColor: colors.success },
]}
>
<View
style={[
styles.quickActionIcon,
{ backgroundColor: "rgba(255,255,255,0.2)" },
]}
>
<Ionicons name="restaurant" size={28} color={colors.white} />
</View>
<Text
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
>
Meal
</Text>
<Text
style={[
typography.caption,
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
]}
>
Track calories
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setAddWaterModalVisible(true)}
activeOpacity={0.85}
style={[styles.quickActionCard, { backgroundColor: colors.info }]}
>
<View
style={[
styles.quickActionIcon,
{ backgroundColor: "rgba(255,255,255,0.2)" },
]}
>
<Ionicons name="water" size={28} color={colors.white} />
</View>
<Text
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
>
Hydrate
</Text>
<Text
style={[
typography.caption,
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
]}
>
Add water
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setScanFoodModalVisible(true)}
activeOpacity={0.85}
style={[
styles.quickActionCard,
{ backgroundColor: colors.accent },
]}
>
<View
style={[
styles.quickActionIcon,
{ backgroundColor: "rgba(255,255,255,0.2)" },
]}
>
<Ionicons name="scan" size={28} color={colors.white} />
</View>
<Text
style={[typography.h4, { color: colors.white, marginTop: 12 }]}
>
Scan
</Text>
<Text
style={[
typography.caption,
{ color: "rgba(255,255,255,0.7)", marginTop: 4 },
]}
>
Quick add
</Text>
</TouchableOpacity>
</View>
</View>
{/* Today's Progress */}
<View style={styles.section}>
<SectionHeader
title="Today's Progress"
subtitle="Track your daily goals"
/>
<MinimalCard variant="bordered" style={styles.progressCard}>
<View style={styles.progressHeader}>
<View style={styles.progressLabelRow}>
<View
style={[
styles.progressIcon,
{ backgroundColor: `${colors.calories}20` },
]}
>
<Text style={{ fontSize: 20 }}>🔥</Text>
</View>
<View style={{ marginLeft: 12 }}>
<Text style={[typography.h4, { color: colors.textPrimary }]}>
Calories
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
{calories >= CALORIE_GOAL
? "Goal reached! 🎉"
: `${CALORIE_GOAL - calories} remaining`}
</Text>
</View>
</View>
<Text style={[typography.h3, { color: colors.calories }]}>
{calories}/{CALORIE_GOAL}
</Text>
</View>
<ProgressBar
progress={Math.min(calories / CALORIE_GOAL, 1)}
color={colors.calories}
height={12}
style={{ marginTop: 16 }}
/>
</MinimalCard>
<MinimalCard
variant="bordered"
style={[styles.progressCard, { marginTop: 12 }]}
>
<View style={styles.progressHeader}>
<View style={styles.progressLabelRow}>
<View
style={[
styles.progressIcon,
{ backgroundColor: `${colors.water}20` },
]}
>
<Text style={{ fontSize: 20 }}>💧</Text>
</View>
<View style={{ marginLeft: 12 }}>
<Text style={[typography.h4, { color: colors.textPrimary }]}>
Hydration
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
{waterIntake >= WATER_GOAL
? "Fully hydrated! 🌊"
: `${WATER_GOAL - waterIntake}ml remaining`}
</Text>
</View>
</View>
<Text style={[typography.h3, { color: colors.water }]}>
{waterIntake}/{WATER_GOAL}
</Text>
</View>
<ProgressBar
progress={Math.min(waterIntake / WATER_GOAL, 1)}
color={colors.water}
height={12}
style={{ marginTop: 16 }}
/>
</MinimalCard>
</View>
{/* Recent Activity */}
<View style={styles.section}>
<SectionHeader
title="Recent Activity"
subtitle={
recentActivities.length === 0
? "No activity yet"
: `${recentActivities.length} activities`
}
/>
{recentActivities.length === 0 ? (
<MinimalCard variant="elevated" style={styles.emptyCard}>
<Text style={{ fontSize: 48 }}>🏃</Text>
<Text
style={[
typography.h3,
{
color: colors.textPrimary,
marginTop: 16,
textAlign: "center",
},
]}
>
No Activity Yet
</Text>
<Text
style={[
typography.body,
{
color: colors.textSecondary,
marginTop: 8,
textAlign: "center",
},
]}
>
Start by checking in at the gym or completing a goal!
</Text>
</MinimalCard>
) : (
<View style={styles.activityList}>
{recentActivities.map((activity) => {
const iconName = getActivityIcon(activity.type);
const emoji = getActivityEmoji(activity.type);
const timeStr = formatActivityTime(activity.timestamp);
let iconColor = colors.primary;
if (activity.type === ActivityType.GOAL_COMPLETED)
iconColor = colors.success;
else if (activity.type === ActivityType.GOAL_CREATED)
iconColor = colors.accent;
return (
<MinimalCard
key={activity.id}
variant="bordered"
style={styles.activityItem}
>
<View
style={[
styles.activityIconContainer,
{ backgroundColor: `${iconColor}15` },
]}
>
<Text style={{ fontSize: 22 }}>{emoji}</Text>
</View>
<View style={styles.activityInfo}>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary },
]}
>
{activity.title}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{timeStr}
</Text>
</View>
{activity.duration && (
<Text
style={[
typography.caption,
{ color: colors.textSecondary },
]}
>
{formatDuration(activity.duration)}
</Text>
)}
</MinimalCard>
);
})}
</View>
)}
</View>
<View style={{ height: 100 }} />
</ScrollView>
<TrackMealModal
visible={trackMealModalVisible}
onClose={() => setTrackMealModalVisible(false)}
onSave={handleSaveMeal}
onResetData={handleResetCalories}
/>
<AddWaterModal
visible={addWaterModalVisible}
onClose={() => setAddWaterModalVisible(false)}
onAdd={handleAddWater}
onResetData={handleResetWater}
/>
<ScanFoodModal
visible={scanFoodModalVisible}
onClose={() => setScanFoodModalVisible(false)}
onAddFood={handleAddScannedFood}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
scrollContent: { paddingTop: 60, paddingBottom: 20 },
section: { paddingHorizontal: 20, marginBottom: 28 },
// Hero Header
heroHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
marginBottom: 24,
},
heroTextContainer: { flex: 1, marginRight: 16 },
heroAvatar: {
width: 64,
height: 64,
borderRadius: 20,
borderWidth: 3,
borderColor: "#0066FF",
},
heroPlaceholderAvatar: {
width: 64,
height: 64,
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
borderWidth: 3,
borderColor: "#0066FF",
},
// Activity Rings
ringsCard: { padding: 24 },
ringsContainer: {
flexDirection: "row",
justifyContent: "space-around",
alignItems: "center",
},
streakBanner: {
flexDirection: "row",
alignItems: "center",
marginTop: 24,
padding: 16,
borderRadius: 14,
},
// Quick Actions
quickActionsGrid: { flexDirection: "row", flexWrap: "wrap", gap: 12 },
quickActionCard: {
width: "47%",
padding: 20,
borderRadius: 20,
},
quickActionIcon: {
width: 52,
height: 52,
borderRadius: 14,
justifyContent: "center",
alignItems: "center",
},
// Progress Cards
progressCard: { padding: 20 },
progressHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
progressLabelRow: { flexDirection: "row", alignItems: "center" },
progressIcon: {
width: 48,
height: 48,
borderRadius: 14,
justifyContent: "center",
alignItems: "center",
},
// Activity
activityList: { gap: 12 },
activityItem: { flexDirection: "row", alignItems: "center", padding: 16 },
activityIconContainer: {
width: 48,
height: 48,
borderRadius: 14,
justifyContent: "center",
alignItems: "center",
},
activityInfo: { flex: 1, marginLeft: 14 },
// Empty
emptyCard: { alignItems: "center", padding: 40 },
});