Compare commits

...

2 Commits

Author SHA1 Message Date
febcdc111e redesign completed
alot of work ahead
2025-11-26 02:31:46 +01:00
73d2c4c1ed claude take :( 2025-11-26 01:57:33 +01:00
23 changed files with 2599 additions and 910 deletions

Binary file not shown.

View File

@ -23,6 +23,8 @@
"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-haptics": "^15.0.7",
"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",
@ -35,6 +37,7 @@
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "^15.15.0",
"react-native-web": "^0.21.2", "react-native-web": "^0.21.2",
"zod": "^3.22.0" "zod": "^3.22.0"
}, },
@ -5595,6 +5598,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@ -6267,6 +6276,56 @@
"hyphenate-style-name": "^1.0.3" "hyphenate-style-name": "^1.0.3"
} }
}, },
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -6492,6 +6551,61 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -6579,6 +6693,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-editor": { "node_modules/env-editor": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -7134,6 +7260,26 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/expo-haptics": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-15.0.7.tgz",
"integrity": "sha512-7flWsYPrwjJxZ8x82RiJtzsnk1Xp9ahnbd9PhCy3NnsemyMApoWIEUr4waPqFr80DtiLZfhD9VMLL1CKa8AImQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"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",
@ -9866,6 +10012,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -10463,6 +10615,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -11484,6 +11648,21 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-svg": {
"version": "15.15.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.0.tgz",
"integrity": "sha512-/Wx6F/IZ88B/GcF88bK8K7ZseJDYt+7WGaiggyzLvTowChQ8BM5idmcd4pK+6QJP6a6DmzL2sfOMukFUn/NArg==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-url-polyfill": { "node_modules/react-native-url-polyfill": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz", "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",

View File

@ -29,6 +29,8 @@
"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-haptics": "^15.0.7",
"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",
@ -41,6 +43,7 @@
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "^15.15.0",
"react-native-web": "^0.21.2", "react-native-web": "^0.21.2",
"zod": "^3.22.0" "zod": "^3.22.0"
}, },

View File

