diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db
index f1ddbc5..acfe75f 100644
Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ
diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json
index 477f759..320fb1d 100644
--- a/apps/mobile/package-lock.json
+++ b/apps/mobile/package-lock.json
@@ -23,6 +23,7 @@
"expo-constants": "^18.0.10",
"expo-crypto": "^15.0.7",
"expo-font": "~14.0.9",
+ "expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-notifications": "~0.32.0",
@@ -36,6 +37,7 @@
"react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-svg": "^15.15.0",
"react-native-web": "^0.21.2",
"zod": "^3.22.0"
},
@@ -5596,6 +5598,12 @@
"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": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -6268,6 +6276,56 @@
"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": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -6493,6 +6551,61 @@
"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": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -6580,6 +6693,18 @@
"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": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -7135,6 +7260,15 @@
"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",
@@ -9878,6 +10012,12 @@
"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": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -10475,6 +10615,18 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -11496,6 +11648,21 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 2b487b5..8f5c5d5 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -29,6 +29,7 @@
"expo-constants": "^18.0.10",
"expo-crypto": "^15.0.7",
"expo-font": "~14.0.9",
+ "expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-notifications": "~0.32.0",
@@ -42,6 +43,7 @@
"react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-svg": "^15.15.0",
"react-native-web": "^0.21.2",
"zod": "^3.22.0"
},
diff --git a/apps/mobile/src/app/(tabs)/_layout.tsx b/apps/mobile/src/app/(tabs)/_layout.tsx
index 4c9aa08..e8697a6 100644
--- a/apps/mobile/src/app/(tabs)/_layout.tsx
+++ b/apps/mobile/src/app/(tabs)/_layout.tsx
@@ -1,7 +1,7 @@
import { Tabs, useRouter, useSegments } from "expo-router";
-import { Ionicons } from "@expo/vector-icons";
import { useAuth } from "@clerk/clerk-expo";
import { useEffect } from "react";
+import { CustomTabBar } from "../../components/CustomTabBar";
export default function TabLayout() {
const { isSignedIn, isLoaded } = useAuth();
@@ -25,56 +25,40 @@ export default function TabLayout() {
return (
}
screenOptions={{
- tabBarActiveTintColor: "#2563eb",
- tabBarInactiveTintColor: "#6b7280",
- headerShown: true,
- headerStyle: {
- backgroundColor: "#fff",
- },
- headerTitleStyle: {
- fontWeight: "600",
- },
+ headerShown: false, // We'll use custom headers in screens or layout
+ tabBarShowLabel: false,
}}
>
(
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
- (
-
- ),
}}
/>
(
-
- ),
+ }}
+ />
+
+
+
diff --git a/apps/mobile/src/app/(tabs)/goals.tsx b/apps/mobile/src/app/(tabs)/goals.tsx
index b141fce..a264f63 100644
--- a/apps/mobile/src/app/(tabs)/goals.tsx
+++ b/apps/mobile/src/app/(tabs)/goals.tsx
@@ -1,117 +1,81 @@
-import { useState, useEffect, useRef } from "react";
-import {
- View,
- Text,
- StyleSheet,
- ScrollView,
- TouchableOpacity,
- ActivityIndicator,
- RefreshControl,
- Alert,
- Animated,
-} from "react-native";
-import { useAuth } from "@clerk/clerk-expo";
-import { Ionicons } from "@expo/vector-icons";
-import { LinearGradient } from "expo-linear-gradient";
-import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from "../../services/fitnessGoals";
-import { GoalProgressCard } from "../../components/GoalProgressCard";
-import { GoalCreationModal } from "../../components/GoalCreationModal";
-import { theme } from "../../styles/theme";
+import React, { useState, useCallback, useRef } from 'react';
+import { View, Text, StyleSheet, ScrollView, RefreshControl, TouchableOpacity, Animated, Alert } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { LinearGradient } from 'expo-linear-gradient';
+import { theme } from '../../styles/theme';
+import { GoalProgressCard } from '../../components/GoalProgressCard';
+import { GoalCreationModal } from '../../components/GoalCreationModal';
+import { useUser, useAuth } from "@clerk/clerk-expo";
+import { fitnessGoalsService, type FitnessGoal, type CreateGoalData } from '../../services/fitnessGoals';
+import { useFocusEffect } from 'expo-router';
export default function GoalsScreen() {
- const { userId, getToken } = useAuth();
+ const { user } = useUser();
+ const { getToken } = useAuth();
const [goals, setGoals] = useState([]);
- const [loading, setLoading] = useState(true);
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 () => {
- if (!userId) return;
+ const loadGoals = useCallback(async () => {
+ if (!user?.id) return;
try {
const token = await getToken();
- const data = await fitnessGoalsService.getGoals(userId, token);
- setGoals(data);
+ const loadedGoals = await fitnessGoalsService.getGoals(user.id, token);
+ setGoals(loadedGoals);
} catch (error) {
- console.error("Error fetching fitness goals:", error);
- Alert.alert("Error", "Failed to load goals. Please try again.");
- } finally {
- setLoading(false);
- setRefreshing(false);
+ console.error('Error loading goals:', error);
}
- };
+ }, [user?.id]); // Removed getToken from dependencies
- useEffect(() => {
- fetchGoals();
- }, [userId]);
+ useFocusEffect(
+ useCallback(() => {
+ loadGoals();
+ }, [loadGoals])
+ );
- const onRefresh = () => {
+ const onRefresh = async () => {
setRefreshing(true);
- fetchGoals();
+ await loadGoals();
+ setRefreshing(false);
};
- const handleCreateGoal = async (goalData: CreateGoalData) => {
- try {
- const token = await getToken();
- const newGoal = await fitnessGoalsService.createGoal(goalData, token);
- setGoals(prev => [newGoal, ...prev]);
- Alert.alert("Success", "Goal created successfully!");
- } catch (error) {
- console.error("Error creating goal:", error);
- throw error;
- }
+ const handleCreateGoal = async (newGoal: CreateGoalData) => {
+ const token = await getToken();
+ await fitnessGoalsService.createGoal(newGoal, token);
+ await loadGoals();
+ setIsModalVisible(false);
};
- const handleCompleteGoal = async (goalId: string) => {
- try {
- const token = await getToken();
- const updatedGoal = await fitnessGoalsService.completeGoal(goalId, token);
- 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 handleCompleteGoal = async (goal: FitnessGoal) => {
+ const token = await getToken();
+ await fitnessGoalsService.completeGoal(goal.id, token);
+ await loadGoals();
};
const handleDeleteGoal = async (goalId: string) => {
- try {
- const token = await getToken();
- await fitnessGoalsService.deleteGoal(goalId, token);
- 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 token = await getToken();
+ await fitnessGoalsService.deleteGoal(goalId, token);
+ await loadGoals();
};
const activeGoals = goals.filter(g => g.status === 'active');
const completedGoals = goals.filter(g => g.status === 'completed');
- if (loading && !refreshing) {
- return (
-
-
-
- );
- }
-
return (
+
}
>
- My Fitness Goals
+ Fitness Goals
Track your fitness journey progress
@@ -133,7 +97,7 @@ export default function GoalsScreen() {
{activeGoals.length > 0
? Math.round(
- activeGoals.reduce((sum, g) => sum + g.progress, 0) /
+ activeGoals.reduce((sum, g) => sum + (g.progress || 0), 0) /
activeGoals.length
)
: 0}%
@@ -161,7 +125,7 @@ export default function GoalsScreen() {
handleCompleteGoal(goal.id)}
+ onComplete={() => handleCompleteGoal(goal)}
onDelete={() => handleDeleteGoal(goal.id)}
/>
))
@@ -188,9 +152,9 @@ export default function GoalsScreen() {
{/* Floating Action Button */}
-
+
setShowCreateModal(true)}
+ onPress={() => setIsModalVisible(true)}
onPressIn={() => {
Animated.spring(fabScale, {
toValue: 0.9,
@@ -222,8 +186,8 @@ export default function GoalsScreen() {
{/* Create Goal Modal */}
setShowCreateModal(false)}
+ visible={isModalVisible}
+ onClose={() => setIsModalVisible(false)}
onSubmit={handleCreateGoal}
/>
@@ -235,10 +199,8 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: theme.colors.background,
},
- center: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
+ scrollContent: {
+ paddingBottom: 20,
},
header: {
padding: 24,
@@ -268,7 +230,7 @@ const styles = StyleSheet.create({
backgroundColor: theme.colors.white,
padding: 16,
borderRadius: theme.borderRadius.xl,
- alignItems: "center",
+ alignItems: 'center',
...theme.shadows.medium,
borderWidth: 1,
borderColor: "rgba(59, 130, 246, 0.1)",
@@ -312,10 +274,12 @@ const styles = StyleSheet.create({
footer: {
height: 100,
},
- fab: {
+ fabContainer: {
position: "absolute",
right: 20,
- bottom: 20,
+ bottom: 110, // Adjusted for tab bar height
+ },
+ fab: {
width: 64,
height: 64,
borderRadius: 32,
diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx
index b905e83..9cb174d 100644
--- a/apps/mobile/src/app/(tabs)/index.tsx
+++ b/apps/mobile/src/app/(tabs)/index.tsx
@@ -1,239 +1,118 @@
-import React, { useEffect, useRef } from "react";
-import {
- View,
- Text,
- StyleSheet,
- ScrollView,
- TouchableOpacity,
- Animated,
-} from "react-native";
+import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native";
import { useUser } from "@clerk/clerk-expo";
-import { Ionicons } from "@expo/vector-icons";
-import { useRouter } from "expo-router";
import { LinearGradient } from "expo-linear-gradient";
+import { 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";
export default function HomeScreen() {
- const { user, isLoaded } = useUser();
- const router = useRouter();
- const fadeAnim = useRef(new Animated.Value(0)).current;
+ const { user } = useUser();
+ const [refreshing, setRefreshing] = useState(false);
- useEffect(() => {
- Animated.timing(fadeAnim, {
- toValue: 1,
- duration: 500,
- useNativeDriver: true,
- }).start();
+ const onRefresh = useCallback(() => {
+ setRefreshing(true);
+ setTimeout(() => {
+ setRefreshing(false);
+ }, 2000);
}, []);
- if (!isLoaded || !user) {
- return (
-
- Loading...
-
- );
- }
-
- const firstName = user.firstName || "User";
- const greeting = getGreeting();
+ const getGreeting = () => {
+ const hour = new Date().getHours();
+ if (hour < 12) return "Good Morning";
+ if (hour < 18) return "Good Afternoon";
+ return "Good Evening";
+ };
return (
-
-
- {/* Gradient Header */}
-
- {greeting}!
- {firstName}
-
-
- {/* Quick Stats with Glassmorphism */}
-
-
-
-
-
-
-
- 0
- This Month
-
-
-
-
-
-
-
-
- 0
- Day Streak
-
-
-
-
-
-
-
-
- 0
- Total Visits
-
-
-
- {/* Quick Actions */}
-
- Quick Actions
-
- router.push("/fitness-profile")}
- activeOpacity={0.7}
- >
-
-
-
-
- Fitness Profile
-
- Manage your fitness information
-
-
-
-
-
-
-
-
-
-
- Check In
-
- Start your workout session
-
-
-
-
-
-
-
-
-
-
- View Schedule
-
- Check your upcoming classes
-
-
-
-
-
-
-
-
-
-
- Payments
- View payment history
-
-
-
-
-
- {/* Membership Info with Gradient */}
-
- Membership
-
-
- Basic Plan
-
- Active
+
+
+ }
+ >
+ {/* Header Section */}
+
+
+ {getGreeting()},
+ {user?.firstName || "Athlete"}
+
+
+ {user?.imageUrl ? (
+
+ ) : (
+
+
-
-
- {user.primaryEmailAddress?.emailAddress}
-
-
- Member since {new Date(user.createdAt!).toLocaleDateString()}
-
-
-
-
- {/* Recent Activity */}
-
- Recent Activity
-
-
-
-
-
-
- No recent activity
-
- Check in to start tracking your workouts
-
+ )}
-
-
- );
-}
-function getGreeting(): string {
- const hour = new Date().getHours();
- if (hour < 12) return "Good morning";
- if (hour < 18) return "Good afternoon";
- return "Good evening";
+ {/* Activity Widget */}
+
+
+ {/* Quick Actions */}
+
+
+ {/* Recent Activity Section */}
+
+
+ Recent Activity
+ See All
+
+
+
+
+
+
+
+
+
+
+ Upper Body Power
+ Today, 10:00 AM
+
+ 45m
+
+
+
+
+
+
+
+
+
+ Morning Cardio
+ Yesterday, 7:30 AM
+
+ 30m
+
+
+
+
+ {/* Bottom Spacer for Tab Bar */}
+
+
+
+ );
}
const styles = StyleSheet.create({
@@ -241,167 +120,110 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: theme.colors.background,
},
- content: {
- paddingBottom: 20,
+ scrollContent: {
+ paddingTop: 60,
},
header: {
- padding: 24,
- paddingTop: 60,
- paddingBottom: 32,
- borderBottomLeftRadius: theme.borderRadius['2xl'],
- borderBottomRightRadius: theme.borderRadius['2xl'],
- marginBottom: 20,
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ paddingHorizontal: 24,
+ marginBottom: 32,
},
greeting: {
- fontSize: theme.typography.fontSize.base,
- color: "rgba(255, 255, 255, 0.9)",
+ fontSize: 16,
+ color: theme.colors.gray600,
+ fontWeight: "500",
marginBottom: 4,
},
name: {
- fontSize: theme.typography.fontSize['4xl'],
- fontWeight: theme.typography.fontWeight.bold,
- color: theme.colors.white,
- },
- statsContainer: {
- flexDirection: "row",
- justifyContent: "space-between",
- paddingHorizontal: 16,
- marginBottom: 24,
- gap: 12,
- },
- statCard: {
- flex: 1,
- backgroundColor: theme.colors.white,
- borderRadius: theme.borderRadius.xl,
- padding: 16,
- alignItems: "center",
- borderWidth: 1,
- borderColor: "rgba(255, 255, 255, 0.3)",
- },
- statIconContainer: {
- marginBottom: 8,
- },
- statIconGradient: {
- width: 48,
- height: 48,
- borderRadius: 24,
- justifyContent: "center",
- alignItems: "center",
- },
- statValue: {
- fontSize: theme.typography.fontSize['2xl'],
- fontWeight: theme.typography.fontWeight.bold,
+ fontSize: 32,
+ fontWeight: "800",
color: theme.colors.gray900,
- marginTop: 4,
- marginBottom: 4,
+ letterSpacing: -0.5,
},
- statLabel: {
- fontSize: theme.typography.fontSize.xs,
- color: theme.colors.gray600,
- textAlign: "center",
- fontWeight: theme.typography.fontWeight.medium,
+ avatarContainer: {
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.1,
+ shadowRadius: 12,
+ elevation: 5,
},
- section: {
- marginBottom: 24,
- paddingHorizontal: 20,
- },
- sectionTitle: {
- fontSize: theme.typography.fontSize.xl,
- fontWeight: theme.typography.fontWeight.semibold,
- color: theme.colors.gray900,
- marginBottom: 12,
- },
- actionButton: {
- flexDirection: "row",
- alignItems: "center",
- backgroundColor: theme.colors.white,
- borderRadius: theme.borderRadius.xl,
- padding: 16,
- marginBottom: 12,
- },
- actionIcon: {
+ avatar: {
width: 56,
height: 56,
- borderRadius: 28,
+ borderRadius: 20,
+ borderWidth: 2,
+ borderColor: "#fff",
+ },
+ placeholderAvatar: {
+ width: 56,
+ height: 56,
+ borderRadius: 20,
+ backgroundColor: theme.colors.primary,
justifyContent: "center",
alignItems: "center",
- marginRight: 12,
+ borderWidth: 2,
+ borderColor: "#fff",
},
- actionContent: {
- flex: 1,
+ section: {
+ paddingHorizontal: 20,
+ marginBottom: 24,
},
- actionTitle: {
- fontSize: theme.typography.fontSize.base,
- fontWeight: theme.typography.fontWeight.semibold,
- color: theme.colors.gray900,
- marginBottom: 2,
- },
- actionSubtitle: {
- fontSize: theme.typography.fontSize.sm,
- color: theme.colors.gray600,
- },
- membershipCard: {
- borderRadius: theme.borderRadius.xl,
- padding: 20,
- },
- membershipHeader: {
+ sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
- marginBottom: 12,
- },
- membershipType: {
- fontSize: theme.typography.fontSize.xl,
- fontWeight: theme.typography.fontWeight.semibold,
- color: theme.colors.white,
- },
- statusBadge: {
- backgroundColor: "rgba(255, 255, 255, 0.25)",
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: theme.borderRadius.lg,
- borderWidth: 1,
- borderColor: "rgba(255, 255, 255, 0.3)",
- },
- statusText: {
- fontSize: theme.typography.fontSize.xs,
- fontWeight: theme.typography.fontWeight.semibold,
- color: theme.colors.white,
- },
- membershipEmail: {
- fontSize: theme.typography.fontSize.sm,
- color: "rgba(255, 255, 255, 0.9)",
- marginBottom: 4,
- },
- membershipDate: {
- fontSize: theme.typography.fontSize.xs,
- color: "rgba(255, 255, 255, 0.7)",
- },
- emptyState: {
- backgroundColor: theme.colors.white,
- borderRadius: theme.borderRadius.xl,
- padding: 32,
- alignItems: "center",
- },
- emptyIconContainer: {
marginBottom: 16,
},
- emptyIconGradient: {
- width: 96,
- height: 96,
- borderRadius: 48,
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: "700",
+ color: theme.colors.gray900,
+ },
+ seeAll: {
+ fontSize: 14,
+ color: theme.colors.primary,
+ fontWeight: "600",
+ },
+ activityCard: {
+ gap: 12,
+ },
+ recentItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ padding: 16,
+ borderRadius: 20,
+ backgroundColor: "#fff",
+ borderWidth: 1,
+ borderColor: "rgba(255, 255, 255, 0.6)",
+ },
+ recentIconContainer: {
+ marginRight: 16,
+ },
+ recentIcon: {
+ width: 48,
+ height: 48,
+ borderRadius: 16,
justifyContent: "center",
alignItems: "center",
},
- emptyStateText: {
- fontSize: theme.typography.fontSize.base,
- fontWeight: theme.typography.fontWeight.semibold,
- color: theme.colors.gray700,
+ recentInfo: {
+ flex: 1,
+ },
+ recentTitle: {
+ fontSize: 16,
+ fontWeight: "600",
+ color: theme.colors.gray900,
marginBottom: 4,
},
- emptyStateSubtext: {
- fontSize: theme.typography.fontSize.sm,
+ recentSubtitle: {
+ fontSize: 12,
color: theme.colors.gray500,
- textAlign: "center",
+ },
+ recentValue: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: theme.colors.gray900,
},
});
diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx
index 9e096c1..36c6f89 100644
--- a/apps/mobile/src/app/(tabs)/profile.tsx
+++ b/apps/mobile/src/app/(tabs)/profile.tsx
@@ -1,235 +1,135 @@
-import React from "react";
-import {
- View,
- Text,
- StyleSheet,
- TouchableOpacity,
- Alert,
- ScrollView,
-} from "react-native";
-import { useUser, useAuth } from "@clerk/clerk-expo";
-import { useRouter } from "expo-router";
+import { View, Text, StyleSheet, TouchableOpacity, Image, Alert } from "react-native";
+import { useUser, useClerk } from "@clerk/clerk-expo";
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() {
const { user } = useUser();
- const { signOut } = useAuth();
- const router = useRouter();
+ const { signOut } = useClerk();
- const handleLogout = async () => {
- Alert.alert("Sign Out", "Are you sure you want to sign out?", [
- { text: "Cancel", style: "cancel" },
- {
- text: "Sign Out",
- style: "destructive",
- onPress: async () => {
- try {
- await signOut();
- router.replace("/(auth)/sign-in");
- } catch (error) {
- Alert.alert("Error", "Failed to sign out");
- }
- },
- },
- ]);
+ const handleSignOut = async () => {
+ try {
+ await signOut();
+ } catch (err) {
+ console.error("Error signing out:", err);
+ }
};
- if (!user) {
- return (
-
- Loading...
-
+ const confirmSignOut = () => {
+ Alert.alert(
+ "Sign Out",
+ "Are you sure you want to sign out?",
+ [
+ { text: "Cancel", style: "cancel" },
+ { text: "Sign Out", style: "destructive", onPress: handleSignOut },
+ ]
);
- }
+ };
return (
-
-
- {/* Profile Header with Gradient */}
-
+
+
+
- {user.imageUrl ? (
-
-
- {user.firstName?.charAt(0)}
- {user.lastName?.charAt(0)}
-
-
+ {user?.imageUrl ? (
+
) : (
-
+
+
+ )}
+
+
+
+
+ {user?.fullName || "User"}
+ {user?.primaryEmailAddress?.emailAddress}
+
+ Premium Member
+
+
+
+
+
+
+ Account
+
+
+
+
- )}
-
-
-
- {user.firstName} {user.lastName}
-
-
- {user.primaryEmailAddress?.emailAddress}
-
-
- {user.primaryPhoneNumber && (
-
- {user.primaryPhoneNumber.phoneNumber}
-
- )}
-
-
- {/* Account Information */}
-
- Account Information
-
-
-
-
-
-
-
- Email
-
-
- {user.primaryEmailAddress?.emailAddress}
-
-
-
- {user.primaryPhoneNumber && (
-
-
-
-
-
- Phone
-
-
- {user.primaryPhoneNumber.phoneNumber}
-
-
- )}
-
-
-
-
-
-
- Member Since
-
-
- {new Date(user.createdAt!).toLocaleDateString()}
-
-
-
-
-
-
-
-
- Email Verified
-
-
- {user.primaryEmailAddress?.verification?.status === "verified"
- ? "Yes"
- : "No"}
-
-
+ Personal Details
+
+
+
+
+
+
+
+ Fitness Profile
+
+
+
+
+
+
+
+ Notifications
+
+
- {/* Quick Actions */}
- Quick Actions
-
-
-
-
-
- Edit Profile
-
-
-
-
-
-
-
- Notifications
-
-
-
-
-
-
-
- Payment History
-
-
-
-
-
-
-
- Settings
-
-
+ Support
+
+
+
+
+
+ Help Center
+
+
+
+
+
+
+
+ Privacy & Security
+
+
+
- {/* Sign Out Button */}
-
-
-
- Sign Out
-
-
+ }
+ />
- {/* App Version */}
Version 1.0.0
-
+
);
}
@@ -238,134 +138,130 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: theme.colors.background,
},
- content: {
- padding: 20,
+ header: {
paddingTop: 60,
+ paddingBottom: 30,
+ borderBottomLeftRadius: theme.borderRadius.xl,
+ borderBottomRightRadius: theme.borderRadius.xl,
+ alignItems: 'center',
},
profileCard: {
- borderRadius: theme.borderRadius.xl,
- padding: 28,
- alignItems: "center",
- marginBottom: 24,
+ alignItems: 'center',
},
avatarContainer: {
+ position: 'relative',
marginBottom: 16,
},
avatar: {
- width: 96,
- height: 96,
- borderRadius: 48,
- justifyContent: "center",
- alignItems: "center",
- borderWidth: 3,
- borderColor: "rgba(255, 255, 255, 0.3)",
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ borderWidth: 4,
+ borderColor: 'rgba(255, 255, 255, 0.3)',
},
- avatarText: {
- fontSize: theme.typography.fontSize['4xl'],
- fontWeight: theme.typography.fontWeight.bold,
- color: theme.colors.white,
+ placeholderAvatar: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 4,
+ borderColor: 'rgba(255, 255, 255, 0.3)',
+ },
+ editBadge: {
+ position: 'absolute',
+ bottom: 0,
+ right: 0,
+ backgroundColor: theme.colors.white,
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ justifyContent: 'center',
+ alignItems: 'center',
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
},
name: {
fontSize: theme.typography.fontSize['2xl'],
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.white,
- marginBottom: 6,
- },
- email: {
- fontSize: theme.typography.fontSize.base,
- color: "rgba(255, 255, 255, 0.9)",
marginBottom: 4,
},
- phone: {
+ email: {
fontSize: theme.typography.fontSize.sm,
- color: "rgba(255, 255, 255, 0.8)",
+ color: 'rgba(255, 255, 255, 0.8)',
+ marginBottom: 12,
+ },
+ memberBadge: {
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderRadius: theme.borderRadius.full,
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.3)',
+ },
+ memberText: {
+ color: theme.colors.white,
+ fontSize: theme.typography.fontSize.xs,
+ fontWeight: theme.typography.fontWeight.bold,
+ letterSpacing: 0.5,
+ },
+ content: {
+ padding: 20,
+ marginTop: -20,
},
section: {
marginBottom: 24,
},
sectionTitle: {
- fontSize: theme.typography.fontSize.xl,
- fontWeight: theme.typography.fontWeight.semibold,
+ fontSize: theme.typography.fontSize.lg,
+ fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray900,
marginBottom: 12,
+ marginLeft: 4,
},
infoCard: {
backgroundColor: theme.colors.white,
borderRadius: theme.borderRadius.xl,
- padding: 16,
+ padding: 8,
+ borderWidth: 1,
+ borderColor: theme.colors.gray100,
},
infoRow: {
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingVertical: 14,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.gray200,
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
},
- infoLabel: {
- flexDirection: "row",
- alignItems: "center",
- gap: 10,
- },
- infoIconContainer: {
+ iconContainer: {
width: 36,
height: 36,
- borderRadius: 18,
- justifyContent: "center",
- alignItems: "center",
- },
- infoLabelText: {
- fontSize: theme.typography.fontSize.sm,
- color: theme.colors.gray700,
- fontWeight: theme.typography.fontWeight.medium,
- },
- infoValue: {
- fontSize: theme.typography.fontSize.sm,
- color: theme.colors.gray900,
- fontWeight: theme.typography.fontWeight.semibold,
- },
- actionButton: {
- flexDirection: "row",
- alignItems: "center",
- backgroundColor: theme.colors.white,
- borderRadius: theme.borderRadius.xl,
- padding: 16,
- marginBottom: 12,
- },
- actionIconContainer: {
- width: 44,
- height: 44,
- borderRadius: 22,
- justifyContent: "center",
- alignItems: "center",
+ borderRadius: 10,
+ justifyContent: 'center',
+ alignItems: 'center',
marginRight: 12,
},
- actionButtonText: {
+ infoLabel: {
flex: 1,
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray900,
fontWeight: theme.typography.fontWeight.medium,
},
- logoutButton: {
- paddingVertical: 16,
- borderRadius: theme.borderRadius.lg,
- alignItems: "center",
- justifyContent: "center",
- flexDirection: "row",
- gap: 8,
- marginTop: 8,
- marginBottom: 16,
+ divider: {
+ height: 1,
+ backgroundColor: theme.colors.gray100,
+ marginLeft: 60,
},
- logoutText: {
- color: theme.colors.white,
- fontSize: theme.typography.fontSize.base,
- fontWeight: theme.typography.fontWeight.semibold,
+ signOutButton: {
+ marginTop: 8,
},
version: {
- textAlign: "center",
+ textAlign: 'center',
+ marginTop: 24,
+ color: theme.colors.gray400,
fontSize: theme.typography.fontSize.xs,
- color: theme.colors.gray500,
- marginTop: 8,
- marginBottom: 20,
},
});
diff --git a/apps/mobile/src/components/ActivityWidget.tsx b/apps/mobile/src/components/ActivityWidget.tsx
new file mode 100644
index 0000000..3c1e387
--- /dev/null
+++ b/apps/mobile/src/components/ActivityWidget.tsx
@@ -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 (
+
+
+
+ Daily Activity
+
+
+
+
+
+
+
+
+ {steps.toLocaleString()}
+ Steps
+
+
+
+
+
+
+
+
+ {calories}
+ Kcal
+
+
+
+
+
+
+
+
+ {duration}m
+ Active
+
+
+
+ {/* Simple Bar Chart Visualization */}
+
+ {[0.4, 0.6, 0.3, 0.8, 0.5, 0.9, 0.7].map((height, index) => (
+
+
+
+ {['M', 'T', 'W', 'T', 'F', 'S', 'S'][index]}
+
+
+ ))}
+
+
+
+ );
+}
+
+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',
+ },
+});
diff --git a/apps/mobile/src/components/AnimatedButton.tsx b/apps/mobile/src/components/AnimatedButton.tsx
index 2e6687f..82d0955 100644
--- a/apps/mobile/src/components/AnimatedButton.tsx
+++ b/apps/mobile/src/components/AnimatedButton.tsx
@@ -15,11 +15,13 @@ import {
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from '../styles/theme';
+import { haptics } from '../utils/haptics';
interface AnimatedButtonProps {
- onPress: () => void;
title: string;
- variant?: 'primary' | 'secondary' | 'danger' | 'success';
+ onPress: () => void;
+ variant?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning';
+ size?: 'sm' | 'md' | 'lg';
loading?: boolean;
disabled?: boolean;
style?: ViewStyle;
@@ -28,109 +30,125 @@ interface AnimatedButtonProps {
}
export function AnimatedButton({
- onPress,
title,
+ onPress,
variant = 'primary',
+ size = 'md',
loading = false,
disabled = false,
style,
textStyle,
icon,
}: AnimatedButtonProps) {
- const scaleAnim = useRef(new Animated.Value(1)).current;
+ const scale = useRef(new Animated.Value(1)).current;
const handlePressIn = () => {
- Animated.spring(scaleAnim, {
+ Animated.spring(scale, {
toValue: 0.95,
- friction: 8,
- tension: 100,
useNativeDriver: true,
}).start();
};
const handlePressOut = () => {
- Animated.spring(scaleAnim, {
+ Animated.spring(scale, {
toValue: 1,
- friction: 8,
- tension: 100,
useNativeDriver: true,
}).start();
};
- const getGradientColors = (): readonly [string, string, ...string[]] => {
+ const handlePress = () => {
+ if (!disabled && !loading) {
+ haptics.light();
+ onPress();
+ }
+ };
+
+ const getGradientColors = () => {
+ if (disabled) return ['#9ca3af', '#6b7280'] as const;
switch (variant) {
- case 'primary':
- return theme.gradients.primary;
- case 'danger':
- return theme.gradients.danger;
+ case 'secondary':
+ return theme.gradients.purple;
case 'success':
return theme.gradients.success;
- case 'secondary':
- return [theme.colors.gray600, theme.colors.gray700] as const;
+ case 'danger':
+ return theme.gradients.danger;
+ case 'warning':
+ return theme.gradients.warning;
default:
return theme.gradients.primary;
}
};
- const getShadowStyle = () => {
- if (disabled) return {};
- switch (variant) {
- case 'danger':
- return theme.shadows.glowDanger;
+ const getSizeStyles = () => {
+ switch (size) {
+ case 'sm':
+ return { paddingVertical: 8, paddingHorizontal: 16, fontSize: 14 };
+ case 'lg':
+ return { paddingVertical: 16, paddingHorizontal: 32, fontSize: 18 };
default:
- return theme.shadows.glow;
+ return { paddingVertical: 12, paddingHorizontal: 24, fontSize: 16 };
}
};
+ const sizeStyles = getSizeStyles();
+
return (
-
-
+
+
{loading ? (
) : (
<>
- {icon}
- {title}
+ {icon && {icon}}
+
+ {title}
+
>
)}
-
-
+
+
);
}
const styles = StyleSheet.create({
- button: {
- paddingVertical: 16,
- paddingHorizontal: 24,
- borderRadius: theme.borderRadius.lg,
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ gradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
- gap: 8,
+ borderRadius: 12,
+ ...theme.shadows.medium,
},
text: {
- color: theme.colors.white,
- fontSize: theme.typography.fontSize.base,
- fontWeight: theme.typography.fontWeight.semibold,
- },
- disabled: {
- opacity: 0.5,
+ color: '#fff',
+ fontWeight: '600',
+ textAlign: 'center',
},
});
diff --git a/apps/mobile/src/components/CircularProgress.tsx b/apps/mobile/src/components/CircularProgress.tsx
new file mode 100644
index 0000000..61bfc62
--- /dev/null
+++ b/apps/mobile/src/components/CircularProgress.tsx
@@ -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 (
+
+
+
+
+ {Math.round(progress)}%
+
+
+
+ );
+}
diff --git a/apps/mobile/src/components/CustomTabBar.tsx b/apps/mobile/src/components/CustomTabBar.tsx
new file mode 100644
index 0000000..cad05e1
--- /dev/null
+++ b/apps/mobile/src/components/CustomTabBar.tsx
@@ -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 (
+
+
+ {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 (
+
+
+ {isFocused ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ paddingBottom: Platform.OS === 'ios' ? 30 : 20,
+ pointerEvents: 'box-none',
+ },
+ tabBar: {
+ flexDirection: 'row',
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
+ borderRadius: 35,
+ height: 70,
+ width: width - 40,
+ justifyContent: 'space-around',
+ alignItems: 'center',
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.5)',
+ paddingHorizontal: 10,
+ },
+ tabItem: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%',
+ },
+ iconContainer: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ alignItems: 'center',
+ justifyContent: 'center',
+ shadowColor: theme.colors.primary,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 4,
+ },
+});
diff --git a/apps/mobile/src/components/GoalCarousel.tsx b/apps/mobile/src/components/GoalCarousel.tsx
new file mode 100644
index 0000000..323f0a5
--- /dev/null
+++ b/apps/mobile/src/components/GoalCarousel.tsx
@@ -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 (
+
+ No active goals
+
+ );
+ }
+
+ return (
+
+
+ {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 (
+ onGoalPress(goal)}
+ >
+
+
+
+
+
+
+
+
+
+ {goal.priority.toUpperCase()}
+
+
+
+ {goal.title}
+ {goal.description}
+
+
+
+
+
+ {goal.currentValue} / {goal.targetValue}
+
+ {goal.unit}
+
+
+
+
+
+ Target: {goal.targetDate ? new Date(goal.targetDate).toLocaleDateString() : 'No date'}
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+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',
+ },
+});
diff --git a/apps/mobile/src/components/GoalCreationModal.tsx b/apps/mobile/src/components/GoalCreationModal.tsx
index d078578..8086ef9 100644
--- a/apps/mobile/src/components/GoalCreationModal.tsx
+++ b/apps/mobile/src/components/GoalCreationModal.tsx
@@ -8,6 +8,7 @@ import {
TouchableOpacity,
ScrollView,
Platform,
+ Alert,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker';
@@ -59,7 +60,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
const handleSubmit = async () => {
if (!title.trim()) {
- alert('Please enter a goal title');
+ Alert.alert('Error', 'Please enter a goal title');
return;
}
@@ -81,7 +82,7 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
onClose();
} catch (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 {
setSubmitting(false);
}
@@ -203,28 +204,31 @@ export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationMo
Target Date
setShowDatePicker(true)}
+ onPress={() => setShowDatePicker(!showDatePicker)}
>
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
-
+
{showDatePicker && (
- {
- setShowDatePicker(Platform.OS === 'ios');
- if (selectedDate) {
- setTargetDate(selectedDate);
- }
- }}
- minimumDate={new Date()}
- />
+
+ {
+ setShowDatePicker(Platform.OS === 'ios');
+ if (selectedDate) {
+ setTargetDate(selectedDate);
+ }
+ }}
+ minimumDate={new Date()}
+ themeVariant="light"
+ />
+
)}
{/* Priority */}
@@ -411,4 +415,12 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: '#fff',
},
+ datePickerContainer: {
+ backgroundColor: '#fff',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#d1d5db',
+ marginTop: 8,
+ overflow: 'hidden',
+ },
});
diff --git a/apps/mobile/src/components/ParallaxScrollView.tsx b/apps/mobile/src/components/ParallaxScrollView.tsx
new file mode 100644
index 0000000..221dd5a
--- /dev/null
+++ b/apps/mobile/src/components/ParallaxScrollView.tsx
@@ -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 (
+
+
+ {children}
+
+
+
+
+
+ {headerImage && (
+
+ )}
+
+
+
+ {headerContent}
+
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/apps/mobile/src/components/QuickActionGrid.tsx b/apps/mobile/src/components/QuickActionGrid.tsx
new file mode 100644
index 0000000..b8c1c58
--- /dev/null
+++ b/apps/mobile/src/components/QuickActionGrid.tsx
@@ -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 (
+
+
+
+
+
+ {label}
+
+
+
+ );
+}
+
+export function QuickActionGrid() {
+ return (
+
+ Quick Actions
+
+
+
+
+
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/apps/mobile/src/services/fitnessGoals.ts b/apps/mobile/src/services/fitnessGoals.ts
index 078d046..dc16dfc 100644
--- a/apps/mobile/src/services/fitnessGoals.ts
+++ b/apps/mobile/src/services/fitnessGoals.ts
@@ -34,8 +34,8 @@ export interface CreateGoalData {
}
export class FitnessGoalsService {
- private async getAuthHeaders(token: string | null): Promise {
- const headers: HeadersInit = {
+ private async getAuthHeaders(token: string | null): Promise {
+ const headers: any = {
'Content-Type': 'application/json',
};
diff --git a/apps/mobile/src/styles/theme.ts b/apps/mobile/src/styles/theme.ts
index da73eee..4a10c78 100644
--- a/apps/mobile/src/styles/theme.ts
+++ b/apps/mobile/src/styles/theme.ts
@@ -10,6 +10,7 @@ export const theme = {
primary: '#3b82f6',
primaryDark: '#2563eb',
primaryLight: '#60a5fa',
+ secondary: '#8b5cf6',
// Accent colors
purple: '#8b5cf6',
diff --git a/apps/mobile/src/utils/haptics.ts b/apps/mobile/src/utils/haptics.ts
new file mode 100644
index 0000000..1cdd672
--- /dev/null
+++ b/apps/mobile/src/utils/haptics.ts
@@ -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();
+ }
+ },
+};