claude take :(

This commit is contained in:
echo 2025-11-26 01:57:33 +01:00
parent fc12cecd30
commit 73d2c4c1ed
13 changed files with 1460 additions and 517 deletions

Binary file not shown.

View File

@ -23,6 +23,7 @@
"expo-constants": "^18.0.10",
"expo-crypto": "^15.0.7",
"expo-font": "~14.0.9",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
@ -7134,6 +7135,17 @@
"react-native": "*"
}
},
"node_modules/expo-linear-gradient": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.7.tgz",
"integrity": "sha512-yF+y+9Shpr/OQFfy/wglB/0bykFMbwHBTuMRa5Of/r2P1wbkcacx8rg0JsUWkXH/rn2i2iWdubyqlxSJa3ggZA==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-linking": {
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.9.tgz",

View File

@ -29,6 +29,7 @@
"expo-constants": "^18.0.10",
"expo-crypto": "^15.0.7",
"expo-font": "~14.0.9",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",

View File

@ -1,15 +1,39 @@
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { useAuth } from '@clerk/clerk-expo'
import { LinearGradient } from 'expo-linear-gradient'
import { Ionicons } from '@expo/vector-icons'
import { attendanceApi, Attendance } from '../../api/attendance'
import { theme } from '../../styles/theme'
import { Animated } from 'react-native'
export default function AttendanceScreen() {
const { getToken, userId } = useAuth()
const [loading, setLoading] = useState(true)
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null)
const [history, setHistory] = useState<Attendance[]>([])
const pulseAnim = useRef(new Animated.Value(1)).current
useEffect(() => {
if (activeCheckIn) {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.05,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
])
)
pulse.start()
return () => pulse.stop()
}
}, [activeCheckIn])
const fetchAttendance = async () => {
try {
@ -70,42 +94,100 @@ export default function AttendanceScreen() {
if (loading && !history.length) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#000" />
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
)
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Attendance</Text>
<Text style={styles.subtitle}>Track your gym visits</Text>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<Text style={styles.title}>Attendance</Text>
<Text style={styles.subtitle}>Track your gym visits</Text>
</LinearGradient>
<View style={styles.actionContainer}>
{activeCheckIn ? (
<View style={styles.activeCard}>
<Text style={styles.activeText}>Currently Checked In</Text>
<Text style={styles.timeText}>
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Text>
<TouchableOpacity style={styles.checkOutButton} onPress={handleCheckOut}>
<Text style={styles.buttonText}>Check Out</Text>
<LinearGradient
colors={['rgba(16, 185, 129, 0.15)', 'rgba(5, 150, 105, 0.1)']}
style={[styles.activeCard, theme.shadows.medium]}
>
<View style={styles.activeCardContent}>
<View style={styles.activeIconContainer}>
<LinearGradient
colors={theme.gradients.success}
style={styles.activeIcon}
>
<Ionicons name="checkmark-circle" size={32} color="#fff" />
</LinearGradient>
</View>
<View style={styles.activeTextContainer}>
<Text style={styles.activeText}>Currently Checked In</Text>
<Text style={styles.timeText}>
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</Text>
</View>
</View>
<TouchableOpacity onPress={handleCheckOut} activeOpacity={0.8}>
<LinearGradient
colors={theme.gradients.danger}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.checkOutButton, theme.shadows.medium]}
>
<Ionicons name="log-out-outline" size={20} color="#fff" style={{ marginRight: 8 }} />
<Text style={styles.buttonText}>Check Out</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</LinearGradient>
) : (
<TouchableOpacity style={styles.checkInButton} onPress={handleCheckIn}>
<Text style={styles.buttonText}>Check In</Text>
</TouchableOpacity>
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<TouchableOpacity onPress={handleCheckIn} activeOpacity={0.8}>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.checkInButton, theme.shadows.glow]}
>
<Ionicons name="log-in-outline" size={24} color="#fff" style={{ marginRight: 8 }} />
<Text style={styles.checkInButtonText}>Check In</Text>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
)}
</View>
<Text style={styles.sectionTitle}>Recent History</Text>
{history.map((item) => (
<View key={item.id} style={styles.historyItem}>
<View>
<Text style={styles.dateText}>
{new Date(item.checkInTime).toLocaleDateString()}
</Text>
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
<LinearGradient
key={item.id}
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
style={[styles.historyItem, theme.shadows.medium]}
>
<View style={styles.historyLeft}>
<View style={styles.historyIconContainer}>
<LinearGradient
colors={item.checkOutTime ? theme.gradients.success : theme.gradients.primary}
style={styles.historyIcon}
>
<Ionicons
name={item.checkOutTime ? "checkmark" : "time-outline"}
size={16}
color="#fff"
/>
</LinearGradient>
</View>
<View>
<Text style={styles.dateText}>
{new Date(item.checkInTime).toLocaleDateString()}
</Text>
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
</View>
</View>
<View style={styles.timeContainer}>
<Text style={styles.historyTime}>
@ -117,7 +199,7 @@ export default function AttendanceScreen() {
</Text>
)}
</View>
</View>
</LinearGradient>
))}
</ScrollView>
)
@ -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,
},
})