@ -1,7 +1,7 @@
import { Tabs, useRouter, useSegments } from "expo-router"; import { Tabs, useRouter, useSegments } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { useAuth } from "@clerk/clerk-expo"; import { useAuth } from "@clerk/clerk-expo";
import { useEffect } from "react"; import { useEffect } from "react";
import { CustomTabBar } from "../../components/CustomTabBar";
export default function TabLayout() { export default function TabLayout() {
const { isSignedIn, isLoaded } = useAuth(); const { isSignedIn, isLoaded } = useAuth();
@ -25,56 +25,40 @@ export default function TabLayout() {
return ( return (
<Tabs <Tabs
tabBar={(props) => <CustomTabBar {...props} />}
screenOptions={{ screenOptions={{
tabBarActiveTintColor: "#2563eb", headerShown: false, // We'll use custom headers in screens or layout
tabBarInactiveTintColor: "#6b7280", tabBarShowLabel: false,
headerShown: true,
headerStyle: {
backgroundColor: "#fff",
},
headerTitleStyle: {
fontWeight: "600",
},
}} }}
> >
<Tabs.Screen <Tabs.Screen
name="index" name="index"
options={{ options={{
title: "Home", title: "Home",
headerTitle: "FitAI",
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="attendance"
options={{
title: "Attendance",
headerTitle: "Attendance",
tabBarIcon: ({ color, size }) => (
<Ionicons name="calendar" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
headerTitle: "My Profile",
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name="goals" name="goals"
options={{ options={{
title: "Goals", title: "Goals",
headerTitle: "Fitness Goals", }}
tabBarIcon: ({ color, size }) => ( />
<Ionicons name="trophy" size={size} color={color} /> <Tabs.Screen
), name="recommendations"
options={{
title: "AI",
}}
/>
<Tabs.Screen
name="attendance"
options={{
title: "Attendance",
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
}} }}
/> />
</Tabs> </Tabs>

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,113 +1,86 @@
import { useState, useEffect } from "react"; import React, { useState, useCallback, useRef } from 'react';
import { import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native';
View, import { Ionicons } from '@expo/vector-icons';
Text, import { LinearGradient } from 'expo-linear-gradient';
StyleSheet, import { theme } from '../../styles/theme';
ScrollView, import { GoalProgressCard } from '../../components/GoalProgressCard';
TouchableOpacity, import { GoalCreationModal } from '../../components/GoalCreationModal';
ActivityIndicator, import { useUser, useAuth } from "@clerk/clerk-expo";
RefreshControl, import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals';
Alert, import { useFocusEffect } from 'expo-router';
} from "react-native";
import { useAuth } from "@clerk/clerk-expo";
import { Ionicons } from "@expo/vector-icons";
import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from "../../services/fitnessGoals";
import { GoalProgressCard } from "../../components/GoalProgressCard";
import { GoalCreationModal } from "../../components/GoalCreationModal";
export default function GoalsScreen() { export default function GoalsScreen() {
const { userId, getToken } = useAuth(); const { user } = useUser();
const { getToken } = useAuth();
const [goals, setGoals] = useState<FitnessGoal[]>([]); const [goals, setGoals] = useState<FitnessGoal[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [showCreateModal, setShowCreateModal] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const fabScale = useRef(new Animated.Value(1)).current;
const fetchGoals = async () => { const loadGoals = useCallback(async () => {
if (!userId) return; if (!user?.id) return;
try { try {
const token = await getToken(); const token = await getToken();
const data = await fitnessGoalsService.getGoals(userId, token); const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
setGoals(data); setGoals(loadedGoals);
} catch (error) { } catch (error) {
console.error("Error fetching fitness goals:", error); console.error('Error loading goals:', error);
Alert.alert("Error", "Failed to load goals. Please try again.");
} finally {
setLoading(false);
setRefreshing(false);
} }
}; }, [user?.id]); // Removed getToken from dependencies
useEffect(() => { useFocusEffect(
fetchGoals(); useCallback(() => {
}, [userId]); loadGoals();
}, [loadGoals])
);
const onRefresh = () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
fetchGoals(); await loadGoals();
setRefreshing(false);
}; };
const handleCreateGoal = async (goalData: CreateGoalData) => { const handleCreateGoal = async (newGoal: CreateGoalData) => {
try { const token = await getToken();
const token = await getToken(); await fitnessGoalsService.createGoal(newGoal, token);
const newGoal = await fitnessGoalsService.createGoal(goalData, token); await loadGoals();
setGoals(prev => [newGoal, ...prev]); setIsModalVisible(false);
Alert.alert("Success", "Goal created successfully!");
} catch (error) {
console.error("Error creating goal:", error);
throw error;
}
}; };
const handleCompleteGoal = async (goalId: string) => { const handleCompleteGoal = async (goal: FitnessGoal) => {
try { const token = await getToken();
const token = await getToken(); await fitnessGoalsService.completeGoal(goal.id, token);
const updatedGoal = await fitnessGoalsService.completeGoal(goalId, token); await loadGoals();
setGoals(prev => prev.map(g => g.id === goalId ? updatedGoal : g));
Alert.alert("Success", "Goal completed! 🎉");
} catch (error) {
console.error("Error completing goal:", error);
Alert.alert("Error", "Failed to complete goal. Please try again.");
}
}; };
const handleDeleteGoal = async (goalId: string) => { const handleDeleteGoal = async (goalId: string) => {
try { const token = await getToken();
const token = await getToken(); await fitnessGoalsService.deleteGoal(goalId, token);
await fitnessGoalsService.deleteGoal(goalId, token); await loadGoals();
setGoals(prev => prev.filter(g => g.id !== goalId));
Alert.alert("Success", "Goal deleted");
} catch (error) {
console.error("Error deleting goal:", error);
Alert.alert("Error", "Failed to delete goal. Please try again.");
}
}; };
const activeGoals = goals.filter(g => g.status === 'active'); const activeGoals = goals.filter(g => g.status === 'active');
const completedGoals = goals.filter(g => g.status === 'completed'); const completedGoals = goals.filter(g => g.status === 'completed');
if (loading && !refreshing) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#2563eb" />
</View>
);
}
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView <ScrollView
contentContainerStyle={styles.scrollContent}
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
} }
> >
<View style={styles.header}> <LinearGradient
colors={theme.gradients.primary}
style={styles.header}
>
<View> <View>
<Text style={styles.headerTitle}>My Fitness Goals</Text> <Text style={styles.headerTitle}>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 && (
@ -124,7 +97,7 @@ export default function GoalsScreen() {
<Text style={styles.statValue}> <Text style={styles.statValue}>
{activeGoals.length > 0 {activeGoals.length > 0
? Math.round( ? Math.round(
activeGoals.reduce((sum, g) => sum + g.progress, 0) / activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) /
activeGoals.length activeGoals.length
) )
: 0}% : 0}%
@ -152,7 +125,7 @@ export default function GoalsScreen() {
<GoalProgressCard <GoalProgressCard
key={goal.id} key={goal.id}
goal={goal} goal={goal}
onComplete={() => handleCompleteGoal(goal.id)} onComplete={() => handleCompleteGoal(goal)}
onDelete={() => handleDeleteGoal(goal.id)} onDelete={() => handleDeleteGoal(goal.id)}
/> />
)) ))
@ -179,17 +152,42 @@ export default function GoalsScreen() {
</ScrollView> </ScrollView>
{/* Floating Action Button */} {/* Floating Action Button */}
<TouchableOpacity <Animated.View style={[styles.fabContainer, { transform: [{ scale: fabScale }] }]}>
style={styles.fab} <TouchableOpacity
onPress={() => setShowCreateModal(true)} onPress={() => setIsModalVisible(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
visible={showCreateModal} visible={isModalVisible}
onClose={() => setShowCreateModal(false)} onClose={() => setIsModalVisible(false)}
onSubmit={handleCreateGoal} onSubmit={handleCreateGoal}
/> />
</View> </View>
@ -199,26 +197,27 @@ 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: { scrollContent: {
flex: 1, paddingBottom: 20,
justifyContent: "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 +227,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: {
@ -277,20 +274,17 @@ const styles = StyleSheet.create({
footer: { footer: {
height: 100, height: 100,
}, },
fab: { fabContainer: {
position: "absolute", position: "absolute",
right: 20, right: 20,
bottom: 20, bottom: 110, // Adjusted for tab bar height
width: 56, },
height: 56, fab: {
borderRadius: 28, width: 64,
backgroundColor: "#2563eb", height: 64,
borderRadius: 32,
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,320 +1,229 @@
import React from "react"; import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native";
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
} from "react-native";
import { useUser } from "@clerk/clerk-expo"; import { useUser } from "@clerk/clerk-expo";
import { LinearGradient } from "expo-linear-gradient";
import { useState, useCallback } from "react";
import { theme } from "../../styles/theme";
import { ActivityWidget } from "../../components/ActivityWidget";
import { QuickActionGrid } from "../../components/QuickActionGrid";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
export default function HomeScreen() { export default function HomeScreen() {
const { user, isLoaded } = useUser(); const { user } = useUser();
const router = useRouter(); const [refreshing, setRefreshing] = useState(false);
if (!isLoaded || !user) { const onRefresh = useCallback(() => {
return ( setRefreshing(true);
<View style={styles.container}> setTimeout(() => {
<Text>Loading...</Text> setRefreshing(false);
</View> }, 2000);
); }, []);
}
const firstName = user.firstName || "User"; const getGreeting = () => {
const greeting = getGreeting(); const hour = new Date().getHours();
if (hour < 12) return "Good Morning";
if (hour < 18) return "Good Afternoon";
return "Good Evening";
};
return ( return (
<ScrollView style={styles.container}> <View style={styles.container}>
<View style={styles.content}> <ScrollView
{/* Welcome Header */} contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={theme.colors.primary} />
}
>
{/* Header Section */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.greeting}>{greeting}!</Text> <View>
<Text style={styles.name}>{firstName}</Text> <Text style={styles.greeting}>{getGreeting()},</Text>
</View> <Text style={styles.name}>{user?.firstName || "Athlete"}</Text>
{/* Quick Stats */}
<View style={styles.statsContainer}>
<View style={styles.statCard}>
<Ionicons name="calendar-outline" size={32} color="#2563eb" />
<Text style={styles.statValue}>0</Text>
<Text style={styles.statLabel}>This Month</Text>
</View> </View>
<View style={styles.avatarContainer}>
<View style={styles.statCard}> {user?.imageUrl ? (
<Ionicons name="flame-outline" size={32} color="#ef4444" /> <Image source={{ uri: user.imageUrl }} style={styles.avatar} />
<Text style={styles.statValue}>0</Text> ) : (
<Text style={styles.statLabel}>Day Streak</Text> <View style={styles.placeholderAvatar}>
</View> <Ionicons name="person" size={24} color="#fff" />
</View>
<View style={styles.statCard}> )}
<Ionicons name="trophy-outline" size={32} color="#f59e0b" />
<Text style={styles.statValue}>0</Text>
<Text style={styles.statLabel}>Total Visits</Text>
</View> </View>
</View> </View>
{/* Activity Widget */}
<ActivityWidget
steps={8432}
calories={640}
duration={45}
/>
{/* Quick Actions */} {/* Quick Actions */}
<QuickActionGrid />
{/* Recent Activity Section */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Actions</Text> <View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Recent Activity</Text>
<TouchableOpacity <Text style={styles.seeAll}>See All</Text>
style={styles.actionButton}
onPress={() => router.push("/fitness-profile")}
>
<View style={styles.actionIcon}>
<Ionicons name="fitness-outline" size={24} color="#ec4899" />
</View>
<View style={styles.actionContent}>
<Text style={styles.actionTitle}>Fitness Profile</Text>
<Text style={styles.actionSubtitle}>
Manage your fitness information
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</TouchableOpacity>
<View style={styles.actionButton}>
<View style={styles.actionIcon}>
<Ionicons name="log-in-outline" size={24} color="#2563eb" />
</View>
<View style={styles.actionContent}>
<Text style={styles.actionTitle}>Check In</Text>
<Text style={styles.actionSubtitle}>
Start your workout session
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color="#9ca3af" />
</View> </View>
<View style={styles.activityCard}>
<View style={styles.actionButton}> <LinearGradient
<View style={styles.actionIcon}> colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
<Ionicons name="calendar-outline" size={24} color="#10b981" /> style={[styles.recentItem, theme.shadows.subtle]}
</View> >
<View style={styles.actionContent}> <View style={styles.recentIconContainer}>
<Text style={styles.actionTitle}>View Schedule</Text> <LinearGradient
<Text style={styles.actionSubtitle}> colors={theme.gradients.primary}
Check your upcoming classes style={styles.recentIcon}
</Text> >
</View> <Ionicons name="barbell" size={20} color="#fff" />
<Ionicons name="chevron-forward" size={20} color="#9ca3af" /> </LinearGradient>
</View>
<View style={styles.actionButton}>
<View style={styles.actionIcon}>
<Ionicons name="card-outline" size={24} color="#8b5cf6" />
</View>
<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>
</View>
{/* Membership Info */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Membership</Text>
<View style={styles.membershipCard}>
<View style={styles.membershipHeader}>
<Text style={styles.membershipType}>Basic Plan</Text>
<View style={styles.statusBadge}>
<Text style={styles.statusText}>Active</Text>
</View> </View>
</View> <View style={styles.recentInfo}>
<Text style={styles.membershipEmail}> <Text style={styles.recentTitle}>Upper Body Power</Text>
{user.primaryEmailAddress?.emailAddress} <Text style={styles.recentSubtitle}>Today, 10:00 AM</Text>
</Text> </View>
<Text style={styles.membershipDate}> <Text style={styles.recentValue}>45m</Text>
Member since {new Date(user.createdAt!).toLocaleDateString()} </LinearGradient>
</Text>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.success}
style={styles.recentIcon}
>
<Ionicons name="bicycle" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Morning Cardio</Text>
<Text style={styles.recentSubtitle}>Yesterday, 7:30 AM</Text>
</View>
<Text style={styles.recentValue}>30m</Text>
</LinearGradient>
</View> </View>
</View> </View>
{/* Recent Activity */} {/* Bottom Spacer for Tab Bar */}
<View style={styles.section}> <View style={{ height: 100 }} />
<Text style={styles.sectionTitle}>Recent Activity</Text> </ScrollView>
<View style={styles.emptyState}> </View>
<Ionicons name="barbell-outline" size={48} color="#d1d5db" />
<Text style={styles.emptyStateText}>No recent activity</Text>
<Text style={styles.emptyStateSubtext}>
Check in to start tracking your workouts
</Text>
</View>
</View>
</View>
</ScrollView>
); );
} }
function getGreeting(): string {
const hour = new Date().getHours();
if (hour < 12) return "Good morning";
if (hour < 18) return "Good afternoon";
return "Good evening";
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#f5f5f5", backgroundColor: theme.colors.background,
}, },
content: { scrollContent: {
padding: 20, paddingTop: 60,
}, },
header: { header: {
marginBottom: 24, flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 24,
marginBottom: 32,
}, },
greeting: { greeting: {
fontSize: 16, fontSize: 16,
color: "#6b7280", color: theme.colors.gray600,
fontWeight: "500",
marginBottom: 4, marginBottom: 4,
}, },
name: { name: {
fontSize: 32, fontSize: 32,
fontWeight: "bold", fontWeight: "800",
color: "#1a1a1a", color: theme.colors.gray900,
letterSpacing: -0.5,
}, },
statsContainer: { avatarContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 24,
},
statCard: {
flex: 1,
backgroundColor: "white",
borderRadius: 12,
padding: 16,
alignItems: "center",
marginHorizontal: 4,
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { width: 0, height: 1 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.05, shadowOpacity: 0.1,
shadowRadius: 2, shadowRadius: 12,
elevation: 2, elevation: 5,
}, },
statValue: { avatar: {
fontSize: 24, width: 56,
fontWeight: "bold", height: 56,
color: "#1a1a1a", borderRadius: 20,
marginTop: 8, borderWidth: 2,
marginBottom: 4, borderColor: "#fff",
}, },
statLabel: { placeholderAvatar: {
fontSize: 12, width: 56,
color: "#6b7280", height: 56,
textAlign: "center", borderRadius: 20,
backgroundColor: theme.colors.primary,
justifyContent: "center",
alignItems: "center",
borderWidth: 2,
borderColor: "#fff",
}, },
section: { section: {
paddingHorizontal: 20,
marginBottom: 24, marginBottom: 24,
}, },
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
},
sectionTitle: { sectionTitle: {
fontSize: 18, fontSize: 18,
fontWeight: "600", fontWeight: "700",
color: "#1a1a1a", color: theme.colors.gray900,
marginBottom: 12,
}, },
actionButton: { seeAll: {
fontSize: 14,
color: theme.colors.primary,
fontWeight: "600",
},
activityCard: {
gap: 12,
},
recentItem: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
backgroundColor: "white",
borderRadius: 12,
padding: 16, padding: 16,
marginBottom: 8, borderRadius: 20,
shadowColor: "#000", backgroundColor: "#fff",
shadowOffset: { width: 0, height: 1 }, borderWidth: 1,
shadowOpacity: 0.05, borderColor: "rgba(255, 255, 255, 0.6)",
shadowRadius: 2,
elevation: 2,
}, },
actionIcon: { recentIconContainer: {
marginRight: 16,
},
recentIcon: {
width: 48, width: 48,
height: 48, height: 48,
borderRadius: 24, borderRadius: 16,
backgroundColor: "#f0f9ff",
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
marginRight: 12,
}, },
actionContent: { recentInfo: {
flex: 1, flex: 1,
}, },
actionTitle: { recentTitle: {
fontSize: 16, fontSize: 16,
fontWeight: "600", fontWeight: "600",
color: "#1a1a1a", color: theme.colors.gray900,
marginBottom: 2,
},
actionSubtitle: {
fontSize: 14,
color: "#6b7280",
},
membershipCard: {
backgroundColor: "white",
borderRadius: 12,
padding: 16,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
membershipHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
membershipType: {
fontSize: 18,
fontWeight: "600",
color: "#1a1a1a",
},
statusBadge: {
backgroundColor: "#dcfce7",
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
},
statusText: {
fontSize: 12,
fontWeight: "600",
color: "#16a34a",
},
membershipEmail: {
fontSize: 14,
color: "#6b7280",
marginBottom: 4, marginBottom: 4,
}, },
membershipDate: { recentSubtitle: {
fontSize: 12, fontSize: 12,
color: "#9ca3af", color: theme.colors.gray500,
}, },
emptyState: { recentValue: {
backgroundColor: "white",
borderRadius: 12,
padding: 32,
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
emptyStateText: {
fontSize: 16,
fontWeight: "600",
color: "#6b7280",
marginTop: 12,
marginBottom: 4,
},
emptyStateSubtext: {
fontSize: 14, fontSize: 14,
color: "#9ca3af", fontWeight: "600",
textAlign: "center", color: theme.colors.gray900,
}, },
}); });

View File

@ -1,307 +1,267 @@
import React from "react"; import { View, Text, StyleSheet, TouchableOpacity, Image, Alert } from "react-native";
import { import { useUser, useClerk } from "@clerk/clerk-expo";
View,
Text,
StyleSheet,
TouchableOpacity,
Alert,
ScrollView,
} from "react-native";
import { useUser, useAuth } from "@clerk/clerk-expo";
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";
import { AnimatedButton } from "../../components/AnimatedButton";
import { GradientBackground } from "../../components/GradientBackground";
export default function ProfileScreen() { export default function ProfileScreen() {
const { user } = useUser(); const { user } = useUser();
const { signOut } = useAuth(); const { signOut } = useClerk();
const router = useRouter();
const handleLogout = async () => { const handleSignOut = async () => {
Alert.alert("Sign Out", "Are you sure you want to sign out?", [ try {
{ text: "Cancel", style: "cancel" }, await signOut();
{ } catch (err) {
text: "Sign Out", console.error("Error signing out:", err);
style: "destructive", }
onPress: async () => {
try {
await signOut();
router.replace("/(auth)/sign-in");
} catch (error) {
Alert.alert("Error", "Failed to sign out");
}
},
},
]);
}; };
if (!user) { const confirmSignOut = () => {
return ( Alert.alert(
<View style={styles.container}> "Sign Out",
<Text>Loading...</Text> "Are you sure you want to sign out?",
</View> [
{ text: "Cancel", style: "cancel" },
{ text: "Sign Out", style: "destructive", onPress: handleSignOut },
]
); );
} };
return ( return (
<ScrollView style={styles.container}> <View style={styles.container}>
<View style={styles.content}> <GradientBackground variant="primary" style={styles.header}>
{/* Profile Header */}
<View style={styles.profileCard}> <View style={styles.profileCard}>
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
{user.imageUrl ? ( {user?.imageUrl ? (
<View style={styles.avatar}> <Image source={{ uri: user.imageUrl }} style={styles.avatar} />
<Text style={styles.avatarText}>
{user.firstName?.charAt(0)}
{user.lastName?.charAt(0)}
</Text>
</View>
) : ( ) : (
<View style={styles.avatar}> <View style={styles.placeholderAvatar}>
<Ionicons name="person" size={40} color="#fff" /> <Ionicons name="person" size={40} color="#fff" />
</View> </View>
)} )}
<View style={styles.editBadge}>
<Ionicons name="pencil" size={12} color={theme.colors.primary} />
</View>
</View>
<Text style={styles.name}>{user?.fullName || "User"}</Text>
<Text style={styles.email}>{user?.primaryEmailAddress?.emailAddress}</Text>
<View style={styles.memberBadge}>
<Text style={styles.memberText}>Premium Member</Text>
</View> </View>
<Text style={styles.name}>
{user.firstName} {user.lastName}
</Text>
<Text style={styles.email}>
{user.primaryEmailAddress?.emailAddress}
</Text>
{user.primaryPhoneNumber && (
<Text style={styles.phone}>
{user.primaryPhoneNumber.phoneNumber}
</Text>
)}
</View> </View>
</GradientBackground>
{/* Account Information */} <View style={styles.content}>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Account Information</Text> <Text style={styles.sectionTitle}>Account</Text>
<View style={[styles.infoCard, theme.shadows.subtle]}>
<View style={styles.infoRow}> <TouchableOpacity style={styles.infoRow}>
<View style={styles.infoLabel}> <LinearGradient
<Ionicons name="mail-outline" size={20} color="#666" /> colors={['rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.05)']}
<Text style={styles.infoLabelText}>Email</Text> style={styles.iconContainer}
</View> >
<Text style={styles.infoValue}> <Ionicons name="person-outline" size={20} color={theme.colors.primary} />
{user.primaryEmailAddress?.emailAddress} </LinearGradient>
</Text> <Text style={styles.infoLabel}>Personal Details</Text>
</View> <Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
</TouchableOpacity>
{user.primaryPhoneNumber && ( <View style={styles.divider} />
<View style={styles.infoRow}> <TouchableOpacity style={styles.infoRow}>
<View style={styles.infoLabel}> <LinearGradient
<Ionicons name="call-outline" size={20} color="#666" /> colors={['rgba(16, 185, 129, 0.1)', 'rgba(16, 185, 129, 0.05)']}
<Text style={styles.infoLabelText}>Phone</Text> style={styles.iconContainer}
</View> >
<Text style={styles.infoValue}> <Ionicons name="fitness-outline" size={20} color={theme.colors.success} />
{user.primaryPhoneNumber.phoneNumber} </LinearGradient>
</Text> <Text style={styles.infoLabel}>Fitness Profile</Text>
</View> <Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
)} </TouchableOpacity>
<View style={styles.divider} />
<View style={styles.infoRow}> <TouchableOpacity style={styles.infoRow}>
<View style={styles.infoLabel}> <LinearGradient
<Ionicons name="calendar-outline" size={20} color="#666" /> colors={['rgba(245, 158, 11, 0.1)', 'rgba(245, 158, 11, 0.05)']}
<Text style={styles.infoLabelText}>Member Since</Text> style={styles.iconContainer}
</View> >
<Text style={styles.infoValue}> <Ionicons name="notifications-outline" size={20} color={theme.colors.warning} />
{new Date(user.createdAt!).toLocaleDateString()} </LinearGradient>
</Text> <Text style={styles.infoLabel}>Notifications</Text>
</View> <Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
</TouchableOpacity>
<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>
<Text style={styles.infoValue}>
{user.primaryEmailAddress?.verification?.status === "verified"
? "Yes"
: "No"}
</Text>
</View> </View>
</View> </View>
{/* Quick Actions */}
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Actions</Text> <Text style={styles.sectionTitle}>Support</Text>
<View style={[styles.infoCard, theme.shadows.subtle]}>
<TouchableOpacity style={styles.actionButton}> <TouchableOpacity style={styles.infoRow}>
<Ionicons name="person-outline" size={24} color="#2563eb" /> <LinearGradient
<Text style={styles.actionButtonText}>Edit Profile</Text> colors={['rgba(139, 92, 246, 0.1)', 'rgba(139, 92, 246, 0.05)']}
<Ionicons name="chevron-forward" size={20} color="#999" /> style={styles.iconContainer}
</TouchableOpacity> >
<Ionicons name="help-circle-outline" size={20} color={theme.colors.secondary} />
<TouchableOpacity style={styles.actionButton}> </LinearGradient>
<Ionicons name="notifications-outline" size={24} color="#2563eb" /> <Text style={styles.infoLabel}>Help Center</Text>
<Text style={styles.actionButtonText}>Notifications</Text> <Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
<Ionicons name="chevron-forward" size={20} color="#999" /> </TouchableOpacity>
</TouchableOpacity> <View style={styles.divider} />
<TouchableOpacity style={styles.infoRow}>
<TouchableOpacity style={styles.actionButton}> <LinearGradient
<Ionicons name="card-outline" size={24} color="#2563eb" /> colors={['rgba(107, 114, 128, 0.1)', 'rgba(107, 114, 128, 0.05)']}
<Text style={styles.actionButtonText}>Payment History</Text> style={styles.iconContainer}
<Ionicons name="chevron-forward" size={20} color="#999" /> >
</TouchableOpacity> <Ionicons name="shield-checkmark-outline" size={20} color={theme.colors.gray600} />
</LinearGradient>
<TouchableOpacity style={styles.actionButton}> <Text style={styles.infoLabel}>Privacy & Security</Text>
<Ionicons name="settings-outline" size={24} color="#2563eb" /> <Ionicons name="chevron-forward" size={20} color={theme.colors.gray400} />
<Text style={styles.actionButtonText}>Settings</Text> </TouchableOpacity>
<Ionicons name="chevron-forward" size={20} color="#999" /> </View>
</TouchableOpacity>
</View> </View>
{/* Sign Out Button */} <AnimatedButton
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}> title="Sign Out"
<Ionicons name="log-out-outline" size={20} color="#fff" /> onPress={confirmSignOut}
<Text style={styles.logoutText}>Sign Out</Text> variant="danger"
</TouchableOpacity> style={styles.signOutButton}
icon={<Ionicons name="log-out-outline" size={20} color="#fff" />}
/>
{/* App Version */}
<Text style={styles.version}>Version 1.0.0</Text> <Text style={styles.version}>Version 1.0.0</Text>
</View> </View>
</ScrollView> </View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: "#f5f5f5", backgroundColor: theme.colors.background,
}, },
content: { header: {
padding: 20, paddingTop: 60,
paddingBottom: 30,
borderBottomLeftRadius: theme.borderRadius.xl,
borderBottomRightRadius: theme.borderRadius.xl,
alignItems: 'center',
}, },
profileCard: { profileCard: {
backgroundColor: "white", alignItems: 'center',
borderRadius: 16,
padding: 24,
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
marginBottom: 24,
}, },
avatarContainer: { avatarContainer: {
position: 'relative',
marginBottom: 16, marginBottom: 16,
}, },
avatar: { avatar: {
width: 80, width: 100,
height: 80, height: 100,
borderRadius: 40, borderRadius: 50,
backgroundColor: "#2563eb", borderWidth: 4,
justifyContent: "center", borderColor: 'rgba(255, 255, 255, 0.3)',
alignItems: "center",
}, },
avatarText: { placeholderAvatar: {
fontSize: 32, width: 100,
fontWeight: "bold", height: 100,
color: "#fff", borderRadius: 50,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 4,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
editBadge: {
position: 'absolute',
bottom: 0,
right: 0,
backgroundColor: theme.colors.white,
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
}, },
name: { name: {
fontSize: 24, fontSize: theme.typography.fontSize['2xl'],
fontWeight: "bold", fontWeight: theme.typography.fontWeight.bold,
color: "#1a1a1a", color: theme.colors.white,
marginBottom: 4, marginBottom: 4,
}, },
email: { email: {
fontSize: 16, fontSize: theme.typography.fontSize.sm,
color: "#666", color: 'rgba(255, 255, 255, 0.8)',
marginBottom: 4, marginBottom: 12,
}, },
phone: { memberBadge: {
fontSize: 14, backgroundColor: 'rgba(255, 255, 255, 0.2)',
color: "#999", paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: theme.borderRadius.full,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
memberText: {
color: theme.colors.white,
fontSize: theme.typography.fontSize.xs,
fontWeight: theme.typography.fontWeight.bold,
letterSpacing: 0.5,
},
content: {
padding: 20,
marginTop: -20,
}, },
section: { 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.lg,
fontWeight: "600", fontWeight: theme.typography.fontWeight.bold,
color: "#1a1a1a", color: theme.colors.gray900,
marginBottom: 16, marginBottom: 12,
marginLeft: 4,
},
infoCard: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
padding: 8,
borderWidth: 1,
borderColor: theme.colors.gray100,
}, },
infoRow: { infoRow: {
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-between", alignItems: 'center',
alignItems: "center", padding: 12,
paddingVertical: 12, },
borderBottomWidth: 1, iconContainer: {
borderBottomColor: "#f0f0f0", width: 36,
height: 36,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
}, },
infoLabel: { infoLabel: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
infoLabelText: {
fontSize: 14,
color: "#666",
fontWeight: "500",
},
infoValue: {
fontSize: 14,
color: "#1a1a1a",
fontWeight: "500",
},
actionButton: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
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: { divider: {
backgroundColor: "#ef4444", height: 1,
paddingVertical: 16, backgroundColor: theme.colors.gray100,
borderRadius: 12, marginLeft: 60,
alignItems: "center", },
justifyContent: "center", signOutButton: {
flexDirection: "row",
gap: 8,
marginTop: 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",
}, },
version: { version: {
textAlign: "center", textAlign: 'center',
fontSize: 12, marginTop: 24,
color: "#999", color: theme.colors.gray400,
marginTop: 8, fontSize: theme.typography.fontSize.xs,
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,157 @@
import React from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
const { width } = Dimensions.get('window');
interface ActivityWidgetProps {
steps: number;
calories: number;
duration: number; // in minutes
}
export function ActivityWidget({ steps, calories, duration }: ActivityWidgetProps) {
return (
<View style={styles.container}>
<LinearGradient
colors={theme.gradients.dark}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.header}>
<Text style={styles.title}>Daily Activity</Text>
<Ionicons name="stats-chart" size={20} color={theme.colors.primary} />
</View>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(59, 130, 246, 0.2)' }]}>
<Ionicons name="footsteps" size={20} color="#3b82f6" />
</View>
<Text style={styles.statValue}>{steps.toLocaleString()}</Text>
<Text style={styles.statLabel}>Steps</Text>
</View>
<View style={styles.divider} />
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(239, 68, 68, 0.2)' }]}>
<Ionicons name="flame" size={20} color="#ef4444" />
</View>
<Text style={styles.statValue}>{calories}</Text>
<Text style={styles.statLabel}>Kcal</Text>
</View>
<View style={styles.divider} />
<View style={styles.statItem}>
<View style={[styles.iconContainer, { backgroundColor: 'rgba(16, 185, 129, 0.2)' }]}>
<Ionicons name="time" size={20} color="#10b981" />
</View>
<Text style={styles.statValue}>{duration}m</Text>
<Text style={styles.statLabel}>Active</Text>
</View>
</View>
{/* Simple Bar Chart Visualization */}
<View style={styles.chartContainer}>
{[0.4, 0.6, 0.3, 0.8, 0.5, 0.9, 0.7].map((height, index) => (
<View key={index} style={styles.barContainer}>
<LinearGradient
colors={theme.gradients.primaryVertical}
style={[styles.bar, { height: height * 60 }]}
/>
<Text style={styles.dayLabel}>
{['M', 'T', 'W', 'T', 'F', 'S', 'S'][index]}
</Text>
</View>
))}
</View>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
marginBottom: 20,
},
card: {
borderRadius: 24,
padding: 20,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: '700',
color: '#fff',
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
statItem: {
alignItems: 'center',
flex: 1,
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
statValue: {
fontSize: 20,
fontWeight: '700',
color: '#fff',
marginBottom: 2,
},
statLabel: {
fontSize: 12,
color: theme.colors.gray400,
fontWeight: '500',
},
divider: {
width: 1,
height: 40,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
chartContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-end',
height: 80,
paddingTop: 10,
borderTopWidth: 1,
borderTopColor: 'rgba(255, 255, 255, 0.1)',
},
barContainer: {
alignItems: 'center',
gap: 8,
},
bar: {
width: 6,
borderRadius: 3,
opacity: 0.8,
},
dayLabel: {
fontSize: 10,
color: theme.colors.gray500,
fontWeight: '600',
},
});

View File

@ -0,0 +1,154 @@
/**
* 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';
import { haptics } from '../utils/haptics';
interface AnimatedButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
icon?: React.ReactNode;
}
export function AnimatedButton({
title,
onPress,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
style,
textStyle,
icon,
}: AnimatedButtonProps) {
const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
Animated.spring(scale, {
toValue: 0.95,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
Animated.spring(scale, {
toValue: 1,
useNativeDriver: true,
}).start();
};
const handlePress = () => {
if (!disabled && !loading) {
haptics.light();
onPress();
}
};
const getGradientColors = () => {
if (disabled) return ['#9ca3af', '#6b7280'] as const;
switch (variant) {
case 'secondary':
return theme.gradients.purple;
case 'success':
return theme.gradients.success;
case 'danger':
return theme.gradients.danger;
case 'warning':
return theme.gradients.warning;
default:
return theme.gradients.primary;
}
};
const getSizeStyles = () => {
switch (size) {
case 'sm':
return { paddingVertical: 8, paddingHorizontal: 16, fontSize: 14 };
case 'lg':
return { paddingVertical: 16, paddingHorizontal: 32, fontSize: 18 };
default:
return { paddingVertical: 12, paddingHorizontal: 24, fontSize: 16 };
}
};
const sizeStyles = getSizeStyles();
return (
<TouchableOpacity
onPressIn={handlePressIn}
onPressOut={handlePressOut}
onPress={handlePress}
disabled={disabled || loading}
activeOpacity={0.9}
style={[styles.container, style]}
>
<Animated.View style={{ transform: [{ scale }], width: '100%' }}>
<LinearGradient
colors={getGradientColors()}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={[
styles.gradient,
{ paddingVertical: sizeStyles.paddingVertical, paddingHorizontal: sizeStyles.paddingHorizontal },
]}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<>
{icon && <React.Fragment>{icon}</React.Fragment>}
<Text
style={[
styles.text,
{ fontSize: sizeStyles.fontSize },
icon ? { marginLeft: 8 } : {},
textStyle,
]}
>
{title}
</Text>
</>
)}
</LinearGradient>
</Animated.View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
gradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 12,
...theme.shadows.medium,
},
text: {
color: '#fff',
fontWeight: '600',
textAlign: 'center',
},
});

View File

@ -0,0 +1,72 @@
import React, { useEffect, useRef } from 'react';
import { View, Animated, StyleSheet, Text } from 'react-native';
import Svg, { Circle } from 'react-native-svg';
import { theme } from '../styles/theme';
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
interface CircularProgressProps {
size?: number;
strokeWidth?: number;
progress: number; // 0 to 100
color?: string;
backgroundColor?: string;
}
export function CircularProgress({
size = 60,
strokeWidth = 6,
progress,
color = theme.colors.primary,
backgroundColor = theme.colors.gray200,
}: CircularProgressProps) {
const animatedValue = useRef(new Animated.Value(0)).current;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
useEffect(() => {
Animated.timing(animatedValue, {
toValue: progress,
duration: 1000,
useNativeDriver: true,
}).start();
}, [progress]);
const strokeDashoffset = animatedValue.interpolate({
inputRange: [0, 100],
outputRange: [circumference, 0],
});
return (
<View style={{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }}>
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<Circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={backgroundColor}
strokeWidth={strokeWidth}
fill="transparent"
/>
<AnimatedCircle
cx={size / 2}
cy={size / 2}
r={radius}
stroke={color}
strokeWidth={strokeWidth}
fill="transparent"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
rotation="-90"
origin={`${size / 2}, ${size / 2}`}
/>
</Svg>
<View style={[StyleSheet.absoluteFillObject, { justifyContent: 'center', alignItems: 'center' }]}>
<Text style={{ fontSize: size * 0.25, fontWeight: 'bold', color: theme.colors.gray900 }}>
{Math.round(progress)}%
</Text>
</View>
</View>
);
}

View File

@ -0,0 +1,143 @@
import React from 'react';
import { View, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
import { Animated } from 'react-native';
const { width } = Dimensions.get('window');
export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
return (
<View style={styles.container}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.9)', 'rgba(255, 255, 255, 0.7)']}
style={[styles.tabBar, theme.shadows.medium]}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
const getIconName = (routeName: string, focused: boolean): keyof typeof Ionicons.glyphMap => {
switch (routeName) {
case 'index':
return focused ? 'home' : 'home-outline';
case 'goals':
return focused ? 'trophy' : 'trophy-outline';
case 'attendance':
return focused ? 'calendar' : 'calendar-outline';
case 'recommendations':
return focused ? 'sparkles' : 'sparkles-outline';
case 'profile':
return focused ? 'person' : 'person-outline';
default:
return 'ellipse-outline';
}
};
// Animation for scale
const scaleValue = React.useRef(new Animated.Value(1)).current;
React.useEffect(() => {
Animated.spring(scaleValue, {
toValue: isFocused ? 1.2 : 1,
useNativeDriver: true,
friction: 8,
}).start();
}, [isFocused]);
return (
<TouchableOpacity
key={index}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={(options as any).tabBarTestID}
onPress={onPress}
style={styles.tabItem}
activeOpacity={0.7}
>
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
{isFocused ? (
<LinearGradient
colors={theme.gradients.primary}
style={styles.iconContainer}
>
<Ionicons
name={getIconName(route.name, true)}
size={20}
color="#fff"
/>
</LinearGradient>
) : (
<Ionicons
name={getIconName(route.name, false)}
size={24}
color={theme.colors.gray500}
/>
)}
</Animated.View>
</TouchableOpacity>
);
})}
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
alignItems: 'center',
paddingBottom: Platform.OS === 'ios' ? 30 : 20,
pointerEvents: 'box-none',
},
tabBar: {
flexDirection: 'row',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderRadius: 35,
height: 70,
width: width - 40,
justifyContent: 'space-around',
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.5)',
paddingHorizontal: 10,
},
tabItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
});

View File

@ -0,0 +1,223 @@
import React, { useRef } from 'react';
import { View, Text, StyleSheet, Dimensions, ScrollView, TouchableOpacity, Animated } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
import { CircularProgress } from './CircularProgress';
import type { FitnessGoal } from '../services/fitnessGoals';
const { width } = Dimensions.get('window');
const CARD_WIDTH = width * 0.8;
const SPACING = 20;
interface GoalCarouselProps {
goals: FitnessGoal[];
onGoalPress: (goal: FitnessGoal) => void;
}
export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) {
const scrollX = useRef(new Animated.Value(0)).current;
if (goals.length === 0) {
return (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No active goals</Text>
</View>
);
}
return (
<View>
<Animated.ScrollView
horizontal
showsHorizontalScrollIndicator={false}
snapToInterval={CARD_WIDTH + SPACING}
decelerationRate="fast"
contentContainerStyle={styles.scrollContent}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
>
{goals.map((goal, index) => {
const inputRange = [
(index - 1) * (CARD_WIDTH + SPACING),
index * (CARD_WIDTH + SPACING),
(index + 1) * (CARD_WIDTH + SPACING),
];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0.9, 1, 0.9],
extrapolate: 'clamp',
});
const opacity = scrollX.interpolate({
inputRange,
outputRange: [0.7, 1, 0.7],
extrapolate: 'clamp',
});
return (
<TouchableOpacity
key={goal.id}
activeOpacity={0.9}
onPress={() => onGoalPress(goal)}
>
<Animated.View
style={[
styles.cardContainer,
{ transform: [{ scale }], opacity },
]}
>
<LinearGradient
colors={['#ffffff', '#f9fafb']}
style={[styles.card, theme.shadows.medium]}
>
<View style={styles.cardHeader}>
<View style={styles.iconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.iconGradient}
>
<Ionicons name="trophy" size={24} color="#fff" />
</LinearGradient>
</View>
<View style={styles.priorityBadge}>
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
</View>
</View>
<Text style={styles.title} numberOfLines={1}>{goal.title}</Text>
<Text style={styles.description} numberOfLines={2}>{goal.description}</Text>
<View style={styles.progressContainer}>
<CircularProgress
progress={goal.progress || 0}
size={80}
strokeWidth={8}
color={theme.colors.primary}
/>
<View style={styles.progressDetails}>
<Text style={styles.progressValue}>
{goal.currentValue} / {goal.targetValue}
</Text>
<Text style={styles.progressUnit}>{goal.unit}</Text>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.date}>
Target: {goal.targetDate ? new Date(goal.targetDate).toLocaleDateString() : 'No date'}
</Text>
</View>
</LinearGradient>
</Animated.View>
</TouchableOpacity>
);
})}
</Animated.ScrollView>
</View>
);
}
const styles = StyleSheet.create({
scrollContent: {
paddingHorizontal: (width - CARD_WIDTH) / 2,
paddingVertical: 20,
},
emptyContainer: {
height: 200,
justifyContent: 'center',
alignItems: 'center',
},
emptyText: {
color: theme.colors.gray500,
fontSize: 16,
},
cardContainer: {
width: CARD_WIDTH,
marginRight: SPACING,
},
card: {
borderRadius: 24,
padding: 24,
height: 320,
justifyContent: 'space-between',
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.05)',
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 16,
},
iconContainer: {
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 4,
},
iconGradient: {
width: 48,
height: 48,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
},
priorityBadge: {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
priorityText: {
color: theme.colors.primary,
fontSize: 12,
fontWeight: '700',
},
title: {
fontSize: 24,
fontWeight: '800',
color: theme.colors.gray900,
marginBottom: 8,
},
description: {
fontSize: 14,
color: theme.colors.gray500,
marginBottom: 24,
lineHeight: 20,
},
progressContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 24,
},
progressDetails: {
alignItems: 'flex-end',
},
progressValue: {
fontSize: 24,
fontWeight: '700',
color: theme.colors.gray900,
},
progressUnit: {
fontSize: 14,
color: theme.colors.gray500,
fontWeight: '500',
},
footer: {
borderTopWidth: 1,
borderTopColor: theme.colors.gray100,
paddingTop: 16,
},
date: {
fontSize: 12,
color: theme.colors.gray400,
fontWeight: '500',
},
});

View File

@ -8,6 +8,7 @@ import {
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
Platform, Platform,
Alert,
} from 'react-native'; } from 'react-native';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker'; import DateTimePicker from '@react-native-community/datetimepicker';
@ -59,7 +60,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
const handleSubmit = async () => { const handleSubmit = async () => {
if (!title.trim()) { if (!title.trim()) {
alert('Please enter a goal title'); Alert.alert('Error', 'Please enter a goal title');
return; return;
} }
@ -81,7 +82,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Error creating goal:', error); console.error('Error creating goal:', error);
alert('Failed to create goal. Please try again.'); Alert.alert('Error', 'Failed to create goal. Please try again.');
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@ -203,28 +204,31 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
<Text style={styles.label}>Target Date</Text> <Text style={styles.label}>Target Date</Text>
<TouchableOpacity <TouchableOpacity
style={styles.dateButton} style={styles.dateButton}
onPress={() => setShowDatePicker(true)} onPress={() => setShowDatePicker(!showDatePicker)}
> >
<Text style={targetDate ? styles.dateText : styles.datePlaceholder}> <Text style={targetDate ? styles.dateText : styles.datePlaceholder}>
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'} {targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
</Text> </Text>
<Ionicons name="calendar-outline" size={20} color="#6b7280" /> <Ionicons name={showDatePicker ? "chevron-up" : "calendar-outline"} size={20} color="#6b7280" />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{showDatePicker && ( {showDatePicker && (
<DateTimePicker <View style={Platform.OS === 'ios' ? styles.datePickerContainer : undefined}>
value={targetDate || new Date()} <DateTimePicker
mode="date" value={targetDate || new Date()}
display={Platform.OS === 'ios' ? 'spinner' : 'default'} mode="date"
onChange={(event, selectedDate) => { display={Platform.OS === 'ios' ? 'inline' : 'default'}
setShowDatePicker(Platform.OS === 'ios'); onChange={(event, selectedDate) => {
if (selectedDate) { setShowDatePicker(Platform.OS === 'ios');
setTargetDate(selectedDate); if (selectedDate) {
} setTargetDate(selectedDate);
}} }
minimumDate={new Date()} }}
/> minimumDate={new Date()}
themeVariant="light"
/>
</View>
)} )}
{/* Priority */} {/* Priority */}
@ -411,4 +415,12 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#fff', color: '#fff',
}, },
datePickerContainer: {
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#d1d5db',
marginTop: 8,
overflow: 'hidden',
},
}); });

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,131 @@
import React from 'react';
import { StyleSheet, View, Animated, Dimensions, ImageSourcePropType } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
const { width } = Dimensions.get('window');
const HEADER_HEIGHT = 300;
interface ParallaxScrollViewProps {
children: React.ReactNode;
headerImage?: ImageSourcePropType;
headerBackgroundColor?: string;
headerContent?: React.ReactNode;
}
export function ParallaxScrollView({
children,
headerImage,
headerBackgroundColor = theme.colors.primary,
headerContent,
}: ParallaxScrollViewProps) {
const scrollY = React.useRef(new Animated.Value(0)).current;
const headerTranslateY = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT / 2],
extrapolate: 'clamp',
});
const imageScale = scrollY.interpolate({
inputRange: [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
outputRange: [2, 1, 1],
extrapolate: 'clamp',
});
const headerOpacity = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT / 2],
outputRange: [1, 0],
extrapolate: 'clamp',
});
return (
<View style={styles.container}>
<Animated.ScrollView
contentContainerStyle={styles.scrollContent}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
>
<View style={styles.contentContainer}>{children}</View>
</Animated.ScrollView>
<Animated.View
style={[
styles.header,
{
transform: [{ translateY: headerTranslateY }],
},
]}
>
<Animated.View
style={[
StyleSheet.absoluteFill,
{
transform: [{ scale: imageScale }],
},
]}
>
<LinearGradient
colors={theme.gradients.primary}
style={StyleSheet.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{headerImage && (
<Animated.Image
source={headerImage}
style={[styles.headerImage, { opacity: 0.5 }]}
/>
)}
</Animated.View>
<Animated.View style={[styles.headerContent, { opacity: headerOpacity }]}>
{headerContent}
</Animated.View>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
scrollContent: {
paddingTop: HEADER_HEIGHT,
},
contentContainer: {
backgroundColor: theme.colors.background,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
marginTop: -32,
paddingTop: 32,
minHeight: 800,
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: HEADER_HEIGHT,
overflow: 'hidden',
zIndex: 1,
},
headerImage: {
...StyleSheet.absoluteFillObject,
width: undefined,
height: undefined,
resizeMode: 'cover',
},
headerContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 32,
},
});

View File

@ -0,0 +1,114 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../styles/theme';
const { width } = Dimensions.get('window');
const ITEM_WIDTH = (width - 40 - 16) / 2; // (Screen width - padding - gap) / 2
interface QuickActionProps {
icon: keyof typeof Ionicons.glyphMap;
label: string;
gradient: readonly [string, string, ...string[]];
onPress?: () => void;
}
function QuickActionItem({ icon, label, gradient, onPress }: QuickActionProps) {
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={styles.itemContainer}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.6)']}
style={[styles.item, theme.shadows.subtle]}
>
<LinearGradient
colors={gradient}
style={styles.iconContainer}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Ionicons name={icon} size={24} color="#fff" />
</LinearGradient>
<Text style={styles.label}>{label}</Text>
<Ionicons name="chevron-forward" size={16} color={theme.colors.gray400} style={styles.arrow} />
</LinearGradient>
</TouchableOpacity>
);
}
export function QuickActionGrid() {
return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>Quick Actions</Text>
<View style={styles.grid}>
<QuickActionItem
icon="barbell"
label="Log Workout"
gradient={theme.gradients.primary}
/>
<QuickActionItem
icon="restaurant"
label="Track Meal"
gradient={theme.gradients.success}
/>
<QuickActionItem
icon="water"
label="Add Water"
gradient={theme.gradients.ocean}
/>
<QuickActionItem
icon="scan"
label="Scan Food"
gradient={theme.gradients.purple}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '700',
color: theme.colors.gray900,
marginBottom: 16,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 16,
},
itemContainer: {
width: ITEM_WIDTH,
},
item: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 20,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.6)',
},
iconContainer: {
width: 40,
height: 40,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
label: {
fontSize: 14,
fontWeight: '600',
color: theme.colors.gray800,
flex: 1,
},
arrow: {
opacity: 0.5,
},
});

View File

@ -34,8 +34,8 @@ export interface CreateGoalData {
} }
export class FitnessGoalsService { export class FitnessGoalsService {
private async getAuthHeaders(token: string | null): Promise<HeadersInit> { private async getAuthHeaders(token: string | null): Promise<any> {
const headers: HeadersInit = { const headers: any = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };

View File

@ -0,0 +1,191 @@
/**
* 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',
secondary: '#8b5cf6',
// 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),
},
};

View File

@ -0,0 +1,40 @@
import * as Haptics from 'expo-haptics';
import { Platform } from 'react-native';
export const haptics = {
light: () => {
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
},
medium: () => {
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
}
},
heavy: () => {
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
}
},
success: () => {
if (Platform.OS !== 'web') {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
},
warning: () => {
if (Platform.OS !== 'web') {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
}
},
error: () => {
if (Platform.OS !== 'web') {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
},
selection: () => {
if (Platform.OS !== 'web') {
Haptics.selectionAsync();
}
},
};