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-constants": "^18.0.10",
"expo-crypto": "^15.0.7", "expo-crypto": "^15.0.7",
"expo-font": "~14.0.9", "expo-font": "~14.0.9",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0", "expo-linking": "~8.0.0",
"expo-notifications": "~0.32.0", "expo-notifications": "~0.32.0",
"expo-router": "~6.0.14", "expo-router": "~6.0.14",
@ -7134,6 +7135,17 @@
"react-native": "*" "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": { "node_modules/expo-linking": {
"version": "8.0.9", "version": "8.0.9",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.9.tgz", "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-constants": "^18.0.10",
"expo-crypto": "^15.0.7", "expo-crypto": "^15.0.7",
"expo-font": "~14.0.9", "expo-font": "~14.0.9",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0", "expo-linking": "~8.0.0",
"expo-notifications": "~0.32.0", "expo-notifications": "~0.32.0",
"expo-router": "~6.0.14", "expo-router": "~6.0.14",

View File

@ -1,15 +1,39 @@
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native' 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 { useAuth } from '@clerk/clerk-expo'
import { LinearGradient } from 'expo-linear-gradient'
import { Ionicons } from '@expo/vector-icons'
import { attendanceApi, Attendance } from '../../api/attendance' import { attendanceApi, Attendance } from '../../api/attendance'
import { theme } from '../../styles/theme'
import { Animated } from 'react-native'
export default function AttendanceScreen() { export default function AttendanceScreen() {
const { getToken, userId } = useAuth() const { getToken, userId } = useAuth()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null) const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null)
const [history, setHistory] = useState<Attendance[]>([]) 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 () => { const fetchAttendance = async () => {
try { try {
@ -70,42 +94,100 @@ export default function AttendanceScreen() {
if (loading && !history.length) { if (loading && !history.length) {
return ( return (
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator size="large" color="#000" /> <ActivityIndicator size="large" color={theme.colors.primary} />
</View> </View>
) )
} }
return ( return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Attendance</Text> <LinearGradient
<Text style={styles.subtitle}>Track your gym visits</Text> 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}> <View style={styles.actionContainer}>
{activeCheckIn ? ( {activeCheckIn ? (
<View style={styles.activeCard}> <LinearGradient
<Text style={styles.activeText}>Currently Checked In</Text> colors={['rgba(16, 185, 129, 0.15)', 'rgba(5, 150, 105, 0.1)']}
<Text style={styles.timeText}> style={[styles.activeCard, theme.shadows.medium]}
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} >
</Text> <View style={styles.activeCardContent}>
<TouchableOpacity style={styles.checkOutButton} onPress={handleCheckOut}> <View style={styles.activeIconContainer}>
<Text style={styles.buttonText}>Check Out</Text> <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> </TouchableOpacity>
</View> </LinearGradient>
) : ( ) : (
<TouchableOpacity style={styles.checkInButton} onPress={handleCheckIn}> <Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<Text style={styles.buttonText}>Check In</Text> <TouchableOpacity onPress={handleCheckIn} activeOpacity={0.8}>
</TouchableOpacity> <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> </View>
<Text style={styles.sectionTitle}>Recent History</Text> <Text style={styles.sectionTitle}>Recent History</Text>
{history.map((item) => ( {history.map((item) => (
<View key={item.id} style={styles.historyItem}> <LinearGradient
<View> key={item.id}
<Text style={styles.dateText}> colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
{new Date(item.checkInTime).toLocaleDateString()} style={[styles.historyItem, theme.shadows.medium]}
</Text> >
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text> <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>
<View style={styles.timeContainer}> <View style={styles.timeContainer}>
<Text style={styles.historyTime}> <Text style={styles.historyTime}>
@ -117,7 +199,7 @@ export default function AttendanceScreen() {
</Text> </Text>
)} )}
</View> </View>
</View> </LinearGradient>
))} ))}
</ScrollView> </ScrollView>
) )
@ -126,105 +208,147 @@ export default function AttendanceScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f5f5f5', backgroundColor: theme.colors.background,
}, },
content: { content: {
padding: 20, paddingBottom: 20,
}, },
centered: { centered: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
header: {
paddingTop: 60,
paddingBottom: 24,
paddingHorizontal: 24,
marginBottom: 24,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
},
title: { title: {
fontSize: 28, fontSize: theme.typography.fontSize['3xl'],
fontWeight: 'bold', fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
marginBottom: 4, marginBottom: 4,
color: '#1a1a1a',
}, },
subtitle: { subtitle: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
color: '#666', color: 'rgba(255, 255, 255, 0.9)',
marginBottom: 24,
}, },
actionContainer: { actionContainer: {
marginBottom: 32, marginBottom: 32,
paddingHorizontal: 20,
}, },
checkInButton: { checkInButton: {
backgroundColor: '#000', paddingVertical: 20,
paddingVertical: 16, paddingHorizontal: 24,
borderRadius: 12, borderRadius: theme.borderRadius.xl,
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
shadowColor: '#000', justifyContent: 'center',
shadowOffset: { width: 0, height: 2 }, },
shadowOpacity: 0.1, checkInButtonText: {
shadowRadius: 4, color: theme.colors.white,
elevation: 3, fontSize: theme.typography.fontSize.xl,
fontWeight: theme.typography.fontWeight.semibold,
}, },
checkOutButton: { checkOutButton: {
backgroundColor: '#ff3b30', paddingVertical: 14,
paddingVertical: 16, paddingHorizontal: 20,
borderRadius: 12, borderRadius: theme.borderRadius.lg,
flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
marginTop: 16, marginTop: 16,
}, },
buttonText: { buttonText: {
color: '#fff', color: theme.colors.white,
fontSize: 18, fontSize: theme.typography.fontSize.base,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
}, },
activeCard: { activeCard: {
backgroundColor: '#fff',
padding: 20, padding: 20,
borderRadius: 16, borderRadius: theme.borderRadius.xl,
borderWidth: 1, 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: { activeText: {
fontSize: 18, fontSize: theme.typography.fontSize.lg,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
color: '#2e7d32', color: theme.colors.gray900,
marginBottom: 4, marginBottom: 4,
}, },
timeText: { timeText: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: '#666', color: theme.colors.gray600,
}, },
sectionTitle: { sectionTitle: {
fontSize: 20, fontSize: theme.typography.fontSize.xl,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
marginBottom: 16, marginBottom: 16,
color: '#1a1a1a', paddingHorizontal: 20,
color: theme.colors.gray900,
}, },
historyItem: { historyItem: {
backgroundColor: '#fff',
padding: 16, padding: 16,
borderRadius: 12, borderRadius: theme.borderRadius.xl,
marginBottom: 12, marginBottom: 12,
marginHorizontal: 20,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
shadowColor: '#000', borderWidth: 1,
shadowOffset: { width: 0, height: 1 }, borderColor: 'rgba(59, 130, 246, 0.1)',
shadowOpacity: 0.05, },
shadowRadius: 2, historyLeft: {
elevation: 2, flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
historyIconContainer: {
marginRight: 4,
},
historyIcon: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
}, },
dateText: { dateText: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
fontWeight: '500', fontWeight: theme.typography.fontWeight.semibold,
color: '#1a1a1a', color: theme.colors.gray900,
}, },
typeText: { typeText: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: '#666', color: theme.colors.gray600,
marginTop: 2, marginTop: 2,
}, },
timeContainer: { timeContainer: {
alignItems: 'flex-end', alignItems: 'flex-end',
}, },
historyTime: { historyTime: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: '#444', color: theme.colors.gray700,
}, },
}) })

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { import {
View, View,
Text, Text,
@ -8,12 +8,15 @@ import {
ActivityIndicator, ActivityIndicator,
RefreshControl, RefreshControl,
Alert, Alert,
Animated,
} from "react-native"; } from "react-native";
import { useAuth } from "@clerk/clerk-expo"; import { useAuth } from "@clerk/clerk-expo";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from "../../services/fitnessGoals"; import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from "../../services/fitnessGoals";
import { GoalProgressCard } from "../../components/GoalProgressCard"; import { GoalProgressCard } from "../../components/GoalProgressCard";
import { GoalCreationModal } from "../../components/GoalCreationModal"; import { GoalCreationModal } from "../../components/GoalCreationModal";
import { theme } from "../../styles/theme";
export default function GoalsScreen() { export default function GoalsScreen() {
const { userId, getToken } = useAuth(); const { userId, getToken } = useAuth();
@ -21,6 +24,7 @@ export default function GoalsScreen() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const fabScale = useRef(new Animated.Value(1)).current;
const fetchGoals = async () => { const fetchGoals = async () => {
if (!userId) return; if (!userId) return;
@ -100,14 +104,19 @@ export default function GoalsScreen() {
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> <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> <View>
<Text style={styles.headerTitle}>My Fitness Goals</Text> <Text style={styles.headerTitle}>My Fitness Goals</Text>
<Text style={styles.headerSubtitle}> <Text style={styles.headerSubtitle}>
Track your fitness journey progress Track your fitness journey progress
</Text> </Text>
</View> </View>
</View> </LinearGradient>
{/* Stats Summary */} {/* Stats Summary */}
{goals.length > 0 && ( {goals.length > 0 && (
@ -179,12 +188,37 @@ export default function GoalsScreen() {
</ScrollView> </ScrollView>
{/* Floating Action Button */} {/* Floating Action Button */}
<TouchableOpacity <Animated.View style={{ transform: [{ scale: fabScale }] }}>
style={styles.fab} <TouchableOpacity
onPress={() => setShowCreateModal(true)} onPress={() => setShowCreateModal(true)}
> onPressIn={() => {
<Ionicons name="add" size={28} color="#fff" /> Animated.spring(fabScale, {
</TouchableOpacity> 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 */} {/* Create Goal Modal */}
<GoalCreationModal <GoalCreationModal
@ -199,7 +233,7 @@ export default function GoalsScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#f3f4f6", backgroundColor: theme.colors.background,
}, },
center: { center: {
flex: 1, flex: 1,
@ -207,18 +241,21 @@ const styles = StyleSheet.create({
alignItems: "center", alignItems: "center",
}, },
header: { header: {
padding: 20, padding: 24,
backgroundColor: "#fff", paddingTop: 60,
paddingBottom: 24,
marginBottom: 10, marginBottom: 10,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
}, },
headerTitle: { headerTitle: {
fontSize: 28, fontSize: theme.typography.fontSize['3xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#111827", color: theme.colors.white,
}, },
headerSubtitle: { headerSubtitle: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
color: "#6b7280", color: "rgba(255, 255, 255, 0.9)",
marginTop: 4, marginTop: 4,
}, },
statsContainer: { statsContainer: {
@ -228,20 +265,18 @@ const styles = StyleSheet.create({
}, },
statCard: { statCard: {
flex: 1, flex: 1,
backgroundColor: "#fff", backgroundColor: theme.colors.white,
padding: 16, padding: 16,
borderRadius: 12, borderRadius: theme.borderRadius.xl,
alignItems: "center", alignItems: "center",
shadowColor: "#000", ...theme.shadows.medium,
shadowOffset: { width: 0, height: 1 }, borderWidth: 1,
shadowOpacity: 0.05, borderColor: "rgba(59, 130, 246, 0.1)",
shadowRadius: 2,
elevation: 2,
}, },
statValue: { statValue: {
fontSize: 24, fontSize: theme.typography.fontSize['2xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#2563eb", color: theme.colors.primary,
marginBottom: 4, marginBottom: 4,
}, },
statLabel: { statLabel: {
@ -281,16 +316,11 @@ const styles = StyleSheet.create({
position: "absolute", position: "absolute",
right: 20, right: 20,
bottom: 20, bottom: 20,
width: 56, width: 64,
height: 56, height: 64,
borderRadius: 28, borderRadius: 32,
backgroundColor: "#2563eb",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
shadowColor: "#000", ...theme.shadows.glow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}, },
}); });

View File

@ -1,18 +1,30 @@
import React from "react"; import React, { useEffect, useRef } from "react";
import { import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
ScrollView, ScrollView,
TouchableOpacity, TouchableOpacity,
Animated,
} from "react-native"; } from "react-native";
import { useUser } from "@clerk/clerk-expo"; import { useUser } from "@clerk/clerk-expo";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { LinearGradient } from "expo-linear-gradient";
import { theme } from "../../styles/theme";
export default function HomeScreen() { export default function HomeScreen() {
const { user, isLoaded } = useUser(); const { user, isLoaded } = useUser();
const router = useRouter(); 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) { if (!isLoaded || !user) {
return ( return (
@ -27,32 +39,67 @@ export default function HomeScreen() {
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.container}>
<View style={styles.content}> <Animated.View style={[styles.content, { opacity: fadeAnim }]}>
{/* Welcome Header */} {/* Gradient Header */}
<View style={styles.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.greeting}>{greeting}!</Text>
<Text style={styles.name}>{firstName}</Text> <Text style={styles.name}>{firstName}</Text>
</View> </LinearGradient>
{/* Quick Stats */} {/* Quick Stats with Glassmorphism */}
<View style={styles.statsContainer}> <View style={styles.statsContainer}>
<View style={styles.statCard}> <LinearGradient
<Ionicons name="calendar-outline" size={32} color="#2563eb" /> 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.statValue}>0</Text>
<Text style={styles.statLabel}>This Month</Text> <Text style={styles.statLabel}>This Month</Text>
</View> </LinearGradient>
<View style={styles.statCard}> <LinearGradient
<Ionicons name="flame-outline" size={32} color="#ef4444" /> 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.statValue}>0</Text>
<Text style={styles.statLabel}>Day Streak</Text> <Text style={styles.statLabel}>Day Streak</Text>
</View> </LinearGradient>
<View style={styles.statCard}> <LinearGradient
<Ionicons name="trophy-outline" size={32} color="#f59e0b" /> 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.statValue}>0</Text>
<Text style={styles.statLabel}>Total Visits</Text> <Text style={styles.statLabel}>Total Visits</Text>
</View> </LinearGradient>
</View> </View>
{/* Quick Actions */} {/* Quick Actions */}
@ -60,12 +107,16 @@ export default function HomeScreen() {
<Text style={styles.sectionTitle}>Quick Actions</Text> <Text style={styles.sectionTitle}>Quick Actions</Text>
<TouchableOpacity <TouchableOpacity
style={styles.actionButton} style={[styles.actionButton, theme.shadows.medium]}
onPress={() => router.push("/fitness-profile")} 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" /> <Ionicons name="fitness-outline" size={24} color="#ec4899" />
</View> </LinearGradient>
<View style={styles.actionContent}> <View style={styles.actionContent}>
<Text style={styles.actionTitle}>Fitness Profile</Text> <Text style={styles.actionTitle}>Fitness Profile</Text>
<Text style={styles.actionSubtitle}> <Text style={styles.actionSubtitle}>
@ -75,12 +126,16 @@ export default function HomeScreen() {
<Ionicons name="chevron-forward" size={20} color="#9ca3af" /> <Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, theme.shadows.medium]}
<View style={styles.actionButton}> activeOpacity={0.7}
<View style={styles.actionIcon}> >
<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" /> <Ionicons name="log-in-outline" size={24} color="#2563eb" />
</View> </LinearGradient>
<View style={styles.actionContent}> <View style={styles.actionContent}>
<Text style={styles.actionTitle}>Check In</Text> <Text style={styles.actionTitle}>Check In</Text>
<Text style={styles.actionSubtitle}> <Text style={styles.actionSubtitle}>
@ -88,13 +143,18 @@ export default function HomeScreen() {
</Text> </Text>
</View> </View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" /> <Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View> </TouchableOpacity>
<TouchableOpacity
<View style={styles.actionButton}> style={[styles.actionButton, theme.shadows.medium]}
<View style={styles.actionIcon}> 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" /> <Ionicons name="calendar-outline" size={24} color="#10b981" />
</View> </LinearGradient>
<View style={styles.actionContent}> <View style={styles.actionContent}>
<Text style={styles.actionTitle}>View Schedule</Text> <Text style={styles.actionTitle}>View Schedule</Text>
<Text style={styles.actionSubtitle}> <Text style={styles.actionSubtitle}>
@ -102,24 +162,35 @@ export default function HomeScreen() {
</Text> </Text>
</View> </View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" /> <Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View> </TouchableOpacity>
<View style={styles.actionButton}> <TouchableOpacity
<View style={styles.actionIcon}> 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" /> <Ionicons name="card-outline" size={24} color="#8b5cf6" />
</View> </LinearGradient>
<View style={styles.actionContent}> <View style={styles.actionContent}>
<Text style={styles.actionTitle}>Payments</Text> <Text style={styles.actionTitle}>Payments</Text>
<Text style={styles.actionSubtitle}>View payment history</Text> <Text style={styles.actionSubtitle}>View payment history</Text>
</View> </View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" /> <Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View> </TouchableOpacity>
</View> </View>
{/* Membership Info */} {/* Membership Info with Gradient */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Membership</Text> <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}> <View style={styles.membershipHeader}>
<Text style={styles.membershipType}>Basic Plan</Text> <Text style={styles.membershipType}>Basic Plan</Text>
<View style={styles.statusBadge}> <View style={styles.statusBadge}>
@ -132,21 +203,28 @@ export default function HomeScreen() {
<Text style={styles.membershipDate}> <Text style={styles.membershipDate}>
Member since {new Date(user.createdAt!).toLocaleDateString()} Member since {new Date(user.createdAt!).toLocaleDateString()}
</Text> </Text>
</View> </LinearGradient>
</View> </View>
{/* Recent Activity */} {/* Recent Activity */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Recent Activity</Text> <Text style={styles.sectionTitle}>Recent Activity</Text>
<View style={styles.emptyState}> <View style={[styles.emptyState, theme.shadows.medium]}>
<Ionicons name="barbell-outline" size={48} color="#d1d5db" /> <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.emptyStateText}>No recent activity</Text>
<Text style={styles.emptyStateSubtext}> <Text style={styles.emptyStateSubtext}>
Check in to start tracking your workouts Check in to start tracking your workouts
</Text> </Text>
</View> </View>
</View> </View>
</View> </Animated.View>
</ScrollView> </ScrollView>
); );
} }
@ -161,81 +239,90 @@ function getGreeting(): string {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#f5f5f5", backgroundColor: theme.colors.background,
}, },
content: { content: {
padding: 20, paddingBottom: 20,
}, },
header: { header: {
marginBottom: 24, padding: 24,
paddingTop: 60,
paddingBottom: 32,
borderBottomLeftRadius: theme.borderRadius['2xl'],
borderBottomRightRadius: theme.borderRadius['2xl'],
marginBottom: 20,
}, },
greeting: { greeting: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
color: "#6b7280", color: "rgba(255, 255, 255, 0.9)",
marginBottom: 4, marginBottom: 4,
}, },
name: { name: {
fontSize: 32, fontSize: theme.typography.fontSize['4xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#1a1a1a", color: theme.colors.white,
}, },
statsContainer: { statsContainer: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
paddingHorizontal: 16,
marginBottom: 24, marginBottom: 24,
gap: 12,
}, },
statCard: { statCard: {
flex: 1, flex: 1,
backgroundColor: "white", backgroundColor: theme.colors.white,
borderRadius: 12, borderRadius: theme.borderRadius.xl,
padding: 16, padding: 16,
alignItems: "center", alignItems: "center",
marginHorizontal: 4, borderWidth: 1,
shadowColor: "#000", borderColor: "rgba(255, 255, 255, 0.3)",
shadowOffset: { width: 0, height: 1 }, },
shadowOpacity: 0.05, statIconContainer: {
shadowRadius: 2, marginBottom: 8,
elevation: 2, },
statIconGradient: {
width: 48,
height: 48,
borderRadius: 24,
justifyContent: "center",
alignItems: "center",
}, },
statValue: { statValue: {
fontSize: 24, fontSize: theme.typography.fontSize['2xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#1a1a1a", color: theme.colors.gray900,
marginTop: 8, marginTop: 4,
marginBottom: 4, marginBottom: 4,
}, },
statLabel: { statLabel: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: "#6b7280", color: theme.colors.gray600,
textAlign: "center", textAlign: "center",
fontWeight: theme.typography.fontWeight.medium,
}, },
section: { section: {
marginBottom: 24, marginBottom: 24,
paddingHorizontal: 20,
}, },
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: theme.typography.fontSize.xl,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
color: "#1a1a1a", color: theme.colors.gray900,
marginBottom: 12, marginBottom: 12,
}, },
actionButton: { actionButton: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
backgroundColor: "white", backgroundColor: theme.colors.white,
borderRadius: 12, borderRadius: theme.borderRadius.xl,
padding: 16, padding: 16,
marginBottom: 8, marginBottom: 12,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
}, },
actionIcon: { actionIcon: {
width: 48, width: 56,
height: 48, height: 56,
borderRadius: 24, borderRadius: 28,
backgroundColor: "#f0f9ff",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
marginRight: 12, marginRight: 12,
@ -244,77 +331,77 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
actionTitle: { actionTitle: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
color: "#1a1a1a", color: theme.colors.gray900,
marginBottom: 2, marginBottom: 2,
}, },
actionSubtitle: { actionSubtitle: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: "#6b7280", color: theme.colors.gray600,
}, },
membershipCard: { membershipCard: {
backgroundColor: "white", borderRadius: theme.borderRadius.xl,
borderRadius: 12, padding: 20,
padding: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
}, },
membershipHeader: { membershipHeader: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: 8, marginBottom: 12,
}, },
membershipType: { membershipType: {
fontSize: 18, fontSize: theme.typography.fontSize.xl,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
color: "#1a1a1a", color: theme.colors.white,
}, },
statusBadge: { statusBadge: {
backgroundColor: "#dcfce7", backgroundColor: "rgba(255, 255, 255, 0.25)",
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 4, paddingVertical: 6,
borderRadius: 12, borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.3)",
}, },
statusText: { statusText: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
color: "#16a34a", color: theme.colors.white,
}, },
membershipEmail: { membershipEmail: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: "#6b7280", color: "rgba(255, 255, 255, 0.9)",
marginBottom: 4, marginBottom: 4,
}, },
membershipDate: { membershipDate: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: "#9ca3af", color: "rgba(255, 255, 255, 0.7)",
}, },
emptyState: { emptyState: {
backgroundColor: "white", backgroundColor: theme.colors.white,
borderRadius: 12, borderRadius: theme.borderRadius.xl,
padding: 32, padding: 32,
alignItems: "center", alignItems: "center",
shadowColor: "#000", },
shadowOffset: { width: 0, height: 1 }, emptyIconContainer: {
shadowOpacity: 0.05, marginBottom: 16,
shadowRadius: 2, },
elevation: 2, emptyIconGradient: {
width: 96,
height: 96,
borderRadius: 48,
justifyContent: "center",
alignItems: "center",
}, },
emptyStateText: { emptyStateText: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
color: "#6b7280", color: theme.colors.gray700,
marginTop: 12,
marginBottom: 4, marginBottom: 4,
}, },
emptyStateSubtext: { emptyStateSubtext: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: "#9ca3af", color: theme.colors.gray500,
textAlign: "center", textAlign: "center",
}, },
}); });

