diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 0fc1c97..823a975 100644 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/mobile/src/app/(tabs)/attendance.tsx b/apps/mobile/src/app/(tabs)/attendance.tsx index a8c9d64..d09fa5b 100644 --- a/apps/mobile/src/app/(tabs)/attendance.tsx +++ b/apps/mobile/src/app/(tabs)/attendance.tsx @@ -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(null); const [history, setHistory] = useState([]); - 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 ( - - + + ); } return ( - - - Attendance - Track your gym visits - + + {/* Header */} + + + Attendance + + + Track your gym visits + + - + {/* Check In/Out Section */} + {activeCheckIn ? ( - - - - + + + - - - - - Currently Checked In - - Since{" "} - {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} - + + + + + Currently Checked In + + + Since{" "} + {new Date(activeCheckIn.checkInTime).toLocaleTimeString( + [], + { + hour: "2-digit", + minute: "2-digit", + }, + )} + + - - - - Check Out - - - + + ) : ( - - - - - Check In - - - + )} {/* Attendance Calendar */} - {history.length > 0 && } + + + + + + - Recent History - {history.map((item) => ( - - - - + + {history.length === 0 ? ( + + + - - - - - {new Date(item.checkInTime).toLocaleDateString()} + + + No attendance history yet - {item.type.toUpperCase()} - - - - - In:{" "} - {new Date(item.checkInTime).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} - - {item.checkOutTime && ( - - Out:{" "} - {new Date(item.checkOutTime).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} + + Your check-in history will appear here - )} + + + ) : ( + + {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 ( + + + + + + + + + {checkIn.toLocaleDateString()} + + + {checkIn.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + {checkOut && + ` - ${checkOut.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}`} + + + + {duration && ( + + )} + + + ); + })} - - ))} + )} + + + {/* Bottom Spacer */} + ); } @@ -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: { - flexDirection: "row", - alignItems: "center", - marginBottom: 16, - }, - activeIconContainer: { - marginRight: 16, - }, - activeIcon: { - width: 56, - height: 56, - borderRadius: 28, - justifyContent: "center", - 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, - }, - sectionTitle: { - fontSize: theme.typography.fontSize.xl, - fontWeight: theme.typography.fontWeight.semibold, - marginBottom: 16, - paddingHorizontal: 20, - color: theme.colors.gray900, - }, - historyItem: { - padding: 16, - borderRadius: theme.borderRadius.xl, - marginBottom: 12, - marginHorizontal: 20, + activeHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + }, + activeHeaderLeft: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + emptyState: { + alignItems: "center", + paddingVertical: 40, + paddingHorizontal: 20, + }, + historyList: { + gap: 12, + }, + historyItem: { 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, }, }); diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx index 6776775..db94423 100644 --- a/apps/mobile/src/app/(tabs)/goals.tsx +++ b/apps/mobile/src/app/(tabs)/goals.tsx @@ -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 ( - + } > - - - - Fitness Goals - - Track your fitness journey progress - - - + + + Fitness Goals + + - - + Track your fitness journey progress + - + + + + {/* Stats Summary */} {goals && goals.length > 0 && ( - - - {activeGoals.length} - Active - - - {completedGoals.length} - Completed - - - - {activeGoals.length > 0 - ? Math.round( - activeGoals.reduce( - (sum, g) => sum + (g.progress || 0), - 0, - ) / activeGoals.length, - ) - : 0} - % - - Avg Progress + + + + + {activeGoals.length} + + + Active + + + + + + {completedGoals.length} + + + Completed + + + + + + {avgProgress}% + + + Avg Progress + + )} {/* Analytics Section */} {statistics && ( - - setShowAnalytics(!showAnalytics)} - > - + + + setShowAnalytics(!showAnalytics)} + activeOpacity={0.7} + > + + + + Progress Analytics + + - Progress Analytics - - - + - {showAnalytics && ( - - {statistics.weeklyTrend.length > 0 && ( - - )} - {statistics.goals.goalsByType.length > 0 && ( - - )} - - )} + {showAnalytics && ( + + {statistics.weeklyTrend.length > 0 && ( + + )} + {statistics.goals.goalsByType.length > 0 && ( + + )} + + )} + )} {/* Active Goals */} - - Active Goals ({activeGoals.length}) - + setIsModalVisible(true)} + /> {activeGoals.length === 0 ? ( - - - No active goals yet - - Tap the + button to create your first goal - - + + + + + No active goals yet + + + Tap "Add New" to create your first goal + + + ) : ( - activeGoals.map((goal) => ( - handleCompleteGoal(goal)} - onDelete={() => handleDeleteGoal(goal.id)} - /> - )) + + {activeGoals.map((goal) => ( + handleCompleteGoal(goal)} + onDelete={() => handleDeleteGoal(goal.id)} + /> + ))} + )} {/* Completed Goals */} {completedGoals.length > 0 && ( - - Completed Goals ({completedGoals.length}) - - {completedGoals.map((goal) => ( - handleDeleteGoal(goal.id)} - /> - ))} + + + {completedGoals.map((goal) => ( + handleDeleteGoal(goal.id)} + /> + ))} + )} - {/* Floating Action Button */} - + {/* Floating Action Button - Minimal Style */} + 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, + }, + ]} > - - - + - + {/* Create Goal Modal */} { 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 ( - + } > {/* Header Section */} - {getGreeting()}, - {user?.firstName || "Athlete"} + + {getGreeting()}, + + + {user?.firstName || "Athlete"} + - + {user?.imageUrl ? ( ) : ( - - + + )} - {/* Activity Widget */} - + {/* Daily Stats Card */} + + + + + + + + + {checkInsThisWeek} + + + This Week + + - {/* Quick Action Grid */} - setTrackMealModalVisible(true)} - onAddWaterPress={() => setAddWaterModalVisible(true)} - onScanFoodPress={() => setScanFoodModalVisible(true)} - onLogWorkoutPress={() => { - // TODO: Implement workout logging - console.log("Log workout tapped"); - }} - /> + - {/* Nutrition Widget */} - + + + + + + {calories} + + + Kcal + + - {/* Hydration Widget */} - + - {/* Weekly Progress Widget */} - + + + + + + {currentStreak} + + + Day Streak + + + + + + + {/* Quick Actions */} + + + + { + console.log("Log workout tapped"); + }} + style={styles.actionCard} + > + + + + + Log Workout + + + + setTrackMealModalVisible(true)} + style={styles.actionCard} + > + + + + + Track Meal + + + + setAddWaterModalVisible(true)} + style={styles.actionCard} + > + + + + + Add Water + + + + setScanFoodModalVisible(true)} + style={styles.actionCard} + > + + + + + Scan Food + + + + + + {/* Nutrition Progress */} + + + + + + + + Calories + + + + {calories} / {CALORIE_GOAL} + + + + + + + {/* Hydration Progress */} + + + + + + + Hydration + + + + {waterIntake} / {WATER_GOAL} ml + + + + + + + {/* Recent Activity */} + + console.log("See all tapped")} + /> + + + + + + + + + Upper Body Power + + + Today, 10:00 AM + + + + 45m + + + + + + + + + + Morning Cardio + + + Yesterday, 7:30 AM + + + + 30m + + + + - {/* Recent Activity Section */} - - - Recent Activity - See All - - - - - - - - - - - Upper Body Power - Today, 10:00 AM - - 45m - - - - - - - - - - Morning Cardio - Yesterday, 7:30 AM - - 30m - - - - {/* Bottom Spacer for Tab Bar */} @@ -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, }, }); diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx index a748417..10f86ed 100644 --- a/apps/mobile/src/app/(tabs)/profile.tsx +++ b/apps/mobile/src/app/(tabs)/profile.tsx @@ -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 ( - - - - - {user?.imageUrl ? ( - - ) : ( - - - - )} - - - - - {user?.fullName || "User"} - - {user?.primaryEmailAddress?.emailAddress} - - - Premium Member - - - + const handleSignOut = async () => { + try { + await signOut(); + } catch (err) { + log.error("Failed to sign out", err); + } + }; - - - Account - - 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 ( + + {/* Header Card */} + + + {user?.imageUrl ? ( + + ) : ( + - + + )} + + + {user?.fullName || "User"} + + + {user?.primaryEmailAddress?.emailAddress} + + + + + {/* Theme Settings */} + + + Appearance + + + + + + } + rightElement={ + + } + onPress={handleThemeChange} + /> + + + + {/* Account Settings */} + + + Account + + + - - Personal Details + + } + rightElement={ - - - router.push("/fitness-profile")} - > - router.push("/personal-details")} + /> + + - - Fitness Profile + + } + rightElement={ - - - - router.push("/fitness-profile")} + /> + + - - Notifications + + } + rightElement={ - - + } + /> + + - {/* Gym Selection */} - - + + + Gym Selection + + + - - Gym{" "} - {currentGymName - ? `(Current: ${currentGymName})` - : currentGymId - ? `(Current: ${currentGymId})` - : "(Current: None)"} + Refresh + + + + + {currentGymName && ( + + + Current Gym + + + {currentGymName} - - - Refresh Gyms - - + )} - {gymsLoading ? ( - - ) : ( + {gymsLoading ? ( + + + + ) : ( + <> - - setSelectedGymId(null)} + > + setSelectedGymId(null)} > - Proceed without gym - - {gyms.map((gym) => ( - + + {gyms.map((gym) => ( + setSelectedGymId(gym.id)} + > + setSelectedGymId(gym.id)} > - {gym.name} - - ))} - + {gym.name} + + + ))} - )} - - - - } + style={{ marginTop: 16 }} /> - - - + + )} + + - - Support - - - + + Support + + + - - Help Center + + } + rightElement={ - - - - + } + /> + + - - Privacy & Security + + } + rightElement={ - - - + } + /> + + - + } + size="lg" /> - - Version 1.0.0 - + + + Version 1.0.0 + + ); } 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, }, }); diff --git a/apps/mobile/src/app/(tabs)/recommendations.tsx b/apps/mobile/src/app/(tabs)/recommendations.tsx index bc6112e..2594d2a 100644 --- a/apps/mobile/src/app/(tabs)/recommendations.tsx +++ b/apps/mobile/src/app/(tabs)/recommendations.tsx @@ -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 ( - - + + ); } return ( - + } > {/* Header */} - + - AI Recommendations - + + AI Recommendations + + Personalized fitness & nutrition plans - - {unreadCount > 0 && ( - - {unreadCount} - - )} + + + {unreadCount > 0 && ( + + + {unreadCount} + + + )} + - + {/* Generate Button */} - - + - - {generating ? ( - - ) : ( - <> - - - Generate New Plan - - - )} - - + textStyle={{ fontSize: 16 }} + /> {/* Recommendations List */} + 0 + ? `${recommendations.length} active plan${recommendations.length !== 1 ? "s" : ""}` + : undefined + } + /> + {recommendations.length === 0 ? ( - - - - No Recommendations Yet - + + + + + + + No Recommendations Yet + + Tap "Generate New Plan" to get personalized AI-powered fitness and nutrition recommendations based on your profile and goals. - - + + ) : ( - recommendations.map((recommendation) => ( - - )) + + {recommendations.map((recommendation) => ( + + ))} + )} + + {/* Bottom Spacer */} + ); @@ -221,250 +254,195 @@ interface RecommendationCardProps { } function RecommendationCard({ recommendation }: RecommendationCardProps) { + const { colors, typography } = useTheme(); const [expanded, setExpanded] = useState(false); return ( - - + {/* Header */} + setExpanded(!expanded)} + activeOpacity={0.7} + style={styles.cardHeader} > - {/* Header */} - - - - - - - AI Fitness Plan - - {new Date(recommendation.generatedAt).toLocaleDateString()} + + + + + + + AI Fitness Plan + + + {new Date(recommendation.generatedAt).toLocaleDateString()} + + + + + + + {/* Summary */} + + + {recommendation.recommendationText} + + + + {/* Expanded Content */} + {expanded && ( + + {/* Activity Plan */} + + + + + Activity Plan + + {recommendation.activityPlan} + - setExpanded(!expanded)}> - - - - {/* Summary */} - - - {recommendation.recommendationText} - - - - {/* Expanded Content */} - {expanded && ( - - {/* Activity Plan */} - - - - Activity Plan - - {recommendation.activityPlan} - - - {/* Diet Plan */} - - - - Diet Plan - - {recommendation.dietPlan} + + + + Diet Plan + + + {recommendation.dietPlan} + - )} - - + + )} + ); } 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, }, }); diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 0ea3b95..e2c6872 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -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 ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); } diff --git a/apps/mobile/src/components/Badge.tsx b/apps/mobile/src/components/Badge.tsx new file mode 100644 index 0000000..17fb59c --- /dev/null +++ b/apps/mobile/src/components/Badge.tsx @@ -0,0 +1,124 @@ +import React from "react"; +import { View, Text, StyleSheet, ViewStyle, StyleProp } from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; +import { fontSize, fontWeight } from "../styles/typography"; + +type BadgeVariant = + | "neutral" + | "success" + | "warning" + | "danger" + | "info" + | "primary"; +type BadgeSize = "sm" | "md"; + +interface BadgeProps { + label: string; + variant?: BadgeVariant; + size?: BadgeSize; + style?: StyleProp; +} + +/** + * Badge - Pill-shaped status indicator + * + * Variants: + * - neutral: Gray badge for general labels + * - success: Green badge for positive status + * - warning: Orange/yellow badge for warnings + * - danger: Red badge for errors or critical status + * - info: Blue badge for informational status + * - primary: Primary color badge + * + * Sizes: + * - sm: 5px vertical, 10px horizontal, 11px font + * - md: 6px vertical, 12px horizontal, 13px font (default) + */ +export function Badge({ + label, + variant = "neutral", + size = "md", + style, +}: BadgeProps) { + const { colors } = useTheme(); + + const sizeStyles = { + sm: { + paddingVertical: 5, + paddingHorizontal: 10, + fontSize: fontSize.xs, + }, + md: { + paddingVertical: 6, + paddingHorizontal: 12, + fontSize: fontSize.sm, + }, + }; + + const variantStyles: Record< + BadgeVariant, + { backgroundColor: string; color: string } + > = { + neutral: { + backgroundColor: colors.surfaceElevated, + color: colors.textSecondary, + }, + success: { + backgroundColor: `${colors.success}20`, // 20% opacity + color: colors.success, + }, + warning: { + backgroundColor: `${colors.warning}20`, + color: colors.warning, + }, + danger: { + backgroundColor: `${colors.danger}20`, + color: colors.danger, + }, + info: { + backgroundColor: `${colors.info}20`, + color: colors.info, + }, + primary: { + backgroundColor: `${colors.primary}20`, + color: colors.primary, + }, + }; + + return ( + + + {label} + + + ); +} + +const styles = StyleSheet.create({ + badge: { + borderRadius: 999, // Full pill shape + alignSelf: "flex-start", + }, + label: { + textAlign: "center", + }, +}); diff --git a/apps/mobile/src/components/CustomTabBar.tsx b/apps/mobile/src/components/CustomTabBar.tsx index cad05e1..8ade46a 100644 --- a/apps/mobile/src/components/CustomTabBar.tsx +++ b/apps/mobile/src/components/CustomTabBar.tsx @@ -1,143 +1,129 @@ -import React from 'react'; -import { View, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native'; -import { BottomTabBarProps } from '@react-navigation/bottom-tabs'; -import { Ionicons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; -import { theme } from '../styles/theme'; -import { Animated } from 'react-native'; +import React from "react"; +import { View, StyleSheet, TouchableOpacity } from "react-native"; +import { BottomTabBarProps } from "@react-navigation/bottom-tabs"; +import { Ionicons } from "@expo/vector-icons"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useTheme } from "../contexts/ThemeContext"; -const { width } = Dimensions.get('window'); +/** + * CustomTabBar - Minimal bottom navigation with pill indicator + * + * Design: + * - Simple flat design (no floating, no glassmorphism) + * - Clean icons with outline/filled states + * - Small pill indicator below active tab + * - 56px height (reduced from 70px) + * - No animations (just opacity fade on press) + * - Theme-aware colors + */ +export function CustomTabBar({ + state, + descriptors, + navigation, +}: BottomTabBarProps) { + const { colors } = useTheme(); + const insets = useSafeAreaInsets(); -export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) { - return ( - - - {state.routes.map((route, index) => { - const { options } = descriptors[route.key]; - const isFocused = state.index === index; + return ( + + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]; + const isFocused = state.index === index; - const onPress = () => { - const event = navigation.emit({ - type: 'tabPress', - target: route.key, - canPreventDefault: true, - }); + const onPress = () => { + const event = navigation.emit({ + type: "tabPress", + target: route.key, + canPreventDefault: true, + }); - if (!isFocused && !event.defaultPrevented) { - navigation.navigate(route.name); - } - }; + if (!isFocused && !event.defaultPrevented) { + navigation.navigate(route.name); + } + }; - const getIconName = (routeName: string, focused: boolean): keyof typeof Ionicons.glyphMap => { - switch (routeName) { - case 'index': - return focused ? 'home' : 'home-outline'; - case 'goals': - return focused ? 'trophy' : 'trophy-outline'; - case 'attendance': - return focused ? 'calendar' : 'calendar-outline'; - case 'recommendations': - return focused ? 'sparkles' : 'sparkles-outline'; - case 'profile': - return focused ? 'person' : 'person-outline'; - default: - return 'ellipse-outline'; - } - }; + const getIconName = ( + routeName: string, + focused: boolean, + ): keyof typeof Ionicons.glyphMap => { + switch (routeName) { + case "index": + return focused ? "home" : "home-outline"; + case "goals": + return focused ? "trophy" : "trophy-outline"; + case "attendance": + return focused ? "calendar" : "calendar-outline"; + case "recommendations": + return focused ? "sparkles" : "sparkles-outline"; + case "profile": + return focused ? "person" : "person-outline"; + default: + return "ellipse-outline"; + } + }; - // Animation for scale - const scaleValue = React.useRef(new Animated.Value(1)).current; - - React.useEffect(() => { - Animated.spring(scaleValue, { - toValue: isFocused ? 1.2 : 1, - useNativeDriver: true, - friction: 8, - }).start(); - }, [isFocused]); - - return ( - - - {isFocused ? ( - - - - ) : ( - - )} - - - ); - })} - - - ); + return ( + + + + {isFocused && ( + + )} + + + ); + })} + + ); } const styles = StyleSheet.create({ - container: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - alignItems: 'center', - paddingBottom: Platform.OS === 'ios' ? 30 : 20, - pointerEvents: 'box-none', - }, - tabBar: { - flexDirection: 'row', - backgroundColor: 'rgba(255, 255, 255, 0.8)', - borderRadius: 35, - height: 70, - width: width - 40, - justifyContent: 'space-around', - alignItems: 'center', - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.5)', - paddingHorizontal: 10, - }, - tabItem: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - height: '100%', - }, - iconContainer: { - width: 40, - height: 40, - borderRadius: 20, - alignItems: 'center', - justifyContent: 'center', - shadowColor: theme.colors.primary, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 4, - }, + container: { + flexDirection: "row", + height: 56, + borderTopWidth: 1, + }, + tabItem: { + flex: 1, + alignItems: "center", + justifyContent: "center", + height: "100%", + }, + iconWrapper: { + alignItems: "center", + justifyContent: "center", + }, + indicator: { + width: 24, + height: 3, + borderRadius: 999, + marginTop: 4, + }, }); diff --git a/apps/mobile/src/components/IconContainer.tsx b/apps/mobile/src/components/IconContainer.tsx new file mode 100644 index 0000000..2b98ab2 --- /dev/null +++ b/apps/mobile/src/components/IconContainer.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import { View, StyleSheet, ViewStyle, StyleProp } from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; + +type IconContainerVariant = "plain" | "subtle" | "colored"; +type IconContainerSize = "sm" | "md" | "lg"; + +interface IconContainerProps { + children: React.ReactNode; + variant?: IconContainerVariant; + size?: IconContainerSize; + backgroundColor?: string; + style?: StyleProp; +} + +/** + * IconContainer - Clean container for icons with subtle backgrounds + * + * Variants: + * - plain: No background, just the icon + * - subtle: Light background (surfaceSecondary) + * - colored: Custom background color (pass backgroundColor prop) + * + * Sizes: + * - sm: 32px circle + * - md: 40px circle (default) + * - lg: 48px circle + */ +export function IconContainer({ + children, + variant = "subtle", + size = "md", + backgroundColor, + style, +}: IconContainerProps) { + const { colors } = useTheme(); + + const sizeStyles: Record = { + sm: { + width: 32, + height: 32, + borderRadius: 16, + }, + md: { + width: 40, + height: 40, + borderRadius: 20, + }, + lg: { + width: 48, + height: 48, + borderRadius: 24, + }, + }; + + const getBackgroundColor = (): string | undefined => { + if (variant === "colored" && backgroundColor) { + return backgroundColor; + } + if (variant === "subtle") { + return colors.surfaceElevated; + } + return "transparent"; + }; + + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/apps/mobile/src/components/ListItem.tsx b/apps/mobile/src/components/ListItem.tsx new file mode 100644 index 0000000..11fabc7 --- /dev/null +++ b/apps/mobile/src/components/ListItem.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ViewStyle, + StyleProp, +} from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; + +interface ListItemProps { + title: string; + subtitle?: string; + leftIcon?: React.ReactNode; + rightElement?: React.ReactNode; + onPress?: () => void; + style?: StyleProp; +} + +/** + * ListItem - Clean list item with optional icon and trailing element + * + * Usage: + * - Settings lists + * - Navigation items + * - Selectable options + * + * Features: + * - Optional left icon (wrap in IconContainer) + * - Optional subtitle for additional context + * - Optional right element (arrow, badge, switch, etc.) + * - Optional onPress for touchable items + */ +export function ListItem({ + title, + subtitle, + leftIcon, + rightElement, + onPress, + style, +}: ListItemProps) { + const { colors, typography } = useTheme(); + + const content = ( + + {leftIcon && {leftIcon}} + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + {rightElement && {rightElement}} + + ); + + if (onPress) { + return ( + + {content} + + ); + } + + return content; +} + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 12, + paddingHorizontal: 16, + backgroundColor: "transparent", + }, + leftIcon: { + marginRight: 12, + }, + textContainer: { + flex: 1, + }, + rightElement: { + marginLeft: 12, + }, +}); diff --git a/apps/mobile/src/components/MinimalButton.tsx b/apps/mobile/src/components/MinimalButton.tsx new file mode 100644 index 0000000..284dc1c --- /dev/null +++ b/apps/mobile/src/components/MinimalButton.tsx @@ -0,0 +1,152 @@ +import React from "react"; +import { + TouchableOpacity, + Text, + StyleSheet, + ActivityIndicator, + ViewStyle, + TextStyle, + StyleProp, +} from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; +import { fontSize, fontWeight } from "../styles/typography"; + +type ButtonVariant = "primary" | "secondary" | "tertiary" | "danger"; +type ButtonSize = "sm" | "md" | "lg"; + +interface MinimalButtonProps { + title: string; + onPress: () => void; + variant?: ButtonVariant; + size?: ButtonSize; + loading?: boolean; + disabled?: boolean; + style?: StyleProp; + textStyle?: StyleProp; +} + +/** + * MinimalButton - Clean button component with solid colors + * + * Variants: + * - primary: Solid primary background + * - secondary: Outlined with primary color + * - tertiary: Text only, no background + * - danger: Solid danger background + * + * Sizes: + * - sm: 12px vertical padding, 16px horizontal + * - md: 14px vertical padding, 24px horizontal (default) + * - lg: 16px vertical padding, 32px horizontal + */ +export function MinimalButton({ + title, + onPress, + variant = "primary", + size = "md", + loading = false, + disabled = false, + style, + textStyle, +}: MinimalButtonProps) { + const { colors } = useTheme(); + + const isDisabled = disabled || loading; + + // Get button styles based on variant + const getButtonStyle = (): ViewStyle => { + const baseStyle: ViewStyle = { + borderRadius: 10, + alignItems: "center", + justifyContent: "center", + opacity: isDisabled ? 0.5 : 1, + }; + + // Size-specific padding + const sizeStyles: Record< + ButtonSize, + { paddingVertical: number; paddingHorizontal: number } + > = { + sm: { paddingVertical: 12, paddingHorizontal: 16 }, + md: { paddingVertical: 14, paddingHorizontal: 24 }, + lg: { paddingVertical: 16, paddingHorizontal: 32 }, + }; + + const variantStyles: Record = { + primary: { + backgroundColor: colors.primary, + }, + secondary: { + backgroundColor: "transparent", + borderWidth: 1.5, + borderColor: colors.primary, + }, + tertiary: { + backgroundColor: "transparent", + }, + danger: { + backgroundColor: colors.danger, + }, + }; + + return { + ...baseStyle, + ...sizeStyles[size], + ...variantStyles[variant], + }; + }; + + // Get text styles based on variant + const getTextStyle = (): TextStyle => { + const baseTextStyle: TextStyle = { + fontSize: fontSize.base, + fontWeight: fontWeight.semibold, + }; + + const variantTextStyles: Record = { + primary: { + color: colors.white, + }, + secondary: { + color: colors.primary, + }, + tertiary: { + color: colors.primary, + }, + danger: { + color: colors.white, + }, + }; + + return { + ...baseTextStyle, + ...variantTextStyles[variant], + }; + }; + + return ( + + {loading ? ( + + ) : ( + {title} + )} + + ); +} + +const styles = StyleSheet.create({ + // No static styles needed - all dynamic based on theme +}); diff --git a/apps/mobile/src/components/MinimalCard.tsx b/apps/mobile/src/components/MinimalCard.tsx new file mode 100644 index 0000000..8b0d375 --- /dev/null +++ b/apps/mobile/src/components/MinimalCard.tsx @@ -0,0 +1,84 @@ +import React from "react"; +import { + View, + StyleSheet, + TouchableOpacity, + ViewStyle, + StyleProp, +} from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; + +type CardVariant = "default" | "elevated" | "bordered"; + +interface MinimalCardProps { + children: React.ReactNode; + variant?: CardVariant; + onPress?: () => void; + style?: StyleProp; +} + +/** + * MinimalCard - Clean card component without gradients + * + * Variants: + * - default: Subtle shadow on surface background + * - elevated: More prominent shadow + * - bordered: Border instead of shadow + */ +export function MinimalCard({ + children, + variant = "default", + onPress, + style, +}: MinimalCardProps) { + const { colors } = useTheme(); + + const cardStyles = [ + styles.base, + { + backgroundColor: colors.surface, + }, + variant === "default" && styles.default, + variant === "elevated" && styles.elevated, + variant === "bordered" && { + borderWidth: 1, + borderColor: colors.border, + }, + style, + ]; + + if (onPress) { + return ( + + {children} + + ); + } + + return {children}; +} + +const styles = StyleSheet.create({ + base: { + borderRadius: 12, + padding: 16, + }, + default: { + shadowColor: "#000", + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 3, + elevation: 1, + }, + elevated: { + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 2, + }, +}); diff --git a/apps/mobile/src/components/ProgressBar.tsx b/apps/mobile/src/components/ProgressBar.tsx new file mode 100644 index 0000000..362b9dd --- /dev/null +++ b/apps/mobile/src/components/ProgressBar.tsx @@ -0,0 +1,82 @@ +import React from "react"; +import { View, StyleSheet, ViewStyle, StyleProp } from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; + +interface ProgressBarProps { + progress: number; // 0-1 (e.g., 0.75 for 75%) + color?: string; + backgroundColor?: string; + height?: number; + borderRadius?: number; + style?: StyleProp; +} + +/** + * ProgressBar - Simple linear progress indicator + * + * Usage: + * - Goal progress tracking + * - Loading states + * - Completion indicators + * + * Props: + * - progress: Value between 0 and 1 (e.g., 0.75 for 75%) + * - color: Custom fill color (defaults to theme primary) + * - backgroundColor: Custom track color (defaults to theme border) + * - height: Bar height in pixels (defaults to 8) + * - borderRadius: Corner radius (defaults to 999 for full pill shape) + */ +export function ProgressBar({ + progress, + color, + backgroundColor, + height = 8, + borderRadius = 999, + style, +}: ProgressBarProps) { + const { colors } = useTheme(); + + // Clamp progress between 0 and 1 + const clampedProgress = Math.min(Math.max(progress, 0), 1); + + const trackColor = backgroundColor || colors.border; + const fillColor = color || colors.primary; + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + track: { + width: "100%", + overflow: "hidden", + }, + fill: { + position: "absolute", + left: 0, + top: 0, + }, +}); diff --git a/apps/mobile/src/components/SectionHeader.tsx b/apps/mobile/src/components/SectionHeader.tsx new file mode 100644 index 0000000..c7004bf --- /dev/null +++ b/apps/mobile/src/components/SectionHeader.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + ViewStyle, + StyleProp, +} from "react-native"; +import { useTheme } from "../contexts/ThemeContext"; + +interface SectionHeaderProps { + title: string; + subtitle?: string; + actionLabel?: string; + onActionPress?: () => void; + style?: StyleProp; +} + +/** + * SectionHeader - Clean section header with optional action button + * + * Usage: + * - Divides content into logical sections + * - Optional subtitle for additional context + * - Optional action button (e.g., "See All", "Add New") + */ +export function SectionHeader({ + title, + subtitle, + actionLabel, + onActionPress, + style, +}: SectionHeaderProps) { + const { colors, typography } = useTheme(); + + return ( + + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + {actionLabel && onActionPress && ( + + + {actionLabel} + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 12, + }, + textContainer: { + flex: 1, + }, +}); diff --git a/apps/mobile/src/contexts/ThemeContext.tsx b/apps/mobile/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..206bfad --- /dev/null +++ b/apps/mobile/src/contexts/ThemeContext.tsx @@ -0,0 +1,129 @@ +import React, { + createContext, + useContext, + useState, + useEffect, + ReactNode, +} from "react"; +import { useColorScheme } from "react-native"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { ColorScheme, lightColors, darkColors } from "../styles/colors"; +import { + TypographyPresets, + createTypographyPresets, +} from "../styles/typography"; + +type ThemeMode = "light" | "dark" | "system"; +type ActiveTheme = "light" | "dark"; + +interface ThemeContextType { + // Current active theme + theme: ActiveTheme; + + // User's theme preference + themeMode: ThemeMode; + + // Active color scheme + colors: ColorScheme; + + // Typography presets + typography: TypographyPresets; + + // Theme actions + setTheme: (mode: ThemeMode) => void; + toggleTheme: () => void; +} + +const ThemeContext = createContext(undefined); + +const THEME_STORAGE_KEY = "@fitai:theme"; + +interface ThemeProviderProps { + children: ReactNode; +} + +export function ThemeProvider({ children }: ThemeProviderProps) { + const systemColorScheme = useColorScheme(); + const [themeMode, setThemeMode] = useState("system"); + const [isLoading, setIsLoading] = useState(true); + + // Determine active theme based on mode and system preference + const getActiveTheme = (): ActiveTheme => { + if (themeMode === "system") { + return systemColorScheme === "dark" ? "dark" : "light"; + } + return themeMode; + }; + + const activeTheme = getActiveTheme(); + const colors = activeTheme === "dark" ? darkColors : lightColors; + const typography = createTypographyPresets( + colors.textPrimary, + colors.textSecondary, + colors.textTertiary, + ); + + // Load saved theme preference on mount + useEffect(() => { + const loadTheme = async () => { + try { + const savedTheme = await AsyncStorage.getItem(THEME_STORAGE_KEY); + if (savedTheme && ["light", "dark", "system"].includes(savedTheme)) { + setThemeMode(savedTheme as ThemeMode); + } + } catch (error) { + console.error("Failed to load theme preference:", error); + } finally { + setIsLoading(false); + } + }; + + loadTheme(); + }, []); + + // Save theme preference when it changes + const setTheme = async (mode: ThemeMode) => { + try { + setThemeMode(mode); + await AsyncStorage.setItem(THEME_STORAGE_KEY, mode); + } catch (error) { + console.error("Failed to save theme preference:", error); + } + }; + + // Toggle between light and dark (sets explicit mode, not system) + const toggleTheme = () => { + const newMode = activeTheme === "dark" ? "light" : "dark"; + setTheme(newMode); + }; + + // Don't render children until theme is loaded + if (isLoading) { + return null; + } + + const value: ThemeContextType = { + theme: activeTheme, + themeMode, + colors, + typography, + setTheme, + toggleTheme, + }; + + return ( + {children} + ); +} + +/** + * Hook to access theme context + * @throws Error if used outside ThemeProvider + */ +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} diff --git a/apps/mobile/src/styles/colors.ts b/apps/mobile/src/styles/colors.ts new file mode 100644 index 0000000..68198cd --- /dev/null +++ b/apps/mobile/src/styles/colors.ts @@ -0,0 +1,139 @@ +/** + * FitAI Color System + * Minimalist muted earth tones with light and dark mode support + */ + +export interface ColorScheme { + // Primary Earth Tones + primary: string; + primaryDark: string; + primaryLight: string; + + // Accent Colors + accent: string; + terracotta: string; + sand: string; + + // Status Colors + success: string; + warning: string; + danger: string; + info: string; + + // Neutrals + background: string; + surface: string; + surfaceElevated: string; + + // Text + textPrimary: string; + textSecondary: string; + textTertiary: string; + + // Borders + border: string; + borderLight: string; + + // Overlays + overlay: string; + overlayLight: string; + + // Legacy compatibility (will be phased out) + white: string; + black: string; +} + +/** + * Light Mode Color Palette + * Natural, warm, muted earth tones + */ +export const lightColors: ColorScheme = { + // Primary Earth Tones + primary: "#6B9080", // Muted sage green (main actions) + primaryDark: "#5A7A6E", // Darker sage + primaryLight: "#8AAE9E", // Lighter sage + + // Accent Colors (minimal use) + accent: "#A9B4A0", // Warm gray-green + terracotta: "#C1876B", // Warm terracotta (highlights) + sand: "#E8DCC4", // Warm sand (backgrounds) + + // Status Colors (muted) + success: "#7BA05B", // Muted olive green + warning: "#D4A574", // Muted amber + danger: "#B66B6B", // Muted rust red + info: "#7B9BB0", // Muted blue-gray + + // Neutrals + background: "#F9F7F4", // Warm off-white + surface: "#FFFFFF", // Pure white + surfaceElevated: "#FEFDFB", // Slightly warm white + + // Text + textPrimary: "#2C3731", // Deep forest + textSecondary: "#5C6B61", // Medium forest + textTertiary: "#8B9A8F", // Light forest + + // Borders + border: "#E8E4DF", // Very light warm gray + borderLight: "#F2EFE9", // Ultra light warm gray + + // Overlays + overlay: "rgba(44, 55, 49, 0.5)", + overlayLight: "rgba(44, 55, 49, 0.05)", + + // Legacy + white: "#FFFFFF", + black: "#2C3731", +}; + +/** + * Dark Mode Color Palette + * Warm dark tones with adjusted earth colors for contrast + */ +export const darkColors: ColorScheme = { + // Primary Earth Tones (adjusted for dark) + primary: "#8AAE9E", // Lighter sage for contrast + primaryDark: "#6B9080", + primaryLight: "#A4C4B5", + + // Accent Colors + accent: "#B5C2B0", + terracotta: "#D4A285", + sand: "#4A4539", // Dark sand + + // Status Colors + success: "#8DB76A", + warning: "#E0B886", + danger: "#C87D7D", + info: "#8FADC4", + + // Neutrals + background: "#1C1F1D", // Deep warm black + surface: "#252926", // Warm dark gray + surfaceElevated: "#2D3330", // Elevated warm dark + + // Text + textPrimary: "#E8E6E1", // Warm off-white + textSecondary: "#B8BFB5", // Medium warm gray + textTertiary: "#7A8379", // Muted warm gray + + // Borders + border: "#3A3F3C", // Subtle dark border + borderLight: "#2F3432", // Ultra subtle + + // Overlays + overlay: "rgba(0, 0, 0, 0.6)", + overlayLight: "rgba(255, 255, 255, 0.05)", + + // Legacy + white: "#E8E6E1", + black: "#1C1F1D", +}; + +/** + * Get color scheme based on theme mode + */ +export const getColors = (mode: "light" | "dark"): ColorScheme => { + return mode === "dark" ? darkColors : lightColors; +}; diff --git a/apps/mobile/src/styles/theme.ts b/apps/mobile/src/styles/theme.ts index 4a10c78..d7a023b 100644 --- a/apps/mobile/src/styles/theme.ts +++ b/apps/mobile/src/styles/theme.ts @@ -1,191 +1,159 @@ /** - * Modern Design System Theme - * Centralized theme configuration with gradients, colors, shadows, and spacing + * FitAI Design System Theme + * Minimalist theme with light/dark mode support + * @deprecated Use useTheme() hook instead for dynamic theming */ +import { lightColors, darkColors } from "./colors"; +import { typography as typographySystem } from "./typography"; + +/** + * Legacy theme object for backward compatibility + * New components should use useTheme() hook instead + */ export const theme = { - // Color Palette - colors: { - // Primary colors - primary: '#3b82f6', - primaryDark: '#2563eb', - primaryLight: '#60a5fa', - secondary: '#8b5cf6', + // Color Palette (light mode - for legacy components) + colors: { + ...lightColors, - // Accent colors - purple: '#8b5cf6', - purpleDark: '#7c3aed', - pink: '#ec4899', + // Legacy color mappings (deprecated - use new colors instead) + secondary: "#8b5cf6", // Old purple - deprecated + purple: "#A9B4A0", // Mapped to accent + purpleDark: "#5A7A6E", + pink: "#C1876B", // Mapped to terracotta + successLight: "#8DB76A", + gray50: "#F2EFE9", + gray100: "#E8E4DF", + gray200: "#E8E4DF", + gray300: "#B8BFB5", + gray400: "#8B9A8F", + gray500: "#5C6B61", + gray600: "#5C6B61", + gray700: "#2C3731", + gray800: "#2C3731", + gray900: "#2C3731", + }, - // Success - success: '#10b981', - successDark: '#059669', - successLight: '#34d399', + // Typography (updated to new system) + typography: typographySystem, - // Warning - warning: '#f59e0b', - warningDark: '#d97706', + // Spacing Scale (updated for minimalism) + spacing: { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 24, // Increased from 20 + "2xl": 32, // Increased from 24 + "3xl": 40, // Increased from 32 + "4xl": 48, // New + "5xl": 64, // New + }, - // Danger - danger: '#ef4444', - dangerDark: '#dc2626', + // Border Radius (reduced for minimalism) + borderRadius: { + sm: 4, + md: 6, + lg: 10, + xl: 12, // Reduced from 16 + "2xl": 16, // Reduced from 20 + "3xl": 20, // Reduced from 24 + full: 9999, + }, - // Neutrals - white: '#ffffff', - black: '#000000', - gray50: '#f9fafb', - gray100: '#f3f4f6', - gray200: '#e5e7eb', - gray300: '#d1d5db', - gray400: '#9ca3af', - gray500: '#6b7280', - gray600: '#4b5563', - gray700: '#374151', - gray800: '#1f2937', - gray900: '#111827', - - // Backgrounds - background: '#f5f5f5', - backgroundDark: '#0f172a', - surface: '#ffffff', - surfaceDark: '#1e293b', + // Shadow System (simplified) + shadows: { + subtle: { + shadowColor: "#000", + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 3, + elevation: 1, }, - - // Gradient Definitions - gradients: { - primary: ['#3b82f6', '#8b5cf6'] as const, - primaryVertical: ['#3b82f6', '#2563eb'] as const, - success: ['#10b981', '#059669'] as const, - warning: ['#f59e0b', '#d97706'] as const, - danger: ['#ef4444', '#ec4899'] as const, - purple: ['#8b5cf6', '#7c3aed'] as const, - ocean: ['#06b6d4', '#3b82f6'] as const, - sunset: ['#f59e0b', '#ef4444'] as const, - forest: ['#10b981', '#059669'] as const, - lavender: ['#a78bfa', '#ec4899'] as const, - dark: ['#1e293b', '#0f172a'] as const, + medium: { + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 8, + elevation: 2, }, - - // Shadow System - shadows: { - subtle: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, - }, - medium: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, - }, - strong: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 5, - }, - glow: { - shadowColor: '#3b82f6', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 12, - elevation: 8, - }, - glowDanger: { - shadowColor: '#ef4444', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 12, - elevation: 8, - }, + strong: { + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.12, + shadowRadius: 16, + elevation: 4, }, - - // Typography - typography: { - // Font sizes - fontSize: { - xs: 12, - sm: 14, - base: 16, - lg: 18, - xl: 20, - '2xl': 24, - '3xl': 28, - '4xl': 32, - '5xl': 36, - }, - // Font weights - fontWeight: { - normal: '400' as const, - medium: '500' as const, - semibold: '600' as const, - bold: '700' as const, - extrabold: '800' as const, - }, - // Line heights - lineHeight: { - tight: 1.2, - normal: 1.5, - relaxed: 1.75, - }, + // Legacy glow shadows (deprecated - avoid in new designs) + glow: { + shadowColor: "#6B9080", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 12, + elevation: 6, }, - - // Spacing Scale - spacing: { - xs: 4, - sm: 8, - md: 12, - lg: 16, - xl: 20, - '2xl': 24, - '3xl': 32, - '4xl': 40, - '5xl': 48, + glowDanger: { + shadowColor: "#B66B6B", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 12, + elevation: 6, }, + }, - // Border Radius - borderRadius: { - sm: 4, - md: 8, - lg: 12, - xl: 16, - '2xl': 20, - '3xl': 24, - full: 9999, + // Animation Timing (simplified) + animation: { + duration: { + fast: 200, + normal: 300, + slow: 500, }, + }, - // Animation Timing - animation: { - duration: { - fast: 150, - normal: 250, - slow: 350, - }, - easing: { - easeIn: 'ease-in', - easeOut: 'ease-out', - easeInOut: 'ease-in-out', - }, - }, + // Gradients (kept for legacy compatibility - should be avoided in new designs) + gradients: { + primary: ["#6B9080", "#8AAE9E"] as const, + success: ["#7BA05B", "#8DB76A"] as const, + warning: ["#D4A574", "#E0B886"] as const, + danger: ["#B66B6B", "#C87D7D"] as const, + // Legacy gradients (deprecated) + purple: ["#8b5cf6", "#7c3aed"] as const, + ocean: ["#06b6d4", "#3b82f6"] as const, + sunset: ["#f59e0b", "#ef4444"] as const, + forest: ["#10b981", "#059669"] as const, + lavender: ["#a78bfa", "#ec4899"] as const, + dark: ["#1e293b", "#0f172a"] as const, + primaryVertical: ["#6B9080", "#5A7A6E"] as const, + }, +}; - // Glassmorphism - glass: { - light: { - backgroundColor: 'rgba(255, 255, 255, 0.7)', - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.3)', - }, - dark: { - backgroundColor: 'rgba(0, 0, 0, 0.3)', - borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.1)', - }, - }, +/** + * Dark theme object + * @deprecated Use useTheme() hook instead + */ +export const darkTheme = { + ...theme, + colors: { + ...darkColors, + + // Legacy color mappings + secondary: "#8b5cf6", // Old purple - deprecated + purple: "#B5C2B0", // Mapped to dark accent + purpleDark: "#6B9080", + pink: "#D4A285", // Mapped to dark terracotta + successLight: "#8DB76A", + gray50: "#2F3432", + gray100: "#3A3F3C", + gray200: "#3A3F3C", + gray300: "#7A8379", + gray400: "#7A8379", + gray500: "#B8BFB5", + gray600: "#B8BFB5", + gray700: "#E8E6E1", + gray800: "#E8E6E1", + gray900: "#E8E6E1", + }, }; export type Theme = typeof theme; +export type DarkTheme = typeof darkTheme; diff --git a/apps/mobile/src/styles/typography.ts b/apps/mobile/src/styles/typography.ts new file mode 100644 index 0000000..37bb3a1 --- /dev/null +++ b/apps/mobile/src/styles/typography.ts @@ -0,0 +1,145 @@ +/** + * FitAI Typography System + * Minimalist typography with clear hierarchy using system fonts + */ + +import { TextStyle } from "react-native"; + +/** + * Font Sizes + * Refined scale with fewer sizes for clearer hierarchy + */ +export const fontSize = { + xs: 11, + sm: 13, + base: 15, + md: 17, // Body emphasis + lg: 20, + xl: 24, + "2xl": 28, + "3xl": 34, + "4xl": 40, +} as const; + +/** + * Font Weights + */ +export const fontWeight = { + regular: "400" as TextStyle["fontWeight"], + medium: "500" as TextStyle["fontWeight"], + semibold: "600" as TextStyle["fontWeight"], + bold: "700" as TextStyle["fontWeight"], +} as const; + +/** + * Line Heights + */ +export const lineHeight = { + tight: 1.2, + normal: 1.5, + relaxed: 1.7, +} as const; + +/** + * Letter Spacing + */ +export const letterSpacing = { + tight: -0.5, + normal: 0, + wide: 0.5, + wider: 1, +} as const; + +/** + * Typography Presets + * Ready-to-use text styles for common use cases + */ +export interface TypographyPresets { + h1: TextStyle; + h2: TextStyle; + h3: TextStyle; + body: TextStyle; + bodyEmphasis: TextStyle; + label: TextStyle; + stat: TextStyle; + caption: TextStyle; +} + +export const createTypographyPresets = ( + textPrimary: string, + textSecondary: string, + textTertiary: string, +): TypographyPresets => ({ + // Display Text (Screen Titles) + h1: { + fontSize: fontSize["3xl"], + fontWeight: fontWeight.bold, + letterSpacing: letterSpacing.tight, + lineHeight: fontSize["3xl"] * lineHeight.tight, + color: textPrimary, + }, + + // Section Headers + h2: { + fontSize: fontSize.xl, + fontWeight: fontWeight.semibold, + letterSpacing: -0.3, + color: textPrimary, + }, + + // Card Titles + h3: { + fontSize: fontSize.md, + fontWeight: fontWeight.semibold, + color: textPrimary, + }, + + // Body Text + body: { + fontSize: fontSize.base, + fontWeight: fontWeight.regular, + lineHeight: fontSize.base * lineHeight.normal, + color: textSecondary, + }, + + // Emphasized Body + bodyEmphasis: { + fontSize: fontSize.md, + fontWeight: fontWeight.medium, + lineHeight: fontSize.md * lineHeight.normal, + color: textPrimary, + }, + + // Labels (uppercase, spaced) + label: { + fontSize: fontSize.sm, + fontWeight: fontWeight.medium, + letterSpacing: letterSpacing.wide, + textTransform: "uppercase", + color: textTertiary, + }, + + // Stats/Numbers + stat: { + fontSize: fontSize["2xl"], + fontWeight: fontWeight.bold, + color: textPrimary, + }, + + // Caption/Small text + caption: { + fontSize: fontSize.xs, + fontWeight: fontWeight.regular, + color: textTertiary, + }, +}); + +/** + * Typography utility object + */ +export const typography = { + fontSize, + fontWeight, + lineHeight, + letterSpacing, +};