View File

@ -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() {
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
<View style={styles.header}>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<View>
<Text style={styles.headerTitle}>My Fitness Goals</Text>
<Text style={styles.headerSubtitle}>
Track your fitness journey progress
</Text>
</View>
</View>
</LinearGradient>
{/* Stats Summary */}
{goals.length > 0 && (
@ -179,12 +188,37 @@ export default function GoalsScreen() {
</ScrollView>
{/* Floating Action Button */}
<TouchableOpacity
style={styles.fab}
onPress={() => setShowCreateModal(true)}
>
<Ionicons name="add" size={28} color="#fff" />
</TouchableOpacity>
<Animated.View style={{ transform: [{ scale: fabScale }] }}>
<TouchableOpacity
onPress={() => 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}
>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.fab}
>
<Ionicons name="add" size={28} color="#fff" />
</LinearGradient>
</TouchableOpacity>
</Animated.View>
{/* Create Goal Modal */}
<GoalCreationModal
@ -199,7 +233,7 @@ export default function GoalsScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f3f4f6",
backgroundColor: theme.colors.background,
},
center: {
flex: 1,
@ -207,18 +241,21 @@ const styles = StyleSheet.create({
alignItems: "center",
},
header: {
padding: 20,
backgroundColor: "#fff",
padding: 24,
paddingTop: 60,
paddingBottom: 24,
marginBottom: 10,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
headerTitle: {
fontSize: 28,
fontWeight: "bold",
color: "#111827",
fontSize: theme.typography.fontSize['3xl'],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
},
headerSubtitle: {
fontSize: 16,
color: "#6b7280",
fontSize: theme.typography.fontSize.base,
color: "rgba(255, 255, 255, 0.9)",
marginTop: 4,
},
statsContainer: {
@ -228,20 +265,18 @@ const styles = StyleSheet.create({
},
statCard: {
flex: 1,
backgroundColor: "#fff",
backgroundColor: theme.colors.white,
padding: 16,
borderRadius: 12,
borderRadius: theme.borderRadius.xl,
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
...theme.shadows.medium,
borderWidth: 1,
borderColor: "rgba(59, 130, 246, 0.1)",
},
statValue: {
fontSize: 24,
fontWeight: "bold",
color: "#2563eb",
fontSize: theme.typography.fontSize['2xl'],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.primary,
marginBottom: 4,
},
statLabel: {
@ -281,16 +316,11 @@ const styles = StyleSheet.create({
position: "absolute",
right: 20,
bottom: 20,
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: "#2563eb",
width: 64,
height: 64,
borderRadius: 32,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
...theme.shadows.glow,
},
});

View File

@ -1,18 +1,30 @@
import React from "react";
import React, { useEffect, useRef } from "react";
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Animated,
} from "react-native";
import { useUser } from "@clerk/clerk-expo";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import { LinearGradient } from "expo-linear-gradient";
import { theme } from "../../styles/theme";
export default function HomeScreen() {
const { user, isLoaded } = useUser();
const router = useRouter();
const fadeAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
}, []);
if (!isLoaded || !user) {
return (
@ -27,32 +39,67 @@ export default function HomeScreen() {
return (
<ScrollView style={styles.container}>
<View style={styles.content}>
{/* Welcome Header */}
<View style={styles.header}>
<Animated.View style={[styles.content, { opacity: fadeAnim }]}>
{/* Gradient Header */}
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<Text style={styles.greeting}>{greeting}!</Text>
<Text style={styles.name}>{firstName}</Text>
</View>
</LinearGradient>
{/* Quick Stats */}
{/* Quick Stats with Glassmorphism */}
<View style={styles.statsContainer}>
<View style={styles.statCard}>
<Ionicons name="calendar-outline" size={32} color="#2563eb" />
<LinearGradient
colors={['rgba(59, 130, 246, 0.1)', 'rgba(139, 92, 246, 0.1)']}
style={[styles.statCard, theme.shadows.medium]}
>
<View style={styles.statIconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.statIconGradient}
>
<Ionicons name="calendar-outline" size={24} color="#fff" />
</LinearGradient>
</View>
<Text style={styles.statValue}>0</Text>
<Text style={styles.statLabel}>This Month</Text>
</View>
</LinearGradient>
<View style={styles.statCard}>
<Ionicons name="flame-outline" size={32} color="#ef4444" />
<LinearGradient
colors={['rgba(239, 68, 68, 0.1)', 'rgba(236, 72, 153, 0.1)']}
style={[styles.statCard, theme.shadows.medium]}
>
<View style={styles.statIconContainer}>
<LinearGradient
colors={theme.gradients.danger}
style={styles.statIconGradient}
>
<Ionicons name="flame-outline" size={24} color="#fff" />
</LinearGradient>
</View>
<Text style={styles.statValue}>0</Text>
<Text style={styles.statLabel}>Day Streak</Text>
</View>
</LinearGradient>
<View style={styles.statCard}>
<Ionicons name="trophy-outline" size={32} color="#f59e0b" />
<LinearGradient
colors={['rgba(245, 158, 11, 0.1)', 'rgba(217, 119, 6, 0.1)']}
style={[styles.statCard, theme.shadows.medium]}
>
<View style={styles.statIconContainer}>
<LinearGradient
colors={theme.gradients.warning}
style={styles.statIconGradient}
>
<Ionicons name="trophy-outline" size={24} color="#fff" />
</LinearGradient>
</View>
<Text style={styles.statValue}>0</Text>
<Text style={styles.statLabel}>Total Visits</Text>
</View>
</LinearGradient>
</View>
{/* Quick Actions */}
@ -60,12 +107,16 @@ export default function HomeScreen() {
<Text style={styles.sectionTitle}>Quick Actions</Text>
<TouchableOpacity
style={styles.actionButton}
style={[styles.actionButton, theme.shadows.medium]}
onPress={() => router.push("/fitness-profile")}
activeOpacity={0.7}
>
<View style={styles.actionIcon}>
<LinearGradient
colors={['rgba(236, 72, 153, 0.15)', 'rgba(236, 72, 153, 0.05)']}
style={styles.actionIcon}
>
<Ionicons name="fitness-outline" size={24} color="#ec4899" />
</View>
</LinearGradient>
<View style={styles.actionContent}>
<Text style={styles.actionTitle}>Fitness Profile</Text>
<Text style={styles.actionSubtitle}>
@ -75,12 +126,16 @@ export default function HomeScreen() {
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</TouchableOpacity>
<View style={styles.actionButton}>
<View style={styles.actionIcon}>
<TouchableOpacity
style={[styles.actionButton, theme.shadows.medium]}
activeOpacity={0.7}
>
<LinearGradient
colors={['rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.05)']}
style={styles.actionIcon}
>
<Ionicons name="log-in-outline" size={24} color="#2563eb" />
</View>
</LinearGradient>
<View style={styles.actionContent}>
<Text style={styles.actionTitle}>Check In</Text>
<Text style={styles.actionSubtitle}>
@ -88,13 +143,18 @@ export default function HomeScreen() {
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View>
</TouchableOpacity>
<View style={styles.actionButton}>
<View style={styles.actionIcon}>
<TouchableOpacity
style={[styles.actionButton, theme.shadows.medium]}
activeOpacity={0.7}
>
<LinearGradient
colors={['rgba(16, 185, 129, 0.15)', 'rgba(16, 185, 129, 0.05)']}
style={styles.actionIcon}
>
<Ionicons name="calendar-outline" size={24} color="#10b981" />
</View>
</LinearGradient>
<View style={styles.actionContent}>
<Text style={styles.actionTitle}>View Schedule</Text>
<Text style={styles.actionSubtitle}>
@ -102,24 +162,35 @@ export default function HomeScreen() {
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View>
</TouchableOpacity>
<View style={styles.actionButton}>
<View style={styles.actionIcon}>
<TouchableOpacity
style={[styles.actionButton, theme.shadows.medium]}
activeOpacity={0.7}
>
<LinearGradient
colors={['rgba(139, 92, 246, 0.15)', 'rgba(139, 92, 246, 0.05)']}
style={styles.actionIcon}
>
<Ionicons name="card-outline" size={24} color="#8b5cf6" />
</View>
</LinearGradient>
<View style={styles.actionContent}>
<Text style={styles.actionTitle}>Payments</Text>
<Text style={styles.actionSubtitle}>View payment history</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View>
</TouchableOpacity>
</View>
{/* Membership Info */}
{/* Membership Info with Gradient */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Membership</Text>
<View style={styles.membershipCard}>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.membershipCard, theme.shadows.strong]}
>
<View style={styles.membershipHeader}>
<Text style={styles.membershipType}>Basic Plan</Text>
<View style={styles.statusBadge}>
@ -132,21 +203,28 @@ export default function HomeScreen() {
<Text style={styles.membershipDate}>
Member since {new Date(user.createdAt!).toLocaleDateString()}
</Text>
</View>
</LinearGradient>
</View>
{/* Recent Activity */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Recent Activity</Text>
<View style={styles.emptyState}>
<Ionicons name="barbell-outline" size={48} color="#d1d5db" />
<View style={[styles.emptyState, theme.shadows.medium]}>
<View style={styles.emptyIconContainer}>
<LinearGradient
colors={['rgba(209, 213, 219, 0.3)', 'rgba(209, 213, 219, 0.1)']}
style={styles.emptyIconGradient}
>
<Ionicons name="barbell-outline" size={48} color="#9ca3af" />
</LinearGradient>
</View>
<Text style={styles.emptyStateText}>No recent activity</Text>
<Text style={styles.emptyStateSubtext}>
Check in to start tracking your workouts
</Text>
</View>
</View>
</View>
</Animated.View>
</ScrollView>
);
}
@ -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",
},
});

View File

@ -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 (
<ScrollView style={styles.container}>
<View style={styles.content}>
{/* Profile Header */}
<View style={styles.profileCard}>
{/* Profile Header with Gradient */}
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.profileCard, theme.shadows.strong]}
>
<View style={styles.avatarContainer}>
{user.imageUrl ? (
<View style={styles.avatar}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.1)']}
style={styles.avatar}
>
<Text style={styles.avatarText}>
{user.firstName?.charAt(0)}
{user.lastName?.charAt(0)}
</Text>
</View>
</LinearGradient>
) : (
<View style={styles.avatar}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.3)', 'rgba(255, 255, 255, 0.1)']}
style={styles.avatar}
>
<Ionicons name="person" size={40} color="#fff" />
</View>
</LinearGradient>
)}
</View>
@ -74,58 +87,80 @@ export default function ProfileScreen() {
{user.primaryPhoneNumber.phoneNumber}
</Text>
)}
</View>
</LinearGradient>
{/* Account Information */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Account Information</Text>
<View style={styles.infoRow}>
<View style={styles.infoLabel}>
<Ionicons name="mail-outline" size={20} color="#666" />
<Text style={styles.infoLabelText}>Email</Text>
</View>
<Text style={styles.infoValue}>
{user.primaryEmailAddress?.emailAddress}
</Text>
</View>
{user.primaryPhoneNumber && (
<View style={[styles.infoCard, theme.shadows.medium]}>
<View style={styles.infoRow}>
<View style={styles.infoLabel}>
<Ionicons name="call-outline" size={20} color="#666" />
<Text style={styles.infoLabelText}>Phone</Text>
<LinearGradient
colors={['rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.05)']}
style={styles.infoIconContainer}
>
<Ionicons name="mail-outline" size={18} color={theme.colors.primary} />
</LinearGradient>
<Text style={styles.infoLabelText}>Email</Text>
</View>
<Text style={styles.infoValue}>
{user.primaryPhoneNumber.phoneNumber}
{user.primaryEmailAddress?.emailAddress}
</Text>
</View>
)}
<View style={styles.infoRow}>
<View style={styles.infoLabel}>
<Ionicons name="calendar-outline" size={20} color="#666" />
<Text style={styles.infoLabelText}>Member Since</Text>
</View>
<Text style={styles.infoValue}>
{new Date(user.createdAt!).toLocaleDateString()}
</Text>
</View>
{user.primaryPhoneNumber && (
<View style={styles.infoRow}>
<View style={styles.infoLabel}>
<LinearGradient
colors={['rgba(16, 185, 129, 0.15)', 'rgba(16, 185, 129, 0.05)']}
style={styles.infoIconContainer}
>
<Ionicons name="call-outline" size={18} color={theme.colors.success} />
</LinearGradient>
<Text style={styles.infoLabelText}>Phone</Text>
</View>
<Text style={styles.infoValue}>
{user.primaryPhoneNumber.phoneNumber}
</Text>
</View>
)}
<View style={styles.infoRow}>
<View style={styles.infoLabel}>
<Ionicons
name="shield-checkmark-outline"
size={20}
color="#666"
/>
<Text style={styles.infoLabelText}>Email Verified</Text>
<View style={styles.infoRow}>
<View style={styles.infoLabel}>
<LinearGradient
colors={['rgba(245, 158, 11, 0.15)', 'rgba(245, 158, 11, 0.05)']}
style={styles.infoIconContainer}
>
<Ionicons name="calendar-outline" size={18} color={theme.colors.warning} />
</LinearGradient>
<Text style={styles.infoLabelText}>Member Since</Text>
</View>
<Text style={styles.infoValue}>
{new Date(user.createdAt!).toLocaleDateString()}
</Text>
</View>
<View style={[styles.infoRow, { borderBottomWidth: 0 }]}>
<View style={styles.infoLabel}>
<LinearGradient
colors={['rgba(16, 185, 129, 0.15)', 'rgba(16, 185, 129, 0.05)']}
style={styles.infoIconContainer}
>
<Ionicons
name="shield-checkmark-outline"
size={18}
color={theme.colors.success}
/>
</LinearGradient>
<Text style={styles.infoLabelText}>Email Verified</Text>
</View>
<Text style={styles.infoValue}>
{user.primaryEmailAddress?.verification?.status === "verified"
? "Yes"
: "No"}
</Text>
</View>
<Text style={styles.infoValue}>
{user.primaryEmailAddress?.verification?.status === "verified"
? "Yes"
: "No"}
</Text>
</View>
</View>
@ -133,35 +168,62 @@ export default function ProfileScreen() {
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Actions</Text>
<TouchableOpacity style={styles.actionButton}>
<Ionicons name="person-outline" size={24} color="#2563eb" />
<TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<LinearGradient
colors={['rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.05)']}
style={styles.actionIconContainer}
>
<Ionicons name="person-outline" size={22} color={theme.colors.primary} />
</LinearGradient>
<Text style={styles.actionButtonText}>Edit Profile</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Ionicons name="notifications-outline" size={24} color="#2563eb" />
<TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<LinearGradient
colors={['rgba(245, 158, 11, 0.15)', 'rgba(245, 158, 11, 0.05)']}
style={styles.actionIconContainer}
>
<Ionicons name="notifications-outline" size={22} color={theme.colors.warning} />
</LinearGradient>
<Text style={styles.actionButtonText}>Notifications</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Ionicons name="card-outline" size={24} color="#2563eb" />
<TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<LinearGradient
colors={['rgba(139, 92, 246, 0.15)', 'rgba(139, 92, 246, 0.05)']}
style={styles.actionIconContainer}
>
<Ionicons name="card-outline" size={22} color={theme.colors.purple} />
</LinearGradient>
<Text style={styles.actionButtonText}>Payment History</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton}>
<Ionicons name="settings-outline" size={24} color="#2563eb" />
<TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<LinearGradient
colors={['rgba(107, 114, 128, 0.15)', 'rgba(107, 114, 128, 0.05)']}
style={styles.actionIconContainer}
>
<Ionicons name="settings-outline" size={22} color={theme.colors.gray600} />
</LinearGradient>
<Text style={styles.actionButtonText}>Settings</Text>
<Ionicons name="chevron-forward" size={20} color="#999" />
<Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
</TouchableOpacity>
</View>
{/* Sign Out Button */}
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Ionicons name="log-out-outline" size={20} color="#fff" />
<Text style={styles.logoutText}>Sign Out</Text>
<TouchableOpacity onPress={handleLogout} activeOpacity={0.8}>
<LinearGradient
colors={theme.gradients.danger}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[styles.logoutButton, theme.shadows.glowDanger]}
>
<Ionicons name="log-out-outline" size={20} color="#fff" />
<Text style={styles.logoutText}>Sign Out</Text>
</LinearGradient>
</TouchableOpacity>
{/* 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,
},

View File

@ -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 (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#000" />
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.header}>AI Recommendations</Text>
<LinearGradient
colors={theme.gradients.primary}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.header}
>
<Text style={styles.headerTitle}>AI Recommendations</Text>
</LinearGradient>
{/* AI Context Info Banner */}
<View style={styles.infoBanner}>
<Ionicons name="sparkles" size={20} color="#2563eb" />
{/* AI Context Info Banner with Glassmorphism */}
<LinearGradient
colors={['rgba(59, 130, 246, 0.15)', 'rgba(139, 92, 246, 0.1)'] as const}
style={styles.infoBanner}
>
<View style={styles.infoBannerIconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.infoBannerIcon}
>
<Ionicons name="sparkles" size={16} color="#fff" />
</LinearGradient>
</View>
<Text style={styles.infoBannerText}>
Personalized based on your active fitness goals and progress
</Text>
</View>
</LinearGradient>
<FlatList
data={recommendations}
keyExtractor={(item) => item.id}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<View style={styles.emptyIconContainer}>
<LinearGradient
colors={['rgba(209, 213, 219, 0.3)', 'rgba(209, 213, 219, 0.1)'] as const}
style={styles.emptyIconGradient}
>
<Ionicons name="sparkles-outline" size={48} color="#9ca3af" />
</LinearGradient>
</View>
<Text style={styles.empty}>No recommendations available yet.</Text>
<Text style={styles.emptySub}>Pull down to refresh</Text>
</View>
}
renderItem={({ item }) => (
<View style={styles.card}>
<LinearGradient
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.cardHeader}>
<Text style={styles.status}>{item.status.toUpperCase()}</Text>
<LinearGradient
colors={theme.gradients.success}
style={styles.statusBadge}
>
<Text style={styles.statusText}>{item.status.toUpperCase()}</Text>
</LinearGradient>
<Text style={styles.date}>{new Date(item.createdAt).toLocaleDateString()}</Text>
</View>
@ -120,7 +156,7 @@ export default function RecommendationsScreen() {
<Text style={styles.content}>{item.dietPlan}</Text>
</>
)}
</View>
</LinearGradient>
)}
/>
</View>
@ -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,
},
});