View File

@ -10,6 +10,8 @@ import {
import { useUser, useAuth } from "@clerk/clerk-expo"; import { useUser, useAuth } from "@clerk/clerk-expo";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { theme } from "../../styles/theme";
export default function ProfileScreen() { export default function ProfileScreen() {
const { user } = useUser(); const { user } = useUser();
@ -45,20 +47,31 @@ export default function ProfileScreen() {
return ( return (
<ScrollView style={styles.container}> <ScrollView style={styles.container}>
<View style={styles.content}> <View style={styles.content}>
{/* Profile Header */} {/* Profile Header with Gradient */}
<View style={styles.profileCard}> <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}> <View style={styles.avatarContainer}>
{user.imageUrl ? ( {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}> <Text style={styles.avatarText}>
{user.firstName?.charAt(0)} {user.firstName?.charAt(0)}
{user.lastName?.charAt(0)} {user.lastName?.charAt(0)}
</Text> </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" /> <Ionicons name="person" size={40} color="#fff" />
</View> </LinearGradient>
)} )}
</View> </View>
@ -74,58 +87,80 @@ export default function ProfileScreen() {
{user.primaryPhoneNumber.phoneNumber} {user.primaryPhoneNumber.phoneNumber}
</Text> </Text>
)} )}
</View> </LinearGradient>
{/* Account Information */} {/* Account Information */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Account Information</Text> <Text style={styles.sectionTitle}>Account Information</Text>
<View style={styles.infoRow}> <View style={[styles.infoCard, theme.shadows.medium]}>
<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.infoRow}> <View style={styles.infoRow}>
<View style={styles.infoLabel}> <View style={styles.infoLabel}>
<Ionicons name="call-outline" size={20} color="#666" /> <LinearGradient
<Text style={styles.infoLabelText}>Phone</Text> 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> </View>
<Text style={styles.infoValue}> <Text style={styles.infoValue}>
{user.primaryPhoneNumber.phoneNumber} {user.primaryEmailAddress?.emailAddress}
</Text> </Text>
</View> </View>
)}
<View style={styles.infoRow}> {user.primaryPhoneNumber && (
<View style={styles.infoLabel}> <View style={styles.infoRow}>
<Ionicons name="calendar-outline" size={20} color="#666" /> <View style={styles.infoLabel}>
<Text style={styles.infoLabelText}>Member Since</Text> <LinearGradient
</View> colors={['rgba(16, 185, 129, 0.15)', 'rgba(16, 185, 129, 0.05)']}
<Text style={styles.infoValue}> style={styles.infoIconContainer}
{new Date(user.createdAt!).toLocaleDateString()} >
</Text> <Ionicons name="call-outline" size={18} color={theme.colors.success} />
</View> </LinearGradient>
<Text style={styles.infoLabelText}>Phone</Text>
</View>
<Text style={styles.infoValue}>
{user.primaryPhoneNumber.phoneNumber}
</Text>
</View>
)}
<View style={styles.infoRow}> <View style={styles.infoRow}>
<View style={styles.infoLabel}> <View style={styles.infoLabel}>
<Ionicons <LinearGradient
name="shield-checkmark-outline" colors={['rgba(245, 158, 11, 0.15)', 'rgba(245, 158, 11, 0.05)']}
size={20} style={styles.infoIconContainer}
color="#666" >
/> <Ionicons name="calendar-outline" size={18} color={theme.colors.warning} />
<Text style={styles.infoLabelText}>Email Verified</Text> </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> </View>
<Text style={styles.infoValue}>
{user.primaryEmailAddress?.verification?.status === "verified"
? "Yes"
: "No"}
</Text>
</View> </View>
</View> </View>
@ -133,35 +168,62 @@ export default function ProfileScreen() {
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Actions</Text> <Text style={styles.sectionTitle}>Quick Actions</Text>
<TouchableOpacity style={styles.actionButton}> <TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<Ionicons name="person-outline" size={24} color="#2563eb" /> <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> <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>
<TouchableOpacity style={styles.actionButton}> <TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<Ionicons name="notifications-outline" size={24} color="#2563eb" /> <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> <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>
<TouchableOpacity style={styles.actionButton}> <TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<Ionicons name="card-outline" size={24} color="#2563eb" /> <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> <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>
<TouchableOpacity style={styles.actionButton}> <TouchableOpacity style={[styles.actionButton, theme.shadows.medium]} activeOpacity={0.7}>
<Ionicons name="settings-outline" size={24} color="#2563eb" /> <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> <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> </TouchableOpacity>
</View> </View>
{/* Sign Out Button */} {/* Sign Out Button */}
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}> <TouchableOpacity onPress={handleLogout} activeOpacity={0.8}>
<Ionicons name="log-out-outline" size={20} color="#fff" /> <LinearGradient
<Text style={styles.logoutText}>Sign Out</Text> 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> </TouchableOpacity>
{/* App Version */} {/* App Version */}
@ -174,133 +236,135 @@ export default function ProfileScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#f5f5f5", backgroundColor: theme.colors.background,
}, },
content: { content: {
padding: 20, padding: 20,
paddingTop: 60,
}, },
profileCard: { profileCard: {
backgroundColor: "white", borderRadius: theme.borderRadius.xl,
borderRadius: 16, padding: 28,
padding: 24,
alignItems: "center", alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
marginBottom: 24, marginBottom: 24,
}, },
avatarContainer: { avatarContainer: {
marginBottom: 16, marginBottom: 16,
}, },
avatar: { avatar: {
width: 80, width: 96,
height: 80, height: 96,
borderRadius: 40, borderRadius: 48,
backgroundColor: "#2563eb",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
borderWidth: 3,
borderColor: "rgba(255, 255, 255, 0.3)",
}, },
avatarText: { avatarText: {
fontSize: 32, fontSize: theme.typography.fontSize['4xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#fff", color: theme.colors.white,
}, },
name: { name: {
fontSize: 24, fontSize: theme.typography.fontSize['2xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#1a1a1a", color: theme.colors.white,
marginBottom: 4, marginBottom: 6,
}, },
email: { email: {
fontSize: 16, fontSize: theme.typography.fontSize.base,
color: "#666", color: "rgba(255, 255, 255, 0.9)",
marginBottom: 4, marginBottom: 4,
}, },
phone: { phone: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: "#999", color: "rgba(255, 255, 255, 0.8)",
}, },
section: { section: {
backgroundColor: "white", marginBottom: 24,
borderRadius: 16,
padding: 20,
marginBottom: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
}, },
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: theme.typography.fontSize.xl,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
color: "#1a1a1a", color: theme.colors.gray900,
marginBottom: 16, marginBottom: 12,
},
infoCard: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
padding: 16,
}, },
infoRow: { infoRow: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
paddingVertical: 12, paddingVertical: 14,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: "#f0f0f0", borderBottomColor: theme.colors.gray200,
}, },
infoLabel: { infoLabel: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 8, gap: 10,
},
infoIconContainer: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: "center",
alignItems: "center",
}, },
infoLabelText: { infoLabelText: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: "#666", color: theme.colors.gray700,
fontWeight: "500", fontWeight: theme.typography.fontWeight.medium,
}, },
infoValue: { infoValue: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: "#1a1a1a", color: theme.colors.gray900,
fontWeight: "500", fontWeight: theme.typography.fontWeight.semibold,
}, },
actionButton: { actionButton: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
paddingVertical: 16, backgroundColor: theme.colors.white,
borderBottomWidth: 1, borderRadius: theme.borderRadius.xl,
borderBottomColor: "#f0f0f0", padding: 16,
marginBottom: 12,
},
actionIconContainer: {
width: 44,
height: 44,
borderRadius: 22,
justifyContent: "center",
alignItems: "center",
marginRight: 12,
}, },
actionButtonText: { actionButtonText: {
flex: 1, flex: 1,
fontSize: 16, fontSize: theme.typography.fontSize.base,
color: "#1a1a1a", color: theme.colors.gray900,
marginLeft: 12, fontWeight: theme.typography.fontWeight.medium,
fontWeight: "500",
}, },
logoutButton: { logoutButton: {
backgroundColor: "#ef4444",
paddingVertical: 16, paddingVertical: 16,
borderRadius: 12, borderRadius: theme.borderRadius.lg,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexDirection: "row", flexDirection: "row",
gap: 8, gap: 8,
marginTop: 8, marginTop: 8,
marginBottom: 16, marginBottom: 16,
shadowColor: "#ef4444",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
}, },
logoutText: { logoutText: {
color: "white", color: theme.colors.white,
fontSize: 16, fontSize: theme.typography.fontSize.base,
fontWeight: "600", fontWeight: theme.typography.fontWeight.semibold,
}, },
version: { version: {
textAlign: "center", textAlign: "center",
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: "#999", color: theme.colors.gray500,
marginTop: 8, marginTop: 8,
marginBottom: 20, marginBottom: 20,
}, },

