redesign take 1

This commit is contained in:
dimitar committed 2026-03-11 08:22:48 +01:00
1 parent 1143f8ca02
commit e3a3c3fccf
19 files changed
+2783 -1493

No files matched your search

+201 -258
View File
@@ -7,46 +7,28 @@ import {
ScrollView,
Alert,
} from "react-native";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import { useAuth } from "@clerk/clerk-expo";
import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { useTheme } from "../../contexts/ThemeContext";
import { MinimalCard } from "../../components/MinimalCard";
import { SectionHeader } from "../../components/SectionHeader";
import { MinimalButton } from "../../components/MinimalButton";
import { Badge } from "../../components/Badge";
import { IconContainer } from "../../components/IconContainer";
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";
import log from "../../utils/logger";
export default function AttendanceScreen() {
const { getToken, userId } = useAuth();
const { colors, typography } = useTheme();
const { clearCache: clearStatisticsCache } = useStatistics();
const [loading, setLoading] = useState(true);
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null);
const [history, setHistory] = useState<Attendance[]>([]);
const pulseAnim = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (activeCheckIn) {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.05,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]),
);
pulse.start();
return () => pulse.stop();
}
}, [activeCheckIn]);
const fetchAttendance = async () => {
try {
@@ -82,10 +64,7 @@ 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) {
@@ -100,10 +79,7 @@ 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) {
@@ -114,143 +90,204 @@ export default function AttendanceScreen() {
if (loading && !history.length) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<View style={[styles.centered, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<Text style={styles.title}>Attendance</Text>
<Text style={styles.subtitle}>Track your gym visits</Text>
</LinearGradient>
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.content}
>
{/* Header */}
<View style={styles.header}>
<Text style={[typography.h1, { color: colors.textPrimary }]}>
Attendance
</Text>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 4 },
]}
>
Track your gym visits
</Text>
</View>
<View style={styles.actionContainer}>
{/* Check In/Out Section */}
<View style={styles.section}>
{activeCheckIn ? (
<LinearGradient
colors={["rgba(16, 185, 129, 0.15)", "rgba(5, 150, 105, 0.1)"]}
style={[styles.activeCard, theme.shadows.medium]}
>
<View style={styles.activeCardContent}>
<View style={styles.activeIconContainer}>
<LinearGradient
colors={theme.gradients.success}
style={styles.activeIcon}
<MinimalCard variant="elevated" style={styles.activeCard}>
<View style={styles.activeHeader}>
<View style={styles.activeHeaderLeft}>
<IconContainer
variant="colored"
backgroundColor={`${colors.success}20`}
size="lg"
>
<Ionicons name="checkmark-circle" size={32} color="#fff" />
</LinearGradient>
</View>
<View style={styles.activeTextContainer}>
<Text style={styles.activeText}>Currently Checked In</Text>
<Text style={styles.timeText}>
Since{" "}
{new Date(activeCheckIn.checkInTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
<Ionicons
name="checkmark-circle"
size={28}
color={colors.success}
/>
</IconContainer>
<View style={{ marginLeft: 12 }}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Currently Checked In
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
Since{" "}
{new Date(activeCheckIn.checkInTime).toLocaleTimeString(
[],
{
hour: "2-digit",
minute: "2-digit",
},
)}
</Text>
</View>
</View>
</View>
<TouchableOpacity onPress={handleCheckOut} activeOpacity={0.8}>
<LinearGradient
colors={theme.gradients.danger}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.checkOutButton, theme.shadows.medium]}
>
<Ionicons
name="log-out-outline"
size={20}
color="#fff"
style={{ marginRight: 8 }}
/>
<Text style={styles.buttonText}>Check Out</Text>
</LinearGradient>
</TouchableOpacity>
</LinearGradient>
<MinimalButton
title="Check Out"
onPress={handleCheckOut}
variant="danger"
size="lg"
style={{ marginTop: 16 }}
/>
</MinimalCard>
) : (
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<TouchableOpacity onPress={handleCheckIn} activeOpacity={0.8}>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.checkInButton, theme.shadows.glow]}
>
<Ionicons
name="log-in-outline"
size={24}
color="#fff"
style={{ marginRight: 8 }}
/>
<Text style={styles.checkInButtonText}>Check In</Text>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
<MinimalButton
title="Check In"
onPress={handleCheckIn}
variant="primary"
size="lg"
/>
)}
</View>
{/* Attendance Calendar */}
{history.length > 0 && <AttendanceCalendar attendanceRecords={history} />}
<View style={styles.section}>
<SectionHeader title="Calendar" />
<MinimalCard variant="default">
<AttendanceCalendar attendanceRecords={history} />
</MinimalCard>
</View>
<Text style={styles.sectionTitle}>Recent History</Text>
{history.map((item) => (
<LinearGradient
key={item.id}
colors={["rgba(255, 255, 255, 1)", "rgba(249, 250, 251, 1)"] as const}
style={[styles.historyItem, theme.shadows.medium]}
>
<View style={styles.historyLeft}>
<View style={styles.historyIconContainer}>
<LinearGradient
colors={
item.checkOutTime
? theme.gradients.success
: theme.gradients.primary
}
style={styles.historyIcon}
{/* Recent History */}
<View style={styles.section}>
<SectionHeader title="Recent History" />
{history.length === 0 ? (
<MinimalCard variant="default">
<View style={styles.emptyState}>
<IconContainer
variant="colored"
backgroundColor={`${colors.primary}15`}
size="lg"
>
<Ionicons
name={item.checkOutTime ? "checkmark" : "time-outline"}
size={16}
color="#fff"
name="calendar-outline"
size={32}
color={colors.primary}
/>
</LinearGradient>
</View>
<View>
<Text style={styles.dateText}>
{new Date(item.checkInTime).toLocaleDateString()}
</IconContainer>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary, marginTop: 16 },
]}
>
No attendance history yet
</Text>
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
</View>
</View>
<View style={styles.timeContainer}>
<Text style={styles.historyTime}>
In:{" "}
{new Date(item.checkInTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
{item.checkOutTime && (
<Text style={styles.historyTime}>
Out:{" "}
{new Date(item.checkOutTime).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
<Text
style={[
typography.body,
{
color: colors.textSecondary,
marginTop: 8,
textAlign: "center",
},
]}
>
Your check-in history will appear here
</Text>
)}
</View>
</MinimalCard>
) : (
<View style={styles.historyList}>
{history.slice(0, 10).map((record, index) => {
const checkIn = new Date(record.checkInTime);
const checkOut = record.checkOutTime
? new Date(record.checkOutTime)
: null;
const duration = checkOut
? Math.round((checkOut.getTime() - checkIn.getTime()) / 60000)
: null;
return (
<MinimalCard key={index} variant="default">
<View style={styles.historyItem}>
<View style={styles.historyLeft}>
<IconContainer
variant="colored"
backgroundColor={
checkOut ? `${colors.success}20` : `${colors.info}20`
}
>
<Ionicons
name={checkOut ? "checkmark-done" : "time"}
size={20}
color={checkOut ? colors.success : colors.info}
/>
</IconContainer>
<View style={{ marginLeft: 12, flex: 1 }}>
<Text
style={[typography.h3, { color: colors.textPrimary }]}
>
{checkIn.toLocaleDateString()}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{checkIn.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
{checkOut &&
` - ${checkOut.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}`}
</Text>
</View>
</View>
{duration && (
<Badge
label={`${duration}m`}
variant="neutral"
size="sm"
/>
)}
</View>
</MinimalCard>
);
})}
</View>
</LinearGradient>
))}
)}
</View>
{/* Bottom Spacer */}
<View style={{ height: 100 }} />
</ScrollView>
);
}
@@ -258,147 +295,53 @@ export default function AttendanceScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
content: {
paddingBottom: 20,
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
content: {
paddingBottom: 20,
},
header: {
paddingHorizontal: 24,
paddingTop: 60,
paddingBottom: 24,
},
section: {
paddingHorizontal: 24,
marginBottom: 24,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
title: {
fontSize: theme.typography.fontSize["3xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
marginBottom: 4,
},
subtitle: {
fontSize: theme.typography.fontSize.base,
color: "rgba(255, 255, 255, 0.9)",
},
actionContainer: {
marginBottom: 32,
paddingHorizontal: 20,
},
checkInButton: {
paddingVertical: 20,
paddingHorizontal: 24,
borderRadius: theme.borderRadius.xl,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
checkInButtonText: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.xl,
fontWeight: theme.typography.fontWeight.semibold,
},
checkOutButton: {
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: theme.borderRadius.lg,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginTop: 16,
},
buttonText: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.base,
fontWeight: theme.typography.fontWeight.semibold,
},
activeCard: {
padding: 20,
borderRadius: theme.borderRadius.xl,
borderWidth: 1,
borderColor: "rgba(16, 185, 129, 0.2)",
},
activeCardContent: {
activeHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
},
activeIconContainer: {
marginRight: 16,
},
activeIcon: {
width: 56,
height: 56,
borderRadius: 28,
justifyContent: "center",
activeHeaderLeft: {
flexDirection: "row",
alignItems: "center",
},
activeTextContainer: {
flex: 1,
},
activeText: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray900,
marginBottom: 4,
},
timeText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray600,
emptyState: {
alignItems: "center",
paddingVertical: 40,
paddingHorizontal: 20,
},
sectionTitle: {
fontSize: theme.typography.fontSize.xl,
fontWeight: theme.typography.fontWeight.semibold,
marginBottom: 16,
paddingHorizontal: 20,
color: theme.colors.gray900,
historyList: {
gap: 12,
},
historyItem: {
padding: 16,
borderRadius: theme.borderRadius.xl,
marginBottom: 12,
marginHorizontal: 20,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
borderWidth: 1,
borderColor: "rgba(59, 130, 246, 0.1)",
},
historyLeft: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
historyIconContainer: {
marginRight: 4,
},
historyIcon: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: "center",
alignItems: "center",
},
dateText: {
fontSize: theme.typography.fontSize.base,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray900,
},
typeText: {
fontSize: theme.typography.fontSize.xs,
color: theme.colors.gray600,
marginTop: 2,
},
timeContainer: {
alignItems: "flex-end",
},
historyTime: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray700,
flex: 1,
},
});
+217 -219
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef } from "react";
import React, { useState, useCallback } from "react";
import {
View,
Text,
@@ -6,27 +6,31 @@ import {
ScrollView,
RefreshControl,
TouchableOpacity,
Animated,
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { theme } from "../../styles/theme";
import { useUser } from "@clerk/clerk-expo";
import { useFocusEffect } from "expo-router";
import * as SecureStore from "expo-secure-store";
import { useTheme } from "../../contexts/ThemeContext";
import { MinimalCard } from "../../components/MinimalCard";
import { SectionHeader } from "../../components/SectionHeader";
import { MinimalButton } from "../../components/MinimalButton";
import { Badge } from "../../components/Badge";
import { ProgressBar } from "../../components/ProgressBar";
import { GoalProgressCard } from "../../components/GoalProgressCard";
import { GoalCreationModal } from "../../components/GoalCreationModal";
import { WeeklyProgressChart } from "../../components/WeeklyProgressChart";
import { GoalTypeBreakdownChart } from "../../components/GoalTypeBreakdownChart";
import { useUser } from "@clerk/clerk-expo";
import type { FitnessGoal, CreateGoalData } from "../../services/fitnessGoals";
import { useStatistics } from "../../contexts/StatisticsContext";
import { useFitnessGoals } from "../../contexts/FitnessGoalsContext";
import { useRecommendations } from "../../contexts/RecommendationsContext";
import { useFocusEffect } from "expo-router";
import * as SecureStore from "expo-secure-store";
import log from "../../utils/logger";
export default function GoalsScreen() {
const { user } = useUser();
const { colors, typography } = useTheme();
const {
statistics,
refetchStatistics,
@@ -45,10 +49,8 @@ export default function GoalsScreen() {
const [refreshing, setRefreshing] = useState(false);
const [isModalVisible, setIsModalVisible] = useState(false);
const [showAnalytics, setShowAnalytics] = useState(false);
const fabScale = useRef(new Animated.Value(1)).current;
const loadData = useCallback(async () => {
// Load goals and statistics (both cached)
await refetchGoals();
await refetchStatistics();
}, [refetchGoals, refetchStatistics]);
@@ -64,7 +66,6 @@ export default function GoalsScreen() {
style: "destructive",
onPress: async () => {
try {
// Clear all possible Clerk token keys
const keysToDelete = [
"__clerk_client_jwt",
"__clerk_db_jwt",
@@ -82,7 +83,6 @@ export default function GoalsScreen() {
}
}
// Clear all caches
clearStatsCache();
clearGoalsCache();
clearRecommendationsCache();
@@ -128,183 +128,237 @@ export default function GoalsScreen() {
const activeGoals = goals?.filter((g) => g.status === "active") || [];
const completedGoals = goals?.filter((g) => g.status === "completed") || [];
const avgProgress =
activeGoals.length > 0
? Math.round(
activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) /
activeGoals.length,
)
: 0;
return (
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: colors.background }]}>
<ScrollView
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={theme.colors.primary}
tintColor={colors.primary}
/>
}
>
<LinearGradient colors={theme.gradients.primary} style={styles.header}>
<View style={styles.headerContent}>
<View>
<Text style={styles.headerTitle}>Fitness Goals</Text>
<Text style={styles.headerSubtitle}>
Track your fitness journey progress
</Text>
</View>
<TouchableOpacity
onPress={clearClerkCache}
style={styles.debugButton}
{/* Header */}
<View style={styles.header}>
<View>
<Text style={[typography.h1, { color: colors.textPrimary }]}>
Fitness Goals
</Text>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 4 },
]}
>
<Ionicons name="refresh-circle-outline" size={24} color="#fff" />
</TouchableOpacity>
Track your fitness journey progress
</Text>
</View>
</LinearGradient>
<TouchableOpacity
onPress={clearClerkCache}
style={styles.debugButton}
>
<Ionicons
name="refresh-circle-outline"
size={24}
color={colors.textTertiary}
/>
</TouchableOpacity>
</View>
{/* Stats Summary */}
{goals && goals.length > 0 && (
<View style={styles.statsContainer}>
<View style={styles.statCard}>
<Text style={styles.statValue}>{activeGoals.length}</Text>
<Text style={styles.statLabel}>Active</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{completedGoals.length}</Text>
<Text style={styles.statLabel}>Completed</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>
{activeGoals.length > 0
? Math.round(
activeGoals.reduce(
(sum, g) => sum + (g.progress || 0),
0,
) / activeGoals.length,
)
: 0}
%
</Text>
<Text style={styles.statLabel}>Avg Progress</Text>
<View style={styles.section}>
<View style={styles.statsRow}>
<MinimalCard variant="elevated" style={styles.statCard}>
<Text style={[typography.stat, { color: colors.primary }]}>
{activeGoals.length}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 4 },
]}
>
Active
</Text>
</MinimalCard>
<MinimalCard variant="elevated" style={styles.statCard}>
<Text style={[typography.stat, { color: colors.success }]}>
{completedGoals.length}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 4 },
]}
>
Completed
</Text>
</MinimalCard>
<MinimalCard variant="elevated" style={styles.statCard}>
<Text style={[typography.stat, { color: colors.textPrimary }]}>
{avgProgress}%
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 4 },
]}
>
Avg Progress
</Text>
</MinimalCard>
</View>
</View>
)}
{/* Analytics Section */}
{statistics && (
<View style={styles.analyticsSection}>
<TouchableOpacity
style={styles.analyticsHeader}
onPress={() => setShowAnalytics(!showAnalytics)}
>
<View style={styles.analyticsHeaderLeft}>
<View style={styles.section}>
<MinimalCard variant="default">
<TouchableOpacity
style={styles.analyticsHeader}
onPress={() => setShowAnalytics(!showAnalytics)}
activeOpacity={0.7}
>
<View style={styles.analyticsHeaderLeft}>
<Ionicons
name="bar-chart-outline"
size={20}
color={colors.primary}
/>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginLeft: 8 },
]}
>
Progress Analytics
</Text>
</View>
<Ionicons
name="bar-chart-outline"
name={showAnalytics ? "chevron-up" : "chevron-down"}
size={20}
color={theme.colors.primary}
color={colors.textTertiary}
/>
<Text style={styles.analyticsTitle}>Progress Analytics</Text>
</View>
<Ionicons
name={showAnalytics ? "chevron-up" : "chevron-down"}
size={20}
color={theme.colors.gray400}
/>
</TouchableOpacity>
</TouchableOpacity>
{showAnalytics && (
<View style={styles.analyticsContent}>
{statistics.weeklyTrend.length > 0 && (
<WeeklyProgressChart
weeklyData={statistics.weeklyTrend}
title="8-Week Trend"
/>
)}
{statistics.goals.goalsByType.length > 0 && (
<GoalTypeBreakdownChart
data={statistics.goals.goalsByType}
title="Goals by Type"
/>
)}
</View>
)}
{showAnalytics && (
<View style={styles.analyticsContent}>
{statistics.weeklyTrend.length > 0 && (
<WeeklyProgressChart
weeklyData={statistics.weeklyTrend}
title="8-Week Trend"
/>
)}
{statistics.goals.goalsByType.length > 0 && (
<GoalTypeBreakdownChart
data={statistics.goals.goalsByType}
title="Goals by Type"
/>
)}
</View>
)}
</MinimalCard>
</View>
)}
{/* Active Goals */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Active Goals ({activeGoals.length})
</Text>
<SectionHeader
title={`Active Goals (${activeGoals.length})`}
actionLabel="Add New"
onActionPress={() => setIsModalVisible(true)}
/>
{activeGoals.length === 0 ? (
<View style={styles.emptyState}>
<Ionicons name="flag-outline" size={48} color="#d1d5db" />
<Text style={styles.emptyText}>No active goals yet</Text>
<Text style={styles.emptySubtext}>
Tap the + button to create your first goal
</Text>
</View>
<MinimalCard variant="default">
<View style={styles.emptyState}>
<Ionicons
name="flag-outline"
size={48}
color={colors.borderLight}
/>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textSecondary, marginTop: 12 },
]}
>
No active goals yet
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 4 },
]}
>
Tap "Add New" to create your first goal
</Text>
</View>
</MinimalCard>
) : (
activeGoals.map((goal) => (
<GoalProgressCard
key={goal.id}
goal={goal}
onComplete={() => handleCompleteGoal(goal)}
onDelete={() => handleDeleteGoal(goal.id)}
/>
))
<View style={styles.goalsList}>
{activeGoals.map((goal) => (
<GoalProgressCard
key={goal.id}
goal={goal}
onComplete={() => handleCompleteGoal(goal)}
onDelete={() => handleDeleteGoal(goal.id)}
/>
))}
</View>
)}
</View>
{/* Completed Goals */}
{completedGoals.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Completed Goals ({completedGoals.length})
</Text>
{completedGoals.map((goal) => (
<GoalProgressCard
key={goal.id}
goal={goal}
onDelete={() => handleDeleteGoal(goal.id)}
/>
))}
<SectionHeader
title={`Completed Goals (${completedGoals.length})`}
/>
<View style={styles.goalsList}>
{completedGoals.map((goal) => (
<GoalProgressCard
key={goal.id}
goal={goal}
onDelete={() => handleDeleteGoal(goal.id)}
/>
))}
</View>
</View>
)}
<View style={styles.footer} />
</ScrollView>
{/* Floating Action Button */}
<Animated.View
style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}
>
{/* Floating Action Button - Minimal Style */}
<View style={styles.fabContainer}>
<TouchableOpacity
onPress={() => setIsModalVisible(true)}
onPressIn={() => {
Animated.spring(fabScale, {
toValue: 0.9,
friction: 8,
tension: 100,
useNativeDriver: true,
}).start();
}}
onPressOut={() => {
Animated.spring(fabScale, {
toValue: 1,
friction: 8,
tension: 100,
useNativeDriver: true,
}).start();
}}
activeOpacity={0.9}
activeOpacity={0.8}
style={[
styles.fab,
{
backgroundColor: colors.primary,
shadowColor: colors.primary,
},
]}
>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.fab}
>
<Ionicons name="add" size={28} color="#fff" />
</LinearGradient>
<Ionicons name="add" size={28} color={colors.white} />
</TouchableOpacity>
</Animated.View>
</View>
{/* Create Goal Modal */}
<GoalCreationModal
@@ -319,129 +373,73 @@ export default function GoalsScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
scrollContent: {
paddingBottom: 20,
},
header: {
padding: 24,
paddingTop: 60,
paddingBottom: 24,
marginBottom: 10,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
headerContent: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
alignItems: "flex-start",
paddingHorizontal: 24,
paddingTop: 60,
paddingBottom: 24,
},
debugButton: {
padding: 8,
},
headerTitle: {
fontSize: theme.typography.fontSize["3xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
section: {
paddingHorizontal: 24,
marginBottom: 24,
},
headerSubtitle: {
fontSize: theme.typography.fontSize.base,
color: "rgba(255, 255, 255, 0.9)",
marginTop: 4,
},
statsContainer: {
statsRow: {
flexDirection: "row",
padding: 16,
gap: 12,
},
statCard: {
flex: 1,
backgroundColor: theme.colors.white,
padding: 16,
borderRadius: theme.borderRadius.xl,
alignItems: "center",
...theme.shadows.medium,
borderWidth: 1,
borderColor: "rgba(59, 130, 246, 0.1)",
},
statValue: {
fontSize: theme.typography.fontSize["2xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.primary,
marginBottom: 4,
},
statLabel: {
fontSize: 12,
color: "#6b7280",
fontWeight: "500",
},
analyticsSection: {
padding: 16,
paddingTop: 0,
paddingVertical: 20,
},
analyticsHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: theme.colors.white,
padding: 16,
borderRadius: theme.borderRadius.xl,
marginBottom: 12,
...theme.shadows.subtle,
},
analyticsHeaderLeft: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
analyticsTitle: {
fontSize: theme.typography.fontSize.base,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray700,
},
analyticsContent: {
paddingTop: 4,
paddingTop: 16,
marginTop: 16,
borderTopWidth: 1,
borderTopColor: "rgba(0, 0, 0, 0.05)",
},
section: {
padding: 20,
paddingTop: 10,
},
sectionTitle: {
fontSize: 18,
fontWeight: "600",
color: "#374151",
marginBottom: 12,
goalsList: {
gap: 12,
},
emptyState: {
alignItems: "center",
paddingVertical: 40,
},
emptyText: {
fontSize: 16,
fontWeight: "500",
color: "#6b7280",
marginTop: 12,
},
emptySubtext: {
fontSize: 14,
color: "#9ca3af",
marginTop: 4,
},
footer: {
height: 100,
},
fabContainer: {
position: "absolute",
right: 20,
bottom: 110, // Adjusted for tab bar height
right: 24,
bottom: 90,
},
fab: {
width: 64,
height: 64,
borderRadius: 32,
width: 56,
height: 56,
borderRadius: 28,
justifyContent: "center",
alignItems: "center",
...theme.shadows.glow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
elevation: 4,
},
});
+373 -159
View File
@@ -7,28 +7,28 @@ import {
Image,
} from "react-native";
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";
import { QuickActionGrid } from "../../components/QuickActionGrid";
import { Ionicons } from "@expo/vector-icons";
import { useTheme } from "../../contexts/ThemeContext";
import { useStatistics } from "../../contexts/StatisticsContext";
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 { 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 { colors, typography } = useTheme();
const { refetchStatistics, forceRefresh, statistics, loading } =
useStatistics();
const [refreshing, setRefreshing] = useState(false);
const [trackMealModalVisible, setTrackMealModalVisible] = useState(false);
const [addWaterModalVisible, setAddWaterModalVisible] = useState(false);
@@ -45,7 +45,6 @@ export default function HomeScreen() {
const onRefresh = useCallback(async () => {
setRefreshing(true);
// Force refetch statistics bypassing cache
await forceRefresh();
setRefreshing(false);
}, [forceRefresh]);
@@ -168,57 +167,358 @@ export default function HomeScreen() {
return () => clearTimeout(midnightTimer);
}, []);
const checkInsThisWeek = statistics?.attendance.checkInsThisWeek || 0;
const currentStreak = statistics?.attendance.currentStreak || 0;
return (
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: colors.background }]}>
<ScrollView
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={theme.colors.primary}
tintColor={colors.primary}
/>
}
>
{/* Header Section */}
<View style={styles.header}>
<View>
<Text style={styles.greeting}>{getGreeting()},</Text>
<Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
<Text style={[typography.body, { color: colors.textSecondary }]}>
{getGreeting()},
</Text>
<Text
style={[
typography.h1,
{ color: colors.textPrimary, marginTop: 4 },
]}
>
{user?.firstName || "Athlete"}
</Text>
</View>
<View style={styles.avatarContainer}>
<View>
{user?.imageUrl ? (
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
) : (
<View style={styles.placeholderAvatar}>
<Ionicons name="person" size={24} color="#fff" />
<View
style={[
styles.placeholderAvatar,
{ backgroundColor: colors.primary },
]}
>
<Ionicons name="person" size={24} color={colors.white} />
</View>
)}
</View>
</View>
{/* Activity Widget */}
<ActivityWidget calories={calories} />
{/* Daily Stats Card */}
<View style={styles.section}>
<MinimalCard variant="elevated">
<View style={styles.statsRow}>
<View style={styles.statItem}>
<IconContainer
variant="colored"
backgroundColor={`${colors.info}20`}
>
<Ionicons
name="checkmark-circle"
size={24}
color={colors.info}
/>
</IconContainer>
<Text
style={[
typography.stat,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
{checkInsThisWeek}
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
This Week
</Text>
</View>
{/* Quick Action Grid */}
<QuickActionGrid
onTrackMealPress={() => setTrackMealModalVisible(true)}
onAddWaterPress={() => setAddWaterModalVisible(true)}
onScanFoodPress={() => setScanFoodModalVisible(true)}
onLogWorkoutPress={() => {
// TODO: Implement workout logging
console.log("Log workout tapped");
}}
/>
<View
style={[styles.divider, { backgroundColor: colors.border }]}
/>
{/* Nutrition Widget */}
<NutritionWidget current={calories} goal={CALORIE_GOAL} />
<View style={styles.statItem}>
<IconContainer
variant="colored"
backgroundColor={`${colors.danger}20`}
>
<Ionicons name="flame" size={24} color={colors.danger} />
</IconContainer>
<Text
style={[
typography.stat,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
{calories}
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Kcal
</Text>
</View>
{/* Hydration Widget */}
<HydrationWidget current={waterIntake} goal={WATER_GOAL} />
<View
style={[styles.divider, { backgroundColor: colors.border }]}
/>
{/* Weekly Progress Widget */}
<WeeklyProgressWidget />
<View style={styles.statItem}>
<IconContainer
variant="colored"
backgroundColor={`${colors.success}20`}
>
<Ionicons name="trophy" size={24} color={colors.success} />
</IconContainer>
<Text
style={[
typography.stat,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
{currentStreak}
</Text>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Day Streak
</Text>
</View>
</View>
</MinimalCard>
</View>
{/* Quick Actions */}
<View style={styles.section}>
<SectionHeader title="Quick Actions" />
<View style={styles.actionGrid}>
<MinimalCard
variant="default"
onPress={() => {
console.log("Log workout tapped");
}}
style={styles.actionCard}
>
<IconContainer
variant="colored"
backgroundColor={`${colors.primary}20`}
>
<Ionicons name="barbell" size={24} color={colors.primary} />
</IconContainer>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
Log Workout
</Text>
</MinimalCard>
<MinimalCard
variant="default"
onPress={() => setTrackMealModalVisible(true)}
style={styles.actionCard}
>
<IconContainer
variant="colored"
backgroundColor={`${colors.success}20`}
>
<Ionicons name="restaurant" size={24} color={colors.success} />
</IconContainer>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
Track Meal
</Text>
</MinimalCard>
<MinimalCard
variant="default"
onPress={() => setAddWaterModalVisible(true)}
style={styles.actionCard}
>
<IconContainer
variant="colored"
backgroundColor={`${colors.info}20`}
>
<Ionicons name="water" size={24} color={colors.info} />
</IconContainer>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
Add Water
</Text>
</MinimalCard>
<MinimalCard
variant="default"
onPress={() => setScanFoodModalVisible(true)}
style={styles.actionCard}
>
<IconContainer
variant="colored"
backgroundColor={`${colors.accent}20`}
>
<Ionicons name="scan" size={24} color={colors.accent} />
</IconContainer>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginTop: 8 },
]}
>
Scan Food
</Text>
</MinimalCard>
</View>
</View>
{/* Nutrition Progress */}
<View style={styles.section}>
<SectionHeader title="Nutrition" />
<MinimalCard variant="default">
<View style={styles.progressHeader}>
<View style={styles.progressLabelRow}>
<Ionicons name="flame" size={20} color={colors.textSecondary} />
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginLeft: 8 },
]}
>
Calories
</Text>
</View>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
{calories} / {CALORIE_GOAL}
</Text>
</View>
<ProgressBar
progress={calories / CALORIE_GOAL}
color={colors.danger}
style={{ marginTop: 12 }}
/>
</MinimalCard>
</View>
{/* Hydration Progress */}
<View style={styles.section}>
<MinimalCard variant="default">
<View style={styles.progressHeader}>
<View style={styles.progressLabelRow}>
<Ionicons name="water" size={20} color={colors.textSecondary} />
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginLeft: 8 },
]}
>
Hydration
</Text>
</View>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
{waterIntake} / {WATER_GOAL} ml
</Text>
</View>
<ProgressBar
progress={waterIntake / WATER_GOAL}
color={colors.info}
style={{ marginTop: 12 }}
/>
</MinimalCard>
</View>
{/* Recent Activity */}
<View style={styles.section}>
<SectionHeader
title="Recent Activity"
actionLabel="See All"
onActionPress={() => console.log("See all tapped")}
/>
<View style={styles.activityList}>
<MinimalCard variant="default" style={styles.activityItem}>
<IconContainer
variant="colored"
backgroundColor={`${colors.primary}20`}
>
<Ionicons name="barbell" size={20} color={colors.primary} />
</IconContainer>
<View style={styles.activityInfo}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Upper Body Power
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
Today, 10:00 AM
</Text>
</View>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textSecondary },
]}
>
45m
</Text>
</MinimalCard>
<MinimalCard variant="default" style={styles.activityItem}>
<IconContainer
variant="colored"
backgroundColor={`${colors.success}20`}
>
<Ionicons name="bicycle" size={20} color={colors.success} />
</IconContainer>
<View style={styles.activityInfo}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Morning Cardio
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
Yesterday, 7:30 AM
</Text>
</View>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textSecondary },
]}
>
30m
</Text>
</MinimalCard>
</View>
</View>
<TrackMealModal
visible={trackMealModalVisible}
@@ -240,54 +540,6 @@ export default function HomeScreen() {
onAddFood={handleAddScannedFood}
/>
{/* Recent Activity Section */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Recent Activity</Text>
<Text style={styles.seeAll}>See All</Text>
</View>
<View style={styles.activityCard}>
<LinearGradient
colors={["rgba(255, 255, 255, 0.8)", "rgba(255, 255, 255, 0.5)"]}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.recentIcon}
>
<Ionicons name="barbell" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Upper Body Power</Text>
<Text style={styles.recentSubtitle}>Today, 10:00 AM</Text>
</View>
<Text style={styles.recentValue}>45m</Text>
</LinearGradient>
<LinearGradient
colors={["rgba(255, 255, 255, 0.8)", "rgba(255, 255, 255, 0.5)"]}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.success}
style={styles.recentIcon}
>
<Ionicons name="bicycle" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Morning Cardio</Text>
<Text style={styles.recentSubtitle}>Yesterday, 7:30 AM</Text>
</View>
<Text style={styles.recentValue}>30m</Text>
</LinearGradient>
</View>
</View>
{/* Bottom Spacer for Tab Bar */}
<View style={{ height: 100 }} />
</ScrollView>
@@ -298,7 +550,6 @@ export default function HomeScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
scrollContent: {
paddingTop: 60,
@@ -308,102 +559,65 @@ const styles = StyleSheet.create({
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 24,
marginBottom: 32,
},
greeting: {
fontSize: 16,
color: theme.colors.gray600,
fontWeight: "500",
marginBottom: 4,
},
name: {
fontSize: 32,
fontWeight: "800",
color: theme.colors.gray900,
letterSpacing: -0.5,
},
avatarContainer: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
marginBottom: 24,
},
avatar: {
width: 56,
height: 56,
borderRadius: 20,
borderWidth: 2,
borderColor: "#fff",
borderRadius: 28,
},
placeholderAvatar: {
width: 56,
height: 56,
borderRadius: 20,
backgroundColor: theme.colors.primary,
borderRadius: 28,
justifyContent: "center",
alignItems: "center",
borderWidth: 2,
borderColor: "#fff",
},
section: {
paddingHorizontal: 20,
paddingHorizontal: 24,
marginBottom: 24,
},
sectionHeader: {
statsRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: "700",
color: theme.colors.gray900,
},
seeAll: {
fontSize: 14,
color: theme.colors.primary,
fontWeight: "600",
},
activityCard: {
gap: 12,
},
recentItem: {
flexDirection: "row",
statItem: {
alignItems: "center",
padding: 16,
borderRadius: 20,
backgroundColor: "#fff",
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.6)",
},
recentIconContainer: {
marginRight: 16,
},
recentIcon: {
width: 48,
height: 48,
borderRadius: 16,
justifyContent: "center",
alignItems: "center",
},
recentInfo: {
flex: 1,
},
recentTitle: {
fontSize: 16,
fontWeight: "600",
color: theme.colors.gray900,
marginBottom: 4,
divider: {
width: 1,
height: 60,
},
recentSubtitle: {
fontSize: 12,
color: theme.colors.gray500,
actionGrid: {
flexDirection: "row",
flexWrap: "wrap",
gap: 12,
},
recentValue: {
fontSize: 14,
fontWeight: "600",
color: theme.colors.gray900,
actionCard: {
width: "48%",
alignItems: "center",
paddingVertical: 20,
},
progressHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
progressLabelRow: {
flexDirection: "row",
alignItems: "center",
},
activityList: {
gap: 12,
},
activityItem: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
activityInfo: {
flex: 1,
},
});
+371 -289
View File
@@ -11,11 +11,13 @@ import {
import { useUser, useClerk, useAuth } from "@clerk/clerk-expo";
import { useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { theme } from "../../styles/theme";
import { AnimatedButton } from "../../components/AnimatedButton";
import { GradientBackground } from "../../components/GradientBackground";
import { useState, useEffect } from "react";
import { useTheme } from "../../contexts/ThemeContext";
import { MinimalCard } from "../../components/MinimalCard";
import { ListItem } from "../../components/ListItem";
import { MinimalButton } from "../../components/MinimalButton";
import { Badge } from "../../components/Badge";
import { IconContainer } from "../../components/IconContainer";
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
import log from "../../utils/logger";
@@ -23,24 +25,9 @@ export default function ProfileScreen() {
const { user } = useUser();
const { signOut } = useClerk();
const router = useRouter();
const handleSignOut = async () => {
try {
await signOut();
} catch (err) {
log.error("Failed to sign out", err);
}
};
const confirmSignOut = () => {
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
{ text: "Cancel", style: "cancel" },
{ text: "Sign Out", style: "destructive", onPress: handleSignOut },
]);
};
// Gym selection state and handlers
const { colors, typography, theme: activeTheme, setTheme } = useTheme();
const { getToken } = useAuth();
const [gyms, setGyms] = useState<
Array<{ id: string; name: string; location?: string }>
>([]);
@@ -60,7 +47,6 @@ export default function ProfileScreen() {
}
}, [user?.publicMetadata, gyms]);
// Auto-load gyms on mount
useEffect(() => {
loadGyms();
}, []);
@@ -80,9 +66,7 @@ export default function ProfileScreen() {
log.error(
"Failed to fetch gyms - non-OK response",
new Error(text.slice(0, 200)),
{
status: res.status,
},
{ status: res.status },
);
setGyms([]);
return;
@@ -92,9 +76,7 @@ export default function ProfileScreen() {
log.error(
"Failed to fetch gyms - expected JSON",
new Error(text.slice(0, 200)),
{
contentType,
},
{ contentType },
);
setGyms([]);
return;
@@ -149,9 +131,7 @@ export default function ProfileScreen() {
log.error(
"Failed to update gym selection - non-OK response",
new Error(text.slice(0, 200)),
{
status: res.status,
},
{ status: res.status },
);
Alert.alert("Error", "Failed to update gym selection");
return;
@@ -167,14 +147,12 @@ export default function ProfileScreen() {
});
}
}
// Update current gym state for immediate UI reflection
setCurrentGymId(selectedGymId);
setCurrentGymName(
selectedGymId
? (gyms.find((g) => g.id === selectedGymId)?.name ?? null)
: null,
);
// Attempt to reload Clerk user metadata so current gym reflects server state
try {
await (user as any)?.reload?.();
} catch (e) {
@@ -190,372 +168,476 @@ export default function ProfileScreen() {
}
};
return (
<View style={styles.container}>
<GradientBackground variant="primary" style={styles.header}>
<View style={styles.profileCard}>
<View style={styles.avatarContainer}>
{user?.imageUrl ? (
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
) : (
<View style={styles.placeholderAvatar}>
<Ionicons name="person" size={40} color="#fff" />
</View>
)}
<View style={styles.editBadge}>
<Ionicons name="pencil" size={12} color={theme.colors.primary} />
</View>
</View>
<Text style={styles.name}>{user?.fullName || "User"}</Text>
<Text style={styles.email}>
{user?.primaryEmailAddress?.emailAddress}
</Text>
<View style={styles.memberBadge}>
<Text style={styles.memberText}>Premium Member</Text>
</View>
</View>
</GradientBackground>
const handleSignOut = async () => {
try {
await signOut();
} catch (err) {
log.error("Failed to sign out", err);
}
};
<View style={styles.content}>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Account</Text>
<View style={[styles.infoCard, theme.shadows.subtle]}>
<TouchableOpacity
style={styles.infoRow}
onPress={() => router.push("/personal-details")}
const confirmSignOut = () => {
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
{ text: "Cancel", style: "cancel" },
{ text: "Sign Out", style: "destructive", onPress: handleSignOut },
]);
};
const handleThemeChange = () => {
Alert.alert("Choose Theme", "Select your preferred theme", [
{
text: "Light",
onPress: () => setTheme("light"),
},
{
text: "Dark",
onPress: () => setTheme("dark"),
},
{
text: "System",
onPress: () => setTheme("system"),
},
{ text: "Cancel", style: "cancel" },
]);
};
const getThemeLabel = () => {
if (activeTheme === "light") return "Light";
if (activeTheme === "dark") return "Dark";
return "System";
};
return (
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
contentContainerStyle={styles.content}
>
{/* Header Card */}
<MinimalCard variant="elevated" style={styles.profileCard}>
<View style={styles.avatarContainer}>
{user?.imageUrl ? (
<Image source={{ uri: user.imageUrl }} style={styles.avatar} />
) : (
<View
style={[
styles.placeholderAvatar,
{ backgroundColor: colors.primary },
]}
>
<LinearGradient
colors={["rgba(59, 130, 246, 0.1)", "rgba(59, 130, 246, 0.05)"]}
style={styles.iconContainer}
<Ionicons name="person" size={40} color={colors.white} />
</View>
)}
</View>
<Text
style={[typography.h2, { color: colors.textPrimary, marginTop: 16 }]}
>
{user?.fullName || "User"}
</Text>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 4 },
]}
>
{user?.primaryEmailAddress?.emailAddress}
</Text>
<Badge
label="Premium Member"
variant="primary"
style={{ marginTop: 12 }}
/>
</MinimalCard>
{/* Theme Settings */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 12 },
]}
>
Appearance
</Text>
<MinimalCard variant="default">
<ListItem
title="Theme"
subtitle={`Current: ${getThemeLabel()}`}
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.accent}20`}
>
<Ionicons
name="color-palette"
size={20}
color={colors.accent}
/>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={colors.textTertiary}
/>
}
onPress={handleThemeChange}
/>
</MinimalCard>
</View>
{/* Account Settings */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 12 },
]}
>
Account
</Text>
<MinimalCard variant="default">
<ListItem
title="Personal Details"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.primary}20`}
>
<Ionicons
name="person-outline"
size={20}
color={theme.colors.primary}
color={colors.primary}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Personal Details</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity
style={styles.infoRow}
onPress={() => router.push("/fitness-profile")}
>
<LinearGradient
colors={["rgba(16, 185, 129, 0.1)", "rgba(16, 185, 129, 0.05)"]}
style={styles.iconContainer}
}
onPress={() => router.push("/personal-details")}
/>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem
title="Fitness Profile"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.success}20`}
>
<Ionicons
name="fitness-outline"
size={20}
color={theme.colors.success}
color={colors.success}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Fitness Profile</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.infoRow}>
<LinearGradient
colors={["rgba(245, 158, 11, 0.1)", "rgba(245, 158, 11, 0.05)"]}
style={styles.iconContainer}
}
onPress={() => router.push("/fitness-profile")}
/>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem
title="Notifications"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.warning}20`}
>
<Ionicons
name="notifications-outline"
size={20}
color={theme.colors.warning}
color={colors.warning}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Notifications</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
</View>
}
/>
</MinimalCard>
</View>
{/* Gym Selection */}
<View
style={[styles.infoCard, theme.shadows.subtle, { marginTop: 12 }]}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: 12,
}}
{/* Gym Selection */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
Gym Selection
</Text>
<TouchableOpacity onPress={loadGyms}>
<Text
style={[
typography.body,
{ color: colors.primary, fontWeight: "600" },
]}
>
<Text style={styles.infoLabel}>
Gym{" "}
{currentGymName
? `(Current: ${currentGymName})`
: currentGymId
? `(Current: ${currentGymId})`
: "(Current: None)"}
Refresh
</Text>
</TouchableOpacity>
</View>
<MinimalCard variant="default">
{currentGymName && (
<View style={styles.currentGym}>
<Text
style={[typography.caption, { color: colors.textTertiary }]}
>
Current Gym
</Text>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginTop: 4 },
]}
>
{currentGymName}
</Text>
<TouchableOpacity onPress={loadGyms}>
<Text style={{ color: theme.colors.primary }}>
Refresh Gyms
</Text>
</TouchableOpacity>
</View>
)}
{gymsLoading ? (
<ActivityIndicator style={{ padding: 12 }} />
) : (
{gymsLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator color={colors.primary} />
</View>
) : (
<>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ paddingHorizontal: 12, paddingBottom: 12 }}
style={styles.gymScroll}
contentContainerStyle={styles.gymScrollContent}
>
<View style={{ flexDirection: "row" }}>
<TouchableOpacity
<TouchableOpacity
style={[
styles.gymChip,
{
backgroundColor:
selectedGymId === null
? `${colors.primary}20`
: colors.surfaceElevated,
borderColor:
selectedGymId === null ? colors.primary : colors.border,
},
]}
onPress={() => setSelectedGymId(null)}
>
<Text
style={[
styles.infoRow,
selectedGymId === null && {
backgroundColor: "rgba(59, 130, 246, 0.1)",
typography.body,
{
color:
selectedGymId === null
? colors.primary
: colors.textSecondary,
fontWeight: selectedGymId === null ? "600" : "400",
},
]}
onPress={() => setSelectedGymId(null)}
>
<Text style={styles.infoLabel}>Proceed without gym</Text>
</TouchableOpacity>
{gyms.map((gym) => (
<TouchableOpacity
key={gym.id}
No Gym
</Text>
</TouchableOpacity>
{gyms.map((gym) => (
<TouchableOpacity
key={gym.id}
style={[
styles.gymChip,
{
backgroundColor:
selectedGymId === gym.id
? `${colors.primary}20`
: colors.surfaceElevated,
borderColor:
selectedGymId === gym.id
? colors.primary
: colors.border,
},
]}
onPress={() => setSelectedGymId(gym.id)}
>
<Text
style={[
styles.infoRow,
selectedGymId === gym.id && {
backgroundColor: "rgba(59, 130, 246, 0.1)",
typography.body,
{
color:
selectedGymId === gym.id
? colors.primary
: colors.textSecondary,
fontWeight: selectedGymId === gym.id ? "600" : "400",
},
]}
onPress={() => setSelectedGymId(gym.id)}
>
<Text style={styles.infoLabel}>{gym.name}</Text>
</TouchableOpacity>
))}
</View>
{gym.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
)}
<View style={{ padding: 12 }}>
<AnimatedButton
title="Apply Gym Selection"
<MinimalButton
title="Apply Selection"
onPress={handleApplyGym}
variant="primary"
style={{ marginTop: 4 }}
icon={
<Ionicons
name="checkmark-outline"
size={20}
color={theme.colors.white}
/>
}
style={{ marginTop: 16 }}
/>
</View>
</View>
</View>
</>
)}
</MinimalCard>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Support</Text>
<View style={[styles.infoCard, theme.shadows.subtle]}>
<TouchableOpacity style={styles.infoRow}>
<LinearGradient
colors={["rgba(139, 92, 246, 0.1)", "rgba(139, 92, 246, 0.05)"]}
style={styles.iconContainer}
{/* Support */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 12 },
]}
>
Support
</Text>
<MinimalCard variant="default">
<ListItem
title="Help Center"
leftIcon={
<IconContainer
variant="colored"
backgroundColor={`${colors.info}20`}
>
<Ionicons
name="help-circle-outline"
size={20}
color={theme.colors.secondary}
color={colors.info}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Help Center</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.infoRow}>
<LinearGradient
colors={[
"rgba(107, 114, 128, 0.1)",
"rgba(107, 114, 128, 0.05)",
]}
style={styles.iconContainer}
>
}
/>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<ListItem
title="Privacy & Security"
leftIcon={
<IconContainer variant="subtle">
<Ionicons
name="shield-checkmark-outline"
size={20}
color={theme.colors.gray600}
color={colors.textSecondary}
/>
</LinearGradient>
<Text style={styles.infoLabel}>Privacy & Security</Text>
</IconContainer>
}
rightElement={
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
</TouchableOpacity>
</View>
</View>
}
/>
</MinimalCard>
</View>
<AnimatedButton
{/* Sign Out */}
<View style={styles.section}>
<MinimalButton
title="Sign Out"
onPress={confirmSignOut}
variant="danger"
style={styles.signOutButton}
icon={<Ionicons name="log-out-outline" size={20} color="#fff" />}
size="lg"
/>
<Text style={styles.version}>Version 1.0.0</Text>
</View>
</View>
<Text
style={[
typography.caption,
{
color: colors.textTertiary,
textAlign: "center",
marginBottom: 100,
},
]}
>
Version 1.0.0
</Text>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
header: {
content: {
padding: 24,
paddingTop: 60,
paddingBottom: 30,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
alignItems: "center",
},
profileCard: {
alignItems: "center",
paddingVertical: 32,
marginBottom: 24,
},
avatarContainer: {
position: "relative",
marginBottom: 16,
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
borderWidth: 4,
borderColor: "rgba(255, 255, 255, 0.3)",
},
placeholderAvatar: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: "rgba(255, 255, 255, 0.2)",
justifyContent: "center",
alignItems: "center",
borderWidth: 4,
borderColor: "rgba(255, 255, 255, 0.3)",
},
editBadge: {
position: "absolute",
bottom: 0,
right: 0,
backgroundColor: theme.colors.white,
width: 32,
height: 32,
borderRadius: 16,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
name: {
fontSize: theme.typography.fontSize["2xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
marginBottom: 4,
},
email: {
fontSize: theme.typography.fontSize.sm,
color: "rgba(255, 255, 255, 0.8)",
marginBottom: 12,
},
memberBadge: {
backgroundColor: "rgba(255, 255, 255, 0.2)",
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: theme.borderRadius.full,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.3)",
},
memberText: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.xs,
fontWeight: theme.typography.fontWeight.bold,
letterSpacing: 0.5,
},
content: {
padding: 20,
marginTop: -20,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray900,
marginBottom: 12,
marginLeft: 4,
},
infoCard: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
padding: 8,
borderWidth: 1,
borderColor: theme.colors.gray100,
},
infoRow: {
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 12,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 10,
justifyContent: "center",
alignItems: "center",
marginRight: 12,
},
infoLabel: {
flex: 1,
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray900,
fontWeight: theme.typography.fontWeight.medium,
marginBottom: 12,
},
divider: {
height: 1,
backgroundColor: theme.colors.gray100,
marginLeft: 60,
marginLeft: 52,
},
signOutButton: {
marginTop: 8,
currentGym: {
marginBottom: 16,
paddingBottom: 16,
borderBottomWidth: 1,
borderBottomColor: "rgba(0, 0, 0, 0.05)",
},
version: {
textAlign: "center",
marginTop: 24,
color: theme.colors.gray400,
fontSize: theme.typography.fontSize.xs,
loadingContainer: {
paddingVertical: 20,
alignItems: "center",
},
gymScroll: {
marginHorizontal: -16,
},
gymScrollContent: {
paddingHorizontal: 16,
gap: 8,
},
gymChip: {
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
borderWidth: 1.5,
marginRight: 8,
},
});
+230 -252
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef } from "react";
import React, { useState, useCallback } from "react";
import {
View,
Text,
@@ -9,11 +9,15 @@ import {
ActivityIndicator,
Alert,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { useUser } from "@clerk/clerk-expo";
import { useFocusEffect } from "expo-router";
import { theme } from "../../styles/theme";
import { useTheme } from "../../contexts/ThemeContext";
import { MinimalCard } from "../../components/MinimalCard";
import { SectionHeader } from "../../components/SectionHeader";
import { MinimalButton } from "../../components/MinimalButton";
import { Badge } from "../../components/Badge";
import { IconContainer } from "../../components/IconContainer";
import { useRecommendations } from "../../contexts/RecommendationsContext";
import { useNotifications } from "../../contexts/NotificationsContext";
import { NotificationsModal } from "../../components/NotificationsModal";
@@ -22,6 +26,7 @@ import log from "../../utils/logger";
export default function RecommendationsScreen() {
const { user } = useUser();
const { colors, typography } = useTheme();
const {
recommendations: allRecommendations,
loading,
@@ -43,7 +48,7 @@ export default function RecommendationsScreen() {
setNotificationsVisible(false);
};
// Filter to show only approved recommendations for regular users
// Filter to show only approved recommendations
const recommendations = allRecommendations.filter(
(rec) => rec.status === "approved",
);
@@ -101,14 +106,14 @@ export default function RecommendationsScreen() {
if (loading && recommendations.length === 0) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<View style={[styles.centered, { backgroundColor: colors.background }]}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
return (
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: colors.background }]}>
<NotificationsModal
visible={notificationsVisible}
onClose={handleCloseNotifications}
@@ -119,98 +124,126 @@ export default function RecommendationsScreen() {
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={theme.colors.primary}
tintColor={colors.primary}
/>
}
>
{/* Header */}
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<View style={styles.header}>
<View>
<Text style={styles.headerTitle}>AI Recommendations</Text>
<Text style={styles.headerSubtitle}>
<Text style={[typography.h1, { color: colors.textPrimary }]}>
AI Recommendations
</Text>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 4 },
]}
>
Personalized fitness & nutrition plans
</Text>
</View>
<TouchableOpacity
style={styles.iconContainer}
style={styles.notificationButton}
onPress={handleOpenNotifications}
activeOpacity={0.7}
>
<Ionicons name="sparkles" size={32} color="#fff" />
{unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{unreadCount}</Text>
</View>
)}
<IconContainer
variant="colored"
backgroundColor={`${colors.accent}20`}
>
<Ionicons name="sparkles" size={24} color={colors.accent} />
{unreadCount > 0 && (
<View
style={[
styles.notificationBadge,
{ backgroundColor: colors.danger },
]}
>
<Text style={styles.notificationBadgeText}>
{unreadCount}
</Text>
</View>
)}
</IconContainer>
</TouchableOpacity>
</LinearGradient>
</View>
{/* Generate Button */}
<View style={styles.actionContainer}>
<TouchableOpacity
<View style={styles.section}>
<MinimalButton
title="Generate New Plan"
onPress={handleGenerateRecommendation}
variant="primary"
size="lg"
loading={generating}
disabled={generating}
activeOpacity={0.8}
>
<LinearGradient
colors={theme.gradients.purple}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.generateButton, theme.shadows.medium]}
>
{generating ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons
name="bulb"
size={24}
color="#fff"
style={{ marginRight: 12 }}
/>
<Text style={styles.generateButtonText}>
Generate New Plan
</Text>
</>
)}
</LinearGradient>
</TouchableOpacity>
textStyle={{ fontSize: 16 }}
/>
</View>
{/* Recommendations List */}
<View style={styles.section}>
<SectionHeader
title="Your Plans"
subtitle={
recommendations.length > 0
? `${recommendations.length} active plan${recommendations.length !== 1 ? "s" : ""}`
: undefined
}
/>
{recommendations.length === 0 ? (
<View style={styles.emptyState}>
<LinearGradient
colors={["rgba(139, 92, 246, 0.1)", "rgba(139, 92, 246, 0.05)"]}
style={styles.emptyCard}
>
<Ionicons
name="sparkles-outline"
size={64}
color={theme.colors.purple}
/>
<Text style={styles.emptyTitle}>No Recommendations Yet</Text>
<Text style={styles.emptyText}>
<MinimalCard variant="default">
<View style={styles.emptyState}>
<IconContainer
variant="colored"
backgroundColor={`${colors.accent}15`}
size="lg"
>
<Ionicons
name="sparkles-outline"
size={32}
color={colors.accent}
/>
</IconContainer>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary, marginTop: 16 },
]}
>
No Recommendations Yet
</Text>
<Text
style={[
typography.body,
{
color: colors.textSecondary,
marginTop: 8,
textAlign: "center",
},
]}
>
Tap "Generate New Plan" to get personalized AI-powered fitness
and nutrition recommendations based on your profile and goals.
</Text>
</LinearGradient>
</View>
</View>
</MinimalCard>
) : (
recommendations.map((recommendation) => (
<RecommendationCard
key={recommendation.id}
recommendation={recommendation}
/>
))
<View style={styles.recommendationsList}>
{recommendations.map((recommendation) => (
<RecommendationCard
key={recommendation.id}
recommendation={recommendation}
/>
))}
</View>
)}
</View>
{/* Bottom Spacer */}
<View style={{ height: 100 }} />
</ScrollView>
</View>
);
@@ -221,250 +254,195 @@ interface RecommendationCardProps {
}
function RecommendationCard({ recommendation }: RecommendationCardProps) {
const { colors, typography } = useTheme();
const [expanded, setExpanded] = useState(false);
return (
<View style={styles.card}>
<LinearGradient
colors={["rgba(255, 255, 255, 1)", "rgba(249, 250, 251, 1)"]}
style={[styles.cardContent, theme.shadows.medium]}
<MinimalCard variant="elevated">
{/* Header */}
<TouchableOpacity
onPress={() => setExpanded(!expanded)}
activeOpacity={0.7}
style={styles.cardHeader}
>
{/* Header */}
<View style={styles.cardHeader}>
<View style={styles.cardHeaderLeft}>
<LinearGradient
colors={theme.gradients.success}
style={styles.cardIcon}
>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
</LinearGradient>
<View>
<Text style={styles.cardTitle}>AI Fitness Plan</Text>
<Text style={styles.cardDate}>
{new Date(recommendation.generatedAt).toLocaleDateString()}
<View style={styles.cardHeaderLeft}>
<IconContainer
variant="colored"
backgroundColor={`${colors.success}20`}
>
<Ionicons
name="checkmark-circle"
size={20}
color={colors.success}
/>
</IconContainer>
<View style={{ marginLeft: 12 }}>
<Text style={[typography.h3, { color: colors.textPrimary }]}>
AI Fitness Plan
</Text>
<Text style={[typography.caption, { color: colors.textTertiary }]}>
{new Date(recommendation.generatedAt).toLocaleDateString()}
</Text>
</View>
</View>
<Ionicons
name={expanded ? "chevron-up" : "chevron-down"}
size={24}
color={colors.textTertiary}
/>
</TouchableOpacity>
{/* Summary */}
<View style={styles.cardSummary}>
<Text
style={[typography.body, { color: colors.textSecondary }]}
numberOfLines={expanded ? undefined : 3}
>
{recommendation.recommendationText}
</Text>
</View>
{/* Expanded Content */}
{expanded && (
<View style={styles.expandedContent}>
{/* Activity Plan */}
<View
style={[
styles.planSection,
{ backgroundColor: colors.surfaceElevated },
]}
>
<View style={styles.planHeader}>
<Ionicons name="barbell" size={20} color={colors.primary} />
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginLeft: 8 },
]}
>
Activity Plan
</Text>
</View>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 8 },
]}
>
{recommendation.activityPlan}
</Text>
</View>
<TouchableOpacity onPress={() => setExpanded(!expanded)}>
<Ionicons
name={expanded ? "chevron-up" : "chevron-down"}
size={24}
color={theme.colors.gray400}
/>
</TouchableOpacity>
</View>
{/* Summary */}
<View style={styles.cardSummary}>
<Text
style={styles.summaryText}
numberOfLines={expanded ? undefined : 3}
{/* Diet Plan */}
<View
style={[
styles.planSection,
{ backgroundColor: colors.surfaceElevated },
]}
>
{recommendation.recommendationText}
</Text>
</View>
{/* Expanded Content */}
{expanded && (
<View style={styles.expandedContent}>
{/* Activity Plan */}
<View style={styles.planSection}>
<View style={styles.planHeader}>
<Ionicons
name="barbell"
size={20}
color={theme.colors.primary}
/>
<Text style={styles.planTitle}>Activity Plan</Text>
</View>
<Text style={styles.planText}>{recommendation.activityPlan}</Text>
</View>
{/* Diet Plan */}
<View style={styles.planSection}>
<View style={styles.planHeader}>
<Ionicons
name="restaurant"
size={20}
color={theme.colors.success}
/>
<Text style={styles.planTitle}>Diet Plan</Text>
</View>
<Text style={styles.planText}>{recommendation.dietPlan}</Text>
<View style={styles.planHeader}>
<Ionicons name="restaurant" size={20} color={colors.success} />
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginLeft: 8 },
]}
>
Diet Plan
</Text>
</View>
<Text
style={[
typography.body,
{ color: colors.textSecondary, marginTop: 8 },
]}
>
{recommendation.dietPlan}
</Text>
</View>
)}
</LinearGradient>
</View>
</View>
)}
</MinimalCard>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: theme.colors.background,
},
scrollContent: {
paddingBottom: 100,
paddingBottom: 20,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 24,
alignItems: "flex-start",
paddingHorizontal: 24,
paddingTop: 60,
paddingBottom: 24,
marginBottom: 20,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
headerTitle: {
fontSize: theme.typography.fontSize["3xl"],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
notificationButton: {
position: "relative",
},
headerSubtitle: {
fontSize: theme.typography.fontSize.base,
color: "rgba(255, 255, 255, 0.9)",
marginTop: 4,
},
iconContainer: {
backgroundColor: "rgba(255, 255, 255, 0.2)",
width: 64,
height: 64,
borderRadius: 32,
justifyContent: "center",
alignItems: "center",
},
badge: {
notificationBadge: {
position: "absolute",
top: -4,
right: -4,
backgroundColor: theme.colors.danger,
borderRadius: 10,
width: 20,
height: 20,
minWidth: 18,
height: 18,
borderRadius: 9,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 4,
},
badgeText: {
notificationBadgeText: {
color: "#fff",
fontSize: 12,
fontWeight: "bold",
},
actionContainer: {
paddingHorizontal: 20,
marginBottom: 20,
},
generateButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 16,
paddingHorizontal: 24,
borderRadius: theme.borderRadius.xl,
},
generateButtonText: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
fontSize: 11,
fontWeight: "700",
},
section: {
paddingHorizontal: 20,
paddingHorizontal: 24,
marginBottom: 24,
},
emptyState: {
paddingVertical: 40,
},
emptyCard: {
borderRadius: theme.borderRadius["2xl"],
padding: 32,
alignItems: "center",
paddingVertical: 40,
paddingHorizontal: 20,
},
emptyTitle: {
fontSize: theme.typography.fontSize.xl,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray700,
marginTop: 16,
marginBottom: 8,
},
emptyText: {
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray500,
textAlign: "center",
lineHeight: 24,
},
card: {
marginBottom: 16,
},
cardContent: {
borderRadius: theme.borderRadius["2xl"],
padding: 20,
recommendationsList: {
gap: 12,
},
cardHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
},
cardHeaderLeft: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
cardIcon: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: "center",
alignItems: "center",
},
cardTitle: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray800,
},
cardDate: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray500,
marginTop: 2,
flex: 1,
},
cardSummary: {
marginBottom: 12,
},
summaryText: {
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray700,
lineHeight: 24,
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: "rgba(0, 0, 0, 0.05)",
},
expandedContent: {
marginTop: 12,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: theme.colors.gray200,
marginTop: 16,
gap: 12,
},
planSection: {
marginBottom: 16,
padding: 16,
borderRadius: 12,
},
planHeader: {
flexDirection: "row",
alignItems: "center",
gap: 8,
marginBottom: 8,
},
planTitle: {
fontSize: theme.typography.fontSize.base,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray800,
},
planText: {
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray600,
lineHeight: 22,
},
});
+19 -13
View File
@@ -3,7 +3,9 @@ import { Stack } from "expo-router";
import * as SecureStore from "expo-secure-store";
import { View, Text } from "react-native";
import { useEffect, useState } from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { validateEnv } from "../utils/env";
import { ThemeProvider } from "../contexts/ThemeContext";
import { StatisticsProvider } from "../contexts/StatisticsContext";
import { FitnessGoalsProvider } from "../contexts/FitnessGoalsContext";
import { RecommendationsProvider } from "../contexts/RecommendationsContext";
@@ -169,18 +171,22 @@ export default function RootLayout() {
});
return (
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<NotificationsProvider>
<StatisticsProvider>
<FitnessGoalsProvider>
<RecommendationsProvider>
<AppContent />
</RecommendationsProvider>
</FitnessGoalsProvider>
</StatisticsProvider>
</NotificationsProvider>
</ClerkLoaded>
</ClerkProvider>
<SafeAreaProvider>
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<ThemeProvider>
<NotificationsProvider>
<StatisticsProvider>
<FitnessGoalsProvider>
<RecommendationsProvider>
<AppContent />
</RecommendationsProvider>
</FitnessGoalsProvider>
</StatisticsProvider>
</NotificationsProvider>
</ThemeProvider>
</ClerkLoaded>
</ClerkProvider>
</SafeAreaProvider>
);
}