View File

@ -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 (
<Animated.View style={[{ transform: [{ scale: scaleAnim }] }, style]}>
<TouchableOpacity
onPress={onPress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={disabled || loading}
activeOpacity={0.9}
>
<LinearGradient
colors={getGradientColors()}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[
styles.button,
getShadowStyle(),
disabled && styles.disabled,
]}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<>
{icon}
<Text style={[styles.text, textStyle]}>{title}</Text>
</>
)}
</LinearGradient>
</TouchableOpacity>
</Animated.View>
);
}
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,
},
});

View File

@ -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 (
<TouchableOpacity
style={[styles.card, isCompleted && styles.cardCompleted]}
onPress={onPress}
activeOpacity={0.7}
>
<View style={styles.header}>
<View style={styles.titleRow}>
<Ionicons
name={getGoalTypeIcon(goal.goalType) as any}
size={24}
color={isCompleted ? '#9ca3af' : '#2563eb'}
/>
<View style={styles.titleContainer}>
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
{goal.title}
</Text>
{goal.description && (
<Text style={styles.description} numberOfLines={2}>
{goal.description}
<LinearGradient
colors={isCompleted
? ['rgba(16, 185, 129, 0.05)', 'rgba(5, 150, 105, 0.02)'] as const
: ['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const
}
style={[
styles.card,
theme.shadows.medium,
isCompleted && styles.cardCompleted
]}
>
{/* Priority Accent Bar */}
<LinearGradient
colors={getPriorityGradient(goal.priority)}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={styles.priorityAccent}
/>
<View style={styles.header}>
<View style={styles.titleRow}>
<LinearGradient
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
style={styles.iconContainer}
>
<Ionicons
name={getGoalTypeIcon(goal.goalType) as any}
size={20}
color="#fff"
/>
</LinearGradient>
<View style={styles.titleContainer}>
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
{goal.title}
</Text>
{goal.description && (
<Text style={styles.description} numberOfLines={2}>
{goal.description}
</Text>
)}
</View>
</View>
<View style={styles.actions}>
{!isCompleted && onComplete && (
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
<Ionicons name="checkmark-circle-outline" size={24} color={theme.colors.success} />
</TouchableOpacity>
)}
{onDelete && (
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
<Ionicons name="trash-outline" size={22} color={theme.colors.danger} />
</TouchableOpacity>
)}
</View>
</View>
<View style={styles.actions}>
{!isCompleted && onComplete && (
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" />
</TouchableOpacity>
)}
{onDelete && (
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
<Ionicons name="trash-outline" size={22} color="#ef4444" />
</TouchableOpacity>
)}
</View>
</View>
{goal.targetValue && (
<View style={styles.progressSection}>
<View style={styles.progressInfo}>
<Text style={styles.progressText}>
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
</Text>
<Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}>
{progress.toFixed(0)}%
</Text>
</View>
{goal.targetValue && (
<View style={styles.progressSection}>
<View style={styles.progressInfo}>
<Text style={styles.progressText}>
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
<View style={styles.progressBarContainer}>
<LinearGradient
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[
styles.progressBar,
{ width: `${Math.min(progress, 100)}%` }
]}
/>
</View>
</View>
)}
<View style={styles.footer}>
<LinearGradient
colors={getPriorityGradient(goal.priority)}
style={styles.priorityBadge}
>
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
</LinearGradient>
{daysRemaining !== null && !isCompleted && (
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
{daysRemaining < 0
? `${Math.abs(daysRemaining)} days overdue`
: `${daysRemaining} days remaining`
}
</Text>
<Text style={styles.progressPercentage}>{progress.toFixed(0)}%</Text>
</View>
)}
<View style={styles.progressBarContainer}>
<View
style={[
styles.progressBar,
{ width: `${Math.min(progress, 100)}%` },
isCompleted && styles.progressBarCompleted
]}
/>
</View>
{isCompleted && goal.completedDate && (
<Text style={styles.completedDate}>
Completed {new Date(goal.completedDate).toLocaleDateString()}
</Text>
)}
</View>
)}
<View style={styles.footer}>
<View style={[styles.priorityBadge, { backgroundColor: getPriorityColor(goal.priority) }]}>
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
</View>
{daysRemaining !== null && !isCompleted && (
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
{daysRemaining < 0
? `${Math.abs(daysRemaining)} days overdue`
: `${daysRemaining} days remaining`
}
</Text>
)}
{isCompleted && goal.completedDate && (
<Text style={styles.completedDate}>
Completed {new Date(goal.completedDate).toLocaleDateString()}
</Text>
)}
</View>
</LinearGradient>
</TouchableOpacity>
);
}
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,
},
});

View File

@ -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 (
<LinearGradient
colors={gradientColors}
start={start}
end={end}
style={[styles.gradient, style]}
>
{children}
</LinearGradient>
);
}
const styles = StyleSheet.create({
gradient: {
flex: 1,
},
});

View File

@ -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;

View File

@ -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),
},
};