View File

@ -2,7 +2,9 @@ import { useEffect, useState } from "react";
import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native"; import { View, Text, FlatList, ActivityIndicator, StyleSheet, RefreshControl } from "react-native";
import { useAuth } from "@clerk/clerk-expo"; import { useAuth } from "@clerk/clerk-expo";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { API_BASE_URL, API_ENDPOINTS } from "../../config/api"; import { API_BASE_URL, API_ENDPOINTS } from "../../config/api";
import { theme } from "../../styles/theme";
interface Recommendation { interface Recommendation {
id: string; id: string;
@ -70,37 +72,71 @@ export default function RecommendationsScreen() {
if (loading) { if (loading) {
return ( return (
<View style={styles.centered}> <View style={styles.centered}>
<ActivityIndicator size="large" color="#000" /> <ActivityIndicator size="large" color={theme.colors.primary} />
</View> </View>
); );
} }
return ( return (
<View style={styles.container}> <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 */} {/* AI Context Info Banner with Glassmorphism */}
<View style={styles.infoBanner}> <LinearGradient
<Ionicons name="sparkles" size={20} color="#2563eb" /> 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}> <Text style={styles.infoBannerText}>
Personalized based on your active fitness goals and progress Personalized based on your active fitness goals and progress
</Text> </Text>
</View> </LinearGradient>
<FlatList <FlatList
data={recommendations} data={recommendations}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
contentContainerStyle={styles.listContent}
ListEmptyComponent={ ListEmptyComponent={
<View style={styles.emptyContainer}> <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.empty}>No recommendations available yet.</Text>
<Text style={styles.emptySub}>Pull down to refresh</Text> <Text style={styles.emptySub}>Pull down to refresh</Text>
</View> </View>
} }
renderItem={({ item }) => ( 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}> <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> <Text style={styles.date}>{new Date(item.createdAt).toLocaleDateString()}</Text>
</View> </View>
@ -120,7 +156,7 @@ export default function RecommendationsScreen() {
<Text style={styles.content}>{item.dietPlan}</Text> <Text style={styles.content}>{item.dietPlan}</Text>
</> </>
)} )}
</View> </LinearGradient>
)} )}
/> />
</View> </View>
@ -130,83 +166,99 @@ export default function RecommendationsScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f5f5f5', backgroundColor: theme.colors.background,
}, },
header: { header: {
fontSize: 28, paddingTop: 60,
fontWeight: 'bold', paddingBottom: 24,
padding: 20, paddingHorizontal: 24,
paddingBottom: 12, borderBottomLeftRadius: theme.borderRadius.xl,
color: '#1a1a1a', borderBottomRightRadius: theme.borderRadius.xl,
},
headerTitle: {
fontSize: theme.typography.fontSize['3xl'],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
}, },
infoBanner: { infoBanner: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#eff6ff',
marginHorizontal: 16, marginHorizontal: 16,
marginTop: 16,
marginBottom: 12, marginBottom: 12,
padding: 12, padding: 14,
borderRadius: 8, borderRadius: theme.borderRadius.lg,
borderLeftWidth: 3, borderWidth: 1,
borderLeftColor: '#2563eb', borderColor: 'rgba(59, 130, 246, 0.2)',
gap: 8, gap: 10,
},
infoBannerIconContainer: {
marginRight: 4,
},
infoBannerIcon: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
}, },
infoBannerText: { infoBannerText: {
flex: 1, flex: 1,
fontSize: 13, fontSize: theme.typography.fontSize.sm,
color: '#1e40af', color: theme.colors.gray700,
lineHeight: 18, lineHeight: 18,
fontWeight: theme.typography.fontWeight.medium,
}, },
centered: { centered: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#f5f5f5', backgroundColor: theme.colors.background,
},
listContent: {
padding: 16,
}, },
card: { card: {
backgroundColor: '#fff', padding: 18,
padding: 16, marginBottom: 14,
marginHorizontal: 16, borderRadius: theme.borderRadius.xl,
marginBottom: 12, borderWidth: 1,
borderRadius: 12, borderColor: 'rgba(59, 130, 246, 0.1)',
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
elevation: 3,
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: 12, marginBottom: 14,
paddingBottom: 12, paddingBottom: 12,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#e0e0e0', borderBottomColor: theme.colors.gray200,
}, },
status: { statusBadge: {
fontSize: 12, paddingHorizontal: 10,
fontWeight: '600', paddingVertical: 5,
color: '#2e7d32', borderRadius: theme.borderRadius.md,
backgroundColor: '#e8f5e9', },
paddingHorizontal: 8, statusText: {
paddingVertical: 4, fontSize: theme.typography.fontSize.xs,
borderRadius: 4, fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.white,
}, },
date: { date: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: '#666', color: theme.colors.gray600,
fontWeight: theme.typography.fontWeight.medium,
}, },
sectionTitle: { sectionTitle: {
fontSize: 14, fontSize: theme.typography.fontSize.base,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
color: '#1a1a1a', color: theme.colors.gray900,
marginTop: 12, marginTop: 12,
marginBottom: 6, marginBottom: 6,
}, },
content: { content: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: '#333', color: theme.colors.gray700,
lineHeight: 20, lineHeight: 20,
}, },
emptyContainer: { emptyContainer: {
@ -214,16 +266,26 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 60, paddingVertical: 60,
}, },
emptyIconContainer: {
marginBottom: 16,
},
emptyIconGradient: {
width: 96,
height: 96,
borderRadius: 48,
justifyContent: 'center',
alignItems: 'center',
},
empty: { empty: {
textAlign: 'center', textAlign: 'center',
fontSize: 16, fontSize: theme.typography.fontSize.base,
color: '#666', color: theme.colors.gray700,
fontWeight: '500', fontWeight: theme.typography.fontWeight.semibold,
marginBottom: 4,
}, },
emptySub: { emptySub: {
textAlign: 'center', textAlign: 'center',
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: '#999', color: theme.colors.gray500,
marginTop: 8,
}, },
}); });

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 React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import type { FitnessGoal } from '../services/fitnessGoals'; import type { FitnessGoal } from '../services/fitnessGoals';
import { theme } from '../styles/theme';
interface GoalProgressCardProps { interface GoalProgressCardProps {
goal: FitnessGoal; 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) { switch (priority) {
case 'high': return '#ef4444'; case 'high': return theme.gradients.danger;
case 'medium': return '#f59e0b'; case 'medium': return theme.gradients.warning;
case 'low': return '#10b981'; case 'low': return theme.gradients.success;
default: return '#6b7280'; default: return theme.gradients.primary;
} }
}; };
@ -52,134 +54,175 @@ export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalPr
return ( return (
<TouchableOpacity <TouchableOpacity
style={[styles.card, isCompleted && styles.cardCompleted]}
onPress={onPress} onPress={onPress}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={styles.header}> <LinearGradient
<View style={styles.titleRow}> colors={isCompleted
<Ionicons ? ['rgba(16, 185, 129, 0.05)', 'rgba(5, 150, 105, 0.02)'] as const
name={getGoalTypeIcon(goal.goalType) as any} : ['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const
size={24} }
color={isCompleted ? '#9ca3af' : '#2563eb'} style={[
/> styles.card,
<View style={styles.titleContainer}> theme.shadows.medium,
<Text style={[styles.title, isCompleted && styles.titleCompleted]}> isCompleted && styles.cardCompleted
{goal.title} ]}
</Text> >
{goal.description && ( {/* Priority Accent Bar */}
<Text style={styles.description} numberOfLines={2}> <LinearGradient
{goal.description} 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> </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> </View>
<View style={styles.actions}> {goal.targetValue && (
{!isCompleted && onComplete && ( <View style={styles.progressSection}>
<TouchableOpacity onPress={onComplete} style={styles.actionButton}> <View style={styles.progressInfo}>
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" /> <Text style={styles.progressText}>
</TouchableOpacity> {goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
)} </Text>
{onDelete && ( <Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}>
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}> {progress.toFixed(0)}%
<Ionicons name="trash-outline" size={22} color="#ef4444" /> </Text>
</TouchableOpacity> </View>
)}
</View>
</View>
{goal.targetValue && ( <View style={styles.progressBarContainer}>
<View style={styles.progressSection}> <LinearGradient
<View style={styles.progressInfo}> colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
<Text style={styles.progressText}> start={{ x: 0, y: 0 }}
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''} 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>
<Text style={styles.progressPercentage}>{progress.toFixed(0)}%</Text> )}
</View>
<View style={styles.progressBarContainer}> {isCompleted && goal.completedDate && (
<View <Text style={styles.completedDate}>
style={[ Completed {new Date(goal.completedDate).toLocaleDateString()}
styles.progressBar, </Text>
{ width: `${Math.min(progress, 100)}%` }, )}
isCompleted && styles.progressBarCompleted
]}
/>
</View>
</View> </View>
)} </LinearGradient>
<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>
</TouchableOpacity> </TouchableOpacity>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
backgroundColor: '#fff', borderRadius: theme.borderRadius.xl,
borderRadius: 12,
padding: 16, padding: 16,
marginBottom: 12, marginBottom: 12,
shadowColor: '#000', borderWidth: 1,
shadowOffset: { width: 0, height: 2 }, borderColor: 'rgba(59, 130, 246, 0.1)',
shadowOpacity: 0.1, overflow: 'hidden',
shadowRadius: 4,
elevation: 3,
}, },
cardCompleted: { cardCompleted: {
backgroundColor: '#f0fdf4', borderColor: 'rgba(16, 185, 129, 0.2)',
borderColor: '#bbf7d0', },
borderWidth: 1, priorityAccent: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 4,
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'flex-start', alignItems: 'flex-start',
marginBottom: 12, marginBottom: 12,
marginLeft: 8,
}, },
titleRow: { titleRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-start', alignItems: 'flex-start',
flex: 1, flex: 1,
}, },
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
titleContainer: { titleContainer: {
marginLeft: 12,
flex: 1, flex: 1,
}, },
title: { title: {
fontSize: 18, fontSize: theme.typography.fontSize.lg,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
color: '#111827', color: theme.colors.gray900,
marginBottom: 4, marginBottom: 4,
}, },
titleCompleted: { titleCompleted: {
color: '#9ca3af', color: theme.colors.gray600,
textDecorationLine: 'line-through', textDecorationLine: 'line-through',
}, },
description: { description: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
color: '#6b7280', color: theme.colors.gray600,
lineHeight: 20, lineHeight: 18,
}, },
actions: { actions: {
flexDirection: 'row', flexDirection: 'row',
@ -190,6 +233,7 @@ const styles = StyleSheet.create({
}, },
progressSection: { progressSection: {
marginBottom: 12, marginBottom: 12,
marginLeft: 8,
}, },
progressInfo: { progressInfo: {
flexDirection: 'row', flexDirection: 'row',
@ -197,55 +241,53 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
}, },
progressText: { progressText: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
fontWeight: '500', fontWeight: theme.typography.fontWeight.medium,
color: '#374151', color: theme.colors.gray700,
}, },
progressPercentage: { progressPercentage: {
fontSize: 14, fontSize: theme.typography.fontSize.sm,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
color: '#2563eb', color: theme.colors.primary,
}, },
progressBarContainer: { progressBarContainer: {
height: 8, height: 8,
backgroundColor: '#e5e7eb', backgroundColor: theme.colors.gray200,
borderRadius: 4, borderRadius: 4,
overflow: 'hidden', overflow: 'hidden',
}, },
progressBar: { progressBar: {
height: '100%', height: '100%',
backgroundColor: '#2563eb',
borderRadius: 4, borderRadius: 4,
}, },
progressBarCompleted: {
backgroundColor: '#10b981',
},
footer: { footer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginLeft: 8,
}, },
priorityBadge: { priorityBadge: {
paddingHorizontal: 8, paddingHorizontal: 10,
paddingVertical: 4, paddingVertical: 5,
borderRadius: 4, borderRadius: theme.borderRadius.md,
}, },
priorityText: { priorityText: {
fontSize: 10, fontSize: theme.typography.fontSize.xs,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
color: '#fff', color: theme.colors.white,
}, },
daysRemaining: { daysRemaining: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: '#6b7280', color: theme.colors.gray600,
fontWeight: theme.typography.fontWeight.medium,
}, },
overdue: { overdue: {
color: '#ef4444', color: theme.colors.danger,
fontWeight: '600', fontWeight: theme.typography.fontWeight.semibold,
}, },
completedDate: { completedDate: {
fontSize: 12, fontSize: theme.typography.fontSize.xs,
color: '#10b981', color: theme.colors.success,
fontWeight: '500', 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),
},
};