From 73d2c4c1edc465311b8d01aea9143b86ea2273ba Mon Sep 17 00:00:00 2001 From: echo Date: Wed, 26 Nov 2025 01:57:33 +0100 Subject: [PATCH] claude take :( --- apps/admin/data/fitai.db | Bin 139264 -> 139264 bytes apps/mobile/package-lock.json | 12 + apps/mobile/package.json | 1 + apps/mobile/src/app/(tabs)/attendance.tsx | 266 +++++++++++---- apps/mobile/src/app/(tabs)/goals.tsx | 102 ++++-- apps/mobile/src/app/(tabs)/index.tsx | 319 +++++++++++------- apps/mobile/src/app/(tabs)/profile.tsx | 312 ++++++++++------- .../mobile/src/app/(tabs)/recommendations.tsx | 176 ++++++---- apps/mobile/src/components/AnimatedButton.tsx | 136 ++++++++ .../src/components/GoalProgressCard.tsx | 268 ++++++++------- .../src/components/GradientBackground.tsx | 46 +++ apps/mobile/src/styles/theme.ts | 190 +++++++++++ apps/mobile/src/utils/animations.ts | 149 ++++++++ 13 files changed, 1460 insertions(+), 517 deletions(-) create mode 100644 apps/mobile/src/components/AnimatedButton.tsx create mode 100644 apps/mobile/src/components/GradientBackground.tsx create mode 100644 apps/mobile/src/styles/theme.ts create mode 100644 apps/mobile/src/utils/animations.ts diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 56f5a5314efec278d6cfd68154672b93091b70f3..f1ddbc57d4b9b4d38e93f5525fc57d3ece177df6 100644 GIT binary patch delta 187 zcmZoTz|nAkV}dl}w}~>&jNdjUEPT(+&&R;XC(Xbo%_qHCQGlOsvh=^tEDQ_`pEe8q ze=oqze}{pQe>(&JcK$n?1p`*|Pv0)js0xx-+5SMDQSkthg6&WR@1Y7N2mVvw;o|=c zG~g)%|5N^_TNRnE@oNC}@-oXaCgY^KWP1-_Cz$vtYn#{^{H08C6-7nSD98KagisJg}Lu;1K_G9X&<~Rwj_>Q(&BGWZ~4WJdg%<_!M`MCu-sU@i?Ogzl;oNyi&$OZi08TkKf(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 { @@ -70,42 +94,100 @@ export default function AttendanceScreen() { if (loading && !history.length) { return ( - + ) } return ( - Attendance - Track your gym visits + + Attendance + Track your gym visits + {activeCheckIn ? ( - - Currently Checked In - - Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - - - Check Out + + + + + + + + + Currently Checked In + + Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + + + + + + Check Out + - + ) : ( - - Check In - + + + + + Check In + + + )} Recent History {history.map((item) => ( - - - - {new Date(item.checkInTime).toLocaleDateString()} - - {item.type.toUpperCase()} + + + + + + + + + + {new Date(item.checkInTime).toLocaleDateString()} + + {item.type.toUpperCase()} + @@ -117,7 +199,7 @@ export default function AttendanceScreen() { )} - + ))} ) @@ -126,105 +208,147 @@ export default function AttendanceScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f5f5f5', + backgroundColor: theme.colors.background, }, content: { - padding: 20, + paddingBottom: 20, }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center', }, + header: { + paddingTop: 60, + paddingBottom: 24, + paddingHorizontal: 24, + marginBottom: 24, + borderBottomLeftRadius: theme.borderRadius.xl, + borderBottomRightRadius: theme.borderRadius.xl, + }, title: { - fontSize: 28, - fontWeight: 'bold', + fontSize: theme.typography.fontSize['3xl'], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.white, marginBottom: 4, - color: '#1a1a1a', }, subtitle: { - fontSize: 16, - color: '#666', - marginBottom: 24, + fontSize: theme.typography.fontSize.base, + color: 'rgba(255, 255, 255, 0.9)', }, actionContainer: { marginBottom: 32, + paddingHorizontal: 20, }, checkInButton: { - backgroundColor: '#000', - paddingVertical: 16, - borderRadius: 12, + paddingVertical: 20, + paddingHorizontal: 24, + borderRadius: theme.borderRadius.xl, + flexDirection: 'row', alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, + justifyContent: 'center', + }, + checkInButtonText: { + color: theme.colors.white, + fontSize: theme.typography.fontSize.xl, + fontWeight: theme.typography.fontWeight.semibold, }, checkOutButton: { - backgroundColor: '#ff3b30', - paddingVertical: 16, - borderRadius: 12, + paddingVertical: 14, + paddingHorizontal: 20, + borderRadius: theme.borderRadius.lg, + flexDirection: 'row', alignItems: 'center', + justifyContent: 'center', marginTop: 16, }, buttonText: { - color: '#fff', - fontSize: 18, - fontWeight: '600', + color: theme.colors.white, + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, }, activeCard: { - backgroundColor: '#fff', padding: 20, - borderRadius: 16, + borderRadius: theme.borderRadius.xl, borderWidth: 1, - borderColor: '#e0e0e0', + 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: 18, - fontWeight: '600', - color: '#2e7d32', + fontSize: theme.typography.fontSize.lg, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, marginBottom: 4, }, timeText: { - fontSize: 14, - color: '#666', + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray600, }, sectionTitle: { - fontSize: 20, - fontWeight: '600', + fontSize: theme.typography.fontSize.xl, + fontWeight: theme.typography.fontWeight.semibold, marginBottom: 16, - color: '#1a1a1a', + paddingHorizontal: 20, + color: theme.colors.gray900, }, historyItem: { - backgroundColor: '#fff', padding: 16, - borderRadius: 12, + borderRadius: theme.borderRadius.xl, marginBottom: 12, + marginHorizontal: 20, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, + 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: 16, - fontWeight: '500', - color: '#1a1a1a', + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, }, typeText: { - fontSize: 12, - color: '#666', + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray600, marginTop: 2, }, timeContainer: { alignItems: 'flex-end', }, historyTime: { - fontSize: 14, - color: '#444', + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray700, }, }) \ No newline at end of file diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx index ed7872a..b141fce 100644 --- a/apps/mobile/src/app/(tabs)/goals.tsx +++ b/apps/mobile/src/app/(tabs)/goals.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { View, Text, @@ -8,12 +8,15 @@ import { ActivityIndicator, RefreshControl, Alert, + Animated, } from "react-native"; import { useAuth } from "@clerk/clerk-expo"; import { Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from "../../services/fitnessGoals"; import { GoalProgressCard } from "../../components/GoalProgressCard"; import { GoalCreationModal } from "../../components/GoalCreationModal"; +import { theme } from "../../styles/theme"; export default function GoalsScreen() { const { userId, getToken } = useAuth(); @@ -21,6 +24,7 @@ export default function GoalsScreen() { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false); + const fabScale = useRef(new Animated.Value(1)).current; const fetchGoals = async () => { if (!userId) return; @@ -100,14 +104,19 @@ export default function GoalsScreen() { } > - + My Fitness Goals Track your fitness journey progress - + {/* Stats Summary */} {goals.length > 0 && ( @@ -179,12 +188,37 @@ export default function GoalsScreen() { {/* Floating Action Button */} - setShowCreateModal(true)} - > - - + + setShowCreateModal(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} + > + + + + + {/* Create Goal Modal */} { + Animated.timing(fadeAnim, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }).start(); + }, []); if (!isLoaded || !user) { return ( @@ -27,32 +39,67 @@ export default function HomeScreen() { return ( - - {/* Welcome Header */} - + + {/* Gradient Header */} + {greeting}! {firstName} - + - {/* Quick Stats */} + {/* Quick Stats with Glassmorphism */} - - + + + + + + 0 This Month - + - - + + + + + + 0 Day Streak - + - - + + + + + + 0 Total Visits - + {/* Quick Actions */} @@ -60,12 +107,16 @@ export default function HomeScreen() { Quick Actions router.push("/fitness-profile")} + activeOpacity={0.7} > - + - + Fitness Profile @@ -75,12 +126,16 @@ export default function HomeScreen() { - - - - + + - + Check In @@ -88,13 +143,18 @@ export default function HomeScreen() { - + - - - + + - + View Schedule @@ -102,24 +162,35 @@ export default function HomeScreen() { - + - - + + - + Payments View payment history - + - {/* Membership Info */} + {/* Membership Info with Gradient */} Membership - + Basic Plan @@ -132,21 +203,28 @@ export default function HomeScreen() { Member since {new Date(user.createdAt!).toLocaleDateString()} - + {/* Recent Activity */} Recent Activity - - + + + + + + No recent activity Check in to start tracking your workouts - + ); } @@ -161,81 +239,90 @@ function getGreeting(): string { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#f5f5f5", + backgroundColor: theme.colors.background, }, content: { - padding: 20, + paddingBottom: 20, }, header: { - marginBottom: 24, + padding: 24, + paddingTop: 60, + paddingBottom: 32, + borderBottomLeftRadius: theme.borderRadius['2xl'], + borderBottomRightRadius: theme.borderRadius['2xl'], + marginBottom: 20, }, greeting: { - fontSize: 16, - color: "#6b7280", + fontSize: theme.typography.fontSize.base, + color: "rgba(255, 255, 255, 0.9)", marginBottom: 4, }, name: { - fontSize: 32, - fontWeight: "bold", - color: "#1a1a1a", + fontSize: theme.typography.fontSize['4xl'], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.white, }, statsContainer: { flexDirection: "row", justifyContent: "space-between", + paddingHorizontal: 16, marginBottom: 24, + gap: 12, }, statCard: { flex: 1, - backgroundColor: "white", - borderRadius: 12, + backgroundColor: theme.colors.white, + borderRadius: theme.borderRadius.xl, padding: 16, alignItems: "center", - marginHorizontal: 4, - shadowColor: "#000", - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, + borderWidth: 1, + borderColor: "rgba(255, 255, 255, 0.3)", + }, + statIconContainer: { + marginBottom: 8, + }, + statIconGradient: { + width: 48, + height: 48, + borderRadius: 24, + justifyContent: "center", + alignItems: "center", }, statValue: { - fontSize: 24, - fontWeight: "bold", - color: "#1a1a1a", - marginTop: 8, + fontSize: theme.typography.fontSize['2xl'], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.gray900, + marginTop: 4, marginBottom: 4, }, statLabel: { - fontSize: 12, - color: "#6b7280", + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray600, textAlign: "center", + fontWeight: theme.typography.fontWeight.medium, }, section: { marginBottom: 24, + paddingHorizontal: 20, }, sectionTitle: { - fontSize: 18, - fontWeight: "600", - color: "#1a1a1a", + fontSize: theme.typography.fontSize.xl, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, marginBottom: 12, }, actionButton: { flexDirection: "row", alignItems: "center", - backgroundColor: "white", - borderRadius: 12, + backgroundColor: theme.colors.white, + borderRadius: theme.borderRadius.xl, padding: 16, - marginBottom: 8, - shadowColor: "#000", - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, + marginBottom: 12, }, actionIcon: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: "#f0f9ff", + width: 56, + height: 56, + borderRadius: 28, justifyContent: "center", alignItems: "center", marginRight: 12, @@ -244,77 +331,77 @@ const styles = StyleSheet.create({ flex: 1, }, actionTitle: { - fontSize: 16, - fontWeight: "600", - color: "#1a1a1a", + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, marginBottom: 2, }, actionSubtitle: { - fontSize: 14, - color: "#6b7280", + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray600, }, membershipCard: { - backgroundColor: "white", - borderRadius: 12, - padding: 16, - shadowColor: "#000", - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, + borderRadius: theme.borderRadius.xl, + padding: 20, }, membershipHeader: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", - marginBottom: 8, + marginBottom: 12, }, membershipType: { - fontSize: 18, - fontWeight: "600", - color: "#1a1a1a", + fontSize: theme.typography.fontSize.xl, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.white, }, statusBadge: { - backgroundColor: "#dcfce7", + backgroundColor: "rgba(255, 255, 255, 0.25)", paddingHorizontal: 12, - paddingVertical: 4, - borderRadius: 12, + paddingVertical: 6, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: "rgba(255, 255, 255, 0.3)", }, statusText: { - fontSize: 12, - fontWeight: "600", - color: "#16a34a", + fontSize: theme.typography.fontSize.xs, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.white, }, membershipEmail: { - fontSize: 14, - color: "#6b7280", + fontSize: theme.typography.fontSize.sm, + color: "rgba(255, 255, 255, 0.9)", marginBottom: 4, }, membershipDate: { - fontSize: 12, - color: "#9ca3af", + fontSize: theme.typography.fontSize.xs, + color: "rgba(255, 255, 255, 0.7)", }, emptyState: { - backgroundColor: "white", - borderRadius: 12, + backgroundColor: theme.colors.white, + borderRadius: theme.borderRadius.xl, padding: 32, alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 2, - elevation: 2, + }, + emptyIconContainer: { + marginBottom: 16, + }, + emptyIconGradient: { + width: 96, + height: 96, + borderRadius: 48, + justifyContent: "center", + alignItems: "center", }, emptyStateText: { - fontSize: 16, - fontWeight: "600", - color: "#6b7280", - marginTop: 12, + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray700, marginBottom: 4, }, emptyStateSubtext: { - fontSize: 14, - color: "#9ca3af", + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray500, textAlign: "center", }, }); diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx index a19be97..9e096c1 100644 --- a/apps/mobile/src/app/(tabs)/profile.tsx +++ b/apps/mobile/src/app/(tabs)/profile.tsx @@ -10,6 +10,8 @@ import { import { useUser, 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"; export default function ProfileScreen() { const { user } = useUser(); @@ -45,20 +47,31 @@ export default function ProfileScreen() { return ( - {/* Profile Header */} - + {/* Profile Header with Gradient */} + {user.imageUrl ? ( - + {user.firstName?.charAt(0)} {user.lastName?.charAt(0)} - + ) : ( - + - + )} @@ -74,58 +87,80 @@ export default function ProfileScreen() { {user.primaryPhoneNumber.phoneNumber} )} - + {/* Account Information */} Account Information - - - - Email - - - {user.primaryEmailAddress?.emailAddress} - - - - {user.primaryPhoneNumber && ( + - - Phone + + + + Email - {user.primaryPhoneNumber.phoneNumber} + {user.primaryEmailAddress?.emailAddress} - )} - - - - Member Since - - - {new Date(user.createdAt!).toLocaleDateString()} - - + {user.primaryPhoneNumber && ( + + + + + + Phone + + + {user.primaryPhoneNumber.phoneNumber} + + + )} - - - - Email Verified + + + + + + Member Since + + + {new Date(user.createdAt!).toLocaleDateString()} + + + + + + + + + Email Verified + + + {user.primaryEmailAddress?.verification?.status === "verified" + ? "Yes" + : "No"} + - - {user.primaryEmailAddress?.verification?.status === "verified" - ? "Yes" - : "No"} - @@ -133,35 +168,62 @@ export default function ProfileScreen() { Quick Actions - - + + + + Edit Profile - + - - + + + + Notifications - + - - + + + + Payment History - + - - + + + + Settings - + {/* Sign Out Button */} - - - Sign Out + + + + Sign Out + {/* App Version */} @@ -174,133 +236,135 @@ export default function ProfileScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#f5f5f5", + backgroundColor: theme.colors.background, }, content: { padding: 20, + paddingTop: 60, }, profileCard: { - backgroundColor: "white", - borderRadius: 16, - padding: 24, + borderRadius: theme.borderRadius.xl, + padding: 28, alignItems: "center", - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 4, marginBottom: 24, }, avatarContainer: { marginBottom: 16, }, avatar: { - width: 80, - height: 80, - borderRadius: 40, - backgroundColor: "#2563eb", + width: 96, + height: 96, + borderRadius: 48, justifyContent: "center", alignItems: "center", + borderWidth: 3, + borderColor: "rgba(255, 255, 255, 0.3)", }, avatarText: { - fontSize: 32, - fontWeight: "bold", - color: "#fff", + fontSize: theme.typography.fontSize['4xl'], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.white, }, name: { - fontSize: 24, - fontWeight: "bold", - color: "#1a1a1a", - marginBottom: 4, + fontSize: theme.typography.fontSize['2xl'], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.white, + marginBottom: 6, }, email: { - fontSize: 16, - color: "#666", + fontSize: theme.typography.fontSize.base, + color: "rgba(255, 255, 255, 0.9)", marginBottom: 4, }, phone: { - fontSize: 14, - color: "#999", + fontSize: theme.typography.fontSize.sm, + color: "rgba(255, 255, 255, 0.8)", }, section: { - backgroundColor: "white", - borderRadius: 16, - padding: 20, - marginBottom: 16, - shadowColor: "#000", - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.05, - shadowRadius: 4, - elevation: 2, + marginBottom: 24, }, sectionTitle: { - fontSize: 18, - fontWeight: "600", - color: "#1a1a1a", - marginBottom: 16, + fontSize: theme.typography.fontSize.xl, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, + marginBottom: 12, + }, + infoCard: { + backgroundColor: theme.colors.white, + borderRadius: theme.borderRadius.xl, + padding: 16, }, infoRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", - paddingVertical: 12, + paddingVertical: 14, borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", + borderBottomColor: theme.colors.gray200, }, infoLabel: { flexDirection: "row", alignItems: "center", - gap: 8, + gap: 10, + }, + infoIconContainer: { + width: 36, + height: 36, + borderRadius: 18, + justifyContent: "center", + alignItems: "center", }, infoLabelText: { - fontSize: 14, - color: "#666", - fontWeight: "500", + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray700, + fontWeight: theme.typography.fontWeight.medium, }, infoValue: { - fontSize: 14, - color: "#1a1a1a", - fontWeight: "500", + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray900, + fontWeight: theme.typography.fontWeight.semibold, }, actionButton: { flexDirection: "row", alignItems: "center", - paddingVertical: 16, - borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", + backgroundColor: theme.colors.white, + borderRadius: theme.borderRadius.xl, + padding: 16, + marginBottom: 12, + }, + actionIconContainer: { + width: 44, + height: 44, + borderRadius: 22, + justifyContent: "center", + alignItems: "center", + marginRight: 12, }, actionButtonText: { flex: 1, - fontSize: 16, - color: "#1a1a1a", - marginLeft: 12, - fontWeight: "500", + fontSize: theme.typography.fontSize.base, + color: theme.colors.gray900, + fontWeight: theme.typography.fontWeight.medium, }, logoutButton: { - backgroundColor: "#ef4444", paddingVertical: 16, - borderRadius: 12, + borderRadius: theme.borderRadius.lg, alignItems: "center", justifyContent: "center", flexDirection: "row", gap: 8, marginTop: 8, marginBottom: 16, - shadowColor: "#ef4444", - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 4, }, logoutText: { - color: "white", - fontSize: 16, - fontWeight: "600", + color: theme.colors.white, + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, }, version: { textAlign: "center", - fontSize: 12, - color: "#999", + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray500, marginTop: 8, marginBottom: 20, }, diff --git a/apps/mobile/src/app/(tabs)/recommendations.tsx b/apps/mobile/src/app/(tabs)/recommendations.tsx index d3f668f..c402d16 100644 --- a/apps/mobile/src/app/(tabs)/recommendations.tsx +++ b/apps/mobile/src/app/(tabs)/recommendations.tsx @@ -2,7 +2,9 @@ import { useEffect, useState } from "react"; import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native"; import { useAuth } from "@clerk/clerk-expo"; import { Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; import { API_BASE_URL, API_ENDPOINTS } from "../../config/api"; +import { theme } from "../../styles/theme"; interface Recommendation { id: string; @@ -70,37 +72,71 @@ export default function RecommendationsScreen() { if (loading) { return ( - + ); } return ( - AI Recommendations + + AI Recommendations + - {/* AI Context Info Banner */} - - + {/* AI Context Info Banner with Glassmorphism */} + + + + + + Personalized based on your active fitness goals and progress - + item.id} refreshControl={} + contentContainerStyle={styles.listContent} ListEmptyComponent={ + + + + + No recommendations available yet. Pull down to refresh } renderItem={({ item }) => ( - + - {item.status.toUpperCase()} + + {item.status.toUpperCase()} + {new Date(item.createdAt).toLocaleDateString()} @@ -120,7 +156,7 @@ export default function RecommendationsScreen() { {item.dietPlan} )} - + )} /> @@ -130,83 +166,99 @@ export default function RecommendationsScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#f5f5f5', + backgroundColor: theme.colors.background, }, header: { - fontSize: 28, - fontWeight: 'bold', - padding: 20, - paddingBottom: 12, - color: '#1a1a1a', + paddingTop: 60, + paddingBottom: 24, + paddingHorizontal: 24, + borderBottomLeftRadius: theme.borderRadius.xl, + borderBottomRightRadius: theme.borderRadius.xl, + }, + headerTitle: { + fontSize: theme.typography.fontSize['3xl'], + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.white, }, infoBanner: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#eff6ff', marginHorizontal: 16, + marginTop: 16, marginBottom: 12, - padding: 12, - borderRadius: 8, - borderLeftWidth: 3, - borderLeftColor: '#2563eb', - gap: 8, + padding: 14, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: 'rgba(59, 130, 246, 0.2)', + gap: 10, + }, + infoBannerIconContainer: { + marginRight: 4, + }, + infoBannerIcon: { + width: 32, + height: 32, + borderRadius: 16, + justifyContent: 'center', + alignItems: 'center', }, infoBannerText: { flex: 1, - fontSize: 13, - color: '#1e40af', + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray700, lineHeight: 18, + fontWeight: theme.typography.fontWeight.medium, }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#f5f5f5', + backgroundColor: theme.colors.background, + }, + listContent: { + padding: 16, }, card: { - backgroundColor: '#fff', - padding: 16, - marginHorizontal: 16, - marginBottom: 12, - borderRadius: 12, - shadowColor: '#000', - shadowOpacity: 0.1, - shadowRadius: 4, - shadowOffset: { width: 0, height: 2 }, - elevation: 3, + padding: 18, + marginBottom: 14, + borderRadius: theme.borderRadius.xl, + borderWidth: 1, + borderColor: 'rgba(59, 130, 246, 0.1)', }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - marginBottom: 12, + marginBottom: 14, paddingBottom: 12, borderBottomWidth: 1, - borderBottomColor: '#e0e0e0', + borderBottomColor: theme.colors.gray200, }, - status: { - fontSize: 12, - fontWeight: '600', - color: '#2e7d32', - backgroundColor: '#e8f5e9', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, + statusBadge: { + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: theme.borderRadius.md, + }, + statusText: { + fontSize: theme.typography.fontSize.xs, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.white, }, date: { - fontSize: 12, - color: '#666', + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray600, + fontWeight: theme.typography.fontWeight.medium, }, sectionTitle: { - fontSize: 14, - fontWeight: '600', - color: '#1a1a1a', + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, marginTop: 12, marginBottom: 6, }, content: { - fontSize: 14, - color: '#333', + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray700, lineHeight: 20, }, emptyContainer: { @@ -214,16 +266,26 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingVertical: 60, }, + emptyIconContainer: { + marginBottom: 16, + }, + emptyIconGradient: { + width: 96, + height: 96, + borderRadius: 48, + justifyContent: 'center', + alignItems: 'center', + }, empty: { textAlign: 'center', - fontSize: 16, - color: '#666', - fontWeight: '500', + fontSize: theme.typography.fontSize.base, + color: theme.colors.gray700, + fontWeight: theme.typography.fontWeight.semibold, + marginBottom: 4, }, emptySub: { textAlign: 'center', - fontSize: 14, - color: '#999', - marginTop: 8, + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray500, }, }); diff --git a/apps/mobile/src/components/AnimatedButton.tsx b/apps/mobile/src/components/AnimatedButton.tsx new file mode 100644 index 0000000..2e6687f --- /dev/null +++ b/apps/mobile/src/components/AnimatedButton.tsx @@ -0,0 +1,136 @@ +/** + * Animated Button Component + * Modern button with gradient backgrounds and smooth animations + */ + +import React, { useRef } from 'react'; +import { + TouchableOpacity, + Text, + StyleSheet, + Animated, + ActivityIndicator, + ViewStyle, + TextStyle, +} from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import { theme } from '../styles/theme'; + +interface AnimatedButtonProps { + onPress: () => void; + title: string; + variant?: 'primary' | 'secondary' | 'danger' | 'success'; + loading?: boolean; + disabled?: boolean; + style?: ViewStyle; + textStyle?: TextStyle; + icon?: React.ReactNode; +} + +export function AnimatedButton({ + onPress, + title, + variant = 'primary', + loading = false, + disabled = false, + style, + textStyle, + icon, +}: AnimatedButtonProps) { + const scaleAnim = useRef(new Animated.Value(1)).current; + + const handlePressIn = () => { + Animated.spring(scaleAnim, { + toValue: 0.95, + friction: 8, + tension: 100, + useNativeDriver: true, + }).start(); + }; + + const handlePressOut = () => { + Animated.spring(scaleAnim, { + toValue: 1, + friction: 8, + tension: 100, + useNativeDriver: true, + }).start(); + }; + + const getGradientColors = (): readonly [string, string, ...string[]] => { + switch (variant) { + case 'primary': + return theme.gradients.primary; + case 'danger': + return theme.gradients.danger; + case 'success': + return theme.gradients.success; + case 'secondary': + return [theme.colors.gray600, theme.colors.gray700] as const; + default: + return theme.gradients.primary; + } + }; + + const getShadowStyle = () => { + if (disabled) return {}; + switch (variant) { + case 'danger': + return theme.shadows.glowDanger; + default: + return theme.shadows.glow; + } + }; + + return ( + + + + {loading ? ( + + ) : ( + <> + {icon} + {title} + + )} + + + + ); +} + +const styles = StyleSheet.create({ + button: { + paddingVertical: 16, + paddingHorizontal: 24, + borderRadius: theme.borderRadius.lg, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + text: { + color: theme.colors.white, + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.semibold, + }, + disabled: { + opacity: 0.5, + }, +}); diff --git a/apps/mobile/src/components/GoalProgressCard.tsx b/apps/mobile/src/components/GoalProgressCard.tsx index 19e671c..3990c1f 100644 --- a/apps/mobile/src/components/GoalProgressCard.tsx +++ b/apps/mobile/src/components/GoalProgressCard.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; import type { FitnessGoal } from '../services/fitnessGoals'; +import { theme } from '../styles/theme'; interface GoalProgressCardProps { goal: FitnessGoal; @@ -30,12 +32,12 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr } }; - const getPriorityColor = (priority: string) => { + const getPriorityGradient = (priority: string): readonly [string, string] => { switch (priority) { - case 'high': return '#ef4444'; - case 'medium': return '#f59e0b'; - case 'low': return '#10b981'; - default: return '#6b7280'; + case 'high': return theme.gradients.danger; + case 'medium': return theme.gradients.warning; + case 'low': return theme.gradients.success; + default: return theme.gradients.primary; } }; @@ -52,134 +54,175 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr return ( - - - - - - {goal.title} - - {goal.description && ( - - {goal.description} + + {/* Priority Accent Bar */} + + + + + + + + + + {goal.title} + {goal.description && ( + + {goal.description} + + )} + + + + + {!isCompleted && onComplete && ( + + + + )} + {onDelete && ( + + + )} - - {!isCompleted && onComplete && ( - - - - )} - {onDelete && ( - - - - )} - - + {goal.targetValue && ( + + + + {goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''} + + + {progress.toFixed(0)}% + + - {goal.targetValue && ( - - - - {goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''} + + + + + )} + + + + {goal.priority.toUpperCase()} + + + {daysRemaining !== null && !isCompleted && ( + + {daysRemaining < 0 + ? `${Math.abs(daysRemaining)} days overdue` + : `${daysRemaining} days remaining` + } - {progress.toFixed(0)}% - + )} - - - + {isCompleted && goal.completedDate && ( + + Completed {new Date(goal.completedDate).toLocaleDateString()} + + )} - )} - - - - {goal.priority.toUpperCase()} - - - {daysRemaining !== null && !isCompleted && ( - - {daysRemaining < 0 - ? `${Math.abs(daysRemaining)} days overdue` - : `${daysRemaining} days remaining` - } - - )} - - {isCompleted && goal.completedDate && ( - - Completed {new Date(goal.completedDate).toLocaleDateString()} - - )} - + ); } const styles = StyleSheet.create({ card: { - backgroundColor: '#fff', - borderRadius: 12, + borderRadius: theme.borderRadius.xl, padding: 16, marginBottom: 12, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 3, + borderWidth: 1, + borderColor: 'rgba(59, 130, 246, 0.1)', + overflow: 'hidden', }, cardCompleted: { - backgroundColor: '#f0fdf4', - borderColor: '#bbf7d0', - borderWidth: 1, + borderColor: 'rgba(16, 185, 129, 0.2)', + }, + priorityAccent: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + width: 4, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12, + marginLeft: 8, }, titleRow: { flexDirection: 'row', alignItems: 'flex-start', flex: 1, }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 20, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, titleContainer: { - marginLeft: 12, flex: 1, }, title: { - fontSize: 18, - fontWeight: '600', - color: '#111827', + fontSize: theme.typography.fontSize.lg, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray900, marginBottom: 4, }, titleCompleted: { - color: '#9ca3af', + color: theme.colors.gray600, textDecorationLine: 'line-through', }, description: { - fontSize: 14, - color: '#6b7280', - lineHeight: 20, + fontSize: theme.typography.fontSize.sm, + color: theme.colors.gray600, + lineHeight: 18, }, actions: { flexDirection: 'row', @@ -190,6 +233,7 @@ const styles = StyleSheet.create({ }, progressSection: { marginBottom: 12, + marginLeft: 8, }, progressInfo: { flexDirection: 'row', @@ -197,55 +241,53 @@ const styles = StyleSheet.create({ marginBottom: 8, }, progressText: { - fontSize: 14, - fontWeight: '500', - color: '#374151', + fontSize: theme.typography.fontSize.sm, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.gray700, }, progressPercentage: { - fontSize: 14, - fontWeight: '600', - color: '#2563eb', + fontSize: theme.typography.fontSize.sm, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.primary, }, progressBarContainer: { height: 8, - backgroundColor: '#e5e7eb', + backgroundColor: theme.colors.gray200, borderRadius: 4, overflow: 'hidden', }, progressBar: { height: '100%', - backgroundColor: '#2563eb', borderRadius: 4, }, - progressBarCompleted: { - backgroundColor: '#10b981', - }, footer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', + marginLeft: 8, }, priorityBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, + paddingHorizontal: 10, + paddingVertical: 5, + borderRadius: theme.borderRadius.md, }, priorityText: { - fontSize: 10, - fontWeight: '600', - color: '#fff', + fontSize: theme.typography.fontSize.xs, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.white, }, daysRemaining: { - fontSize: 12, - color: '#6b7280', + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray600, + fontWeight: theme.typography.fontWeight.medium, }, overdue: { - color: '#ef4444', - fontWeight: '600', + color: theme.colors.danger, + fontWeight: theme.typography.fontWeight.semibold, }, completedDate: { - fontSize: 12, - color: '#10b981', - fontWeight: '500', + fontSize: theme.typography.fontSize.xs, + color: theme.colors.success, + fontWeight: theme.typography.fontWeight.medium, }, }); diff --git a/apps/mobile/src/components/GradientBackground.tsx b/apps/mobile/src/components/GradientBackground.tsx new file mode 100644 index 0000000..c5a941b --- /dev/null +++ b/apps/mobile/src/components/GradientBackground.tsx @@ -0,0 +1,46 @@ +/** + * Gradient Background Component + * Reusable gradient background with multiple presets + */ + +import React from 'react'; +import { StyleSheet, ViewStyle } from 'react-native'; +import { LinearGradient } from 'expo-linear-gradient'; +import { theme } from '../styles/theme'; + +interface GradientBackgroundProps { + variant?: 'primary' | 'success' | 'warning' | 'danger' | 'purple' | 'ocean' | 'sunset' | 'dark'; + colors?: string[]; + style?: ViewStyle; + children?: React.ReactNode; + start?: { x: number; y: number }; + end?: { x: number; y: number }; +} + +export function GradientBackground({ + variant = 'primary', + colors, + style, + children, + start = { x: 0, y: 0 }, + end = { x: 1, y: 1 }, +}: GradientBackgroundProps) { + const gradientColors = (colors || theme.gradients[variant]) as readonly [string, string, ...string[]]; + + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + gradient: { + flex: 1, + }, +}); diff --git a/apps/mobile/src/styles/theme.ts b/apps/mobile/src/styles/theme.ts new file mode 100644 index 0000000..da73eee --- /dev/null +++ b/apps/mobile/src/styles/theme.ts @@ -0,0 +1,190 @@ +/** + * Modern Design System Theme + * Centralized theme configuration with gradients, colors, shadows, and spacing + */ + +export const theme = { + // Color Palette + colors: { + // Primary colors + primary: '#3b82f6', + primaryDark: '#2563eb', + primaryLight: '#60a5fa', + + // Accent colors + purple: '#8b5cf6', + purpleDark: '#7c3aed', + pink: '#ec4899', + + // Success + success: '#10b981', + successDark: '#059669', + successLight: '#34d399', + + // Warning + warning: '#f59e0b', + warningDark: '#d97706', + + // Danger + danger: '#ef4444', + dangerDark: '#dc2626', + + // 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', + }, + + // 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, + }, + + // 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, + }, + }, + + // 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, + }, + }, + + // Spacing Scale + spacing: { + xs: 4, + sm: 8, + md: 12, + lg: 16, + xl: 20, + '2xl': 24, + '3xl': 32, + '4xl': 40, + '5xl': 48, + }, + + // Border Radius + borderRadius: { + sm: 4, + md: 8, + lg: 12, + xl: 16, + '2xl': 20, + '3xl': 24, + full: 9999, + }, + + // Animation Timing + animation: { + duration: { + fast: 150, + normal: 250, + slow: 350, + }, + easing: { + easeIn: 'ease-in', + easeOut: 'ease-out', + easeInOut: 'ease-in-out', + }, + }, + + // 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)', + }, + }, +}; + +export type Theme = typeof theme; diff --git a/apps/mobile/src/utils/animations.ts b/apps/mobile/src/utils/animations.ts new file mode 100644 index 0000000..9be7d84 --- /dev/null +++ b/apps/mobile/src/utils/animations.ts @@ -0,0 +1,149 @@ +/** + * Reusable Animation Utilities + * Pre-configured animations for consistent motion design + */ + +import { Animated, Easing } from 'react-native'; + +export const animations = { + // Fade animations + fadeIn: (animatedValue: Animated.Value, duration = 250) => { + return Animated.timing(animatedValue, { + toValue: 1, + duration, + easing: Easing.out(Easing.ease), + useNativeDriver: true, + }); + }, + + fadeOut: (animatedValue: Animated.Value, duration = 250) => { + return Animated.timing(animatedValue, { + toValue: 0, + duration, + easing: Easing.in(Easing.ease), + useNativeDriver: true, + }); + }, + + // Scale animations + scaleIn: (animatedValue: Animated.Value, duration = 250) => { + return Animated.spring(animatedValue, { + toValue: 1, + friction: 8, + tension: 40, + useNativeDriver: true, + }); + }, + + scaleOut: (animatedValue: Animated.Value, duration = 200) => { + return Animated.timing(animatedValue, { + toValue: 0, + duration, + easing: Easing.in(Easing.ease), + useNativeDriver: true, + }); + }, + + // Press animation (scale down slightly) + pressIn: (animatedValue: Animated.Value) => { + return Animated.spring(animatedValue, { + toValue: 0.95, + friction: 8, + tension: 100, + useNativeDriver: true, + }); + }, + + pressOut: (animatedValue: Animated.Value) => { + return Animated.spring(animatedValue, { + toValue: 1, + friction: 8, + tension: 100, + useNativeDriver: true, + }); + }, + + // Slide animations + slideInUp: (animatedValue: Animated.Value, duration = 300) => { + return Animated.timing(animatedValue, { + toValue: 0, + duration, + easing: Easing.out(Easing.cubic), + useNativeDriver: true, + }); + }, + + slideOutDown: (animatedValue: Animated.Value, duration = 250) => { + return Animated.timing(animatedValue, { + toValue: 100, + duration, + easing: Easing.in(Easing.cubic), + useNativeDriver: true, + }); + }, + + // Pulse animation (for FAB or notifications) + pulse: (animatedValue: Animated.Value) => { + return Animated.sequence([ + Animated.timing(animatedValue, { + toValue: 1.1, + duration: 150, + easing: Easing.out(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(animatedValue, { + toValue: 1, + duration: 150, + easing: Easing.in(Easing.ease), + useNativeDriver: true, + }), + ]); + }, + + // Stagger animation for lists + stagger: (animations: Animated.CompositeAnimation[], delay = 50) => { + return Animated.stagger(delay, animations); + }, + + // Parallel animations + parallel: (animations: Animated.CompositeAnimation[]) => { + return Animated.parallel(animations); + }, + + // Sequence animations + sequence: (animations: Animated.CompositeAnimation[]) => { + return Animated.sequence(animations); + }, +}; + +// Spring configurations +export const springConfig = { + gentle: { + friction: 10, + tension: 40, + }, + bouncy: { + friction: 5, + tension: 40, + }, + stiff: { + friction: 8, + tension: 100, + }, +}; + +// Timing configurations +export const timingConfig = { + fast: { + duration: 150, + easing: Easing.out(Easing.ease), + }, + normal: { + duration: 250, + easing: Easing.out(Easing.ease), + }, + slow: { + duration: 350, + easing: Easing.out(Easing.ease), + }, +};