diff --git a/apps/admin/data/fitai.db b/apps/admin/data/fitai.db index 40053c3..17fee76 100755 Binary files a/apps/admin/data/fitai.db and b/apps/admin/data/fitai.db differ diff --git a/apps/mobile/src/app/(tabs)/profile.tsx b/apps/mobile/src/app/(tabs)/profile.tsx index 36c6f89..2166788 100644 --- a/apps/mobile/src/app/(tabs)/profile.tsx +++ b/apps/mobile/src/app/(tabs)/profile.tsx @@ -1,5 +1,6 @@ import { View, Text, StyleSheet, TouchableOpacity, Image, Alert } from "react-native"; import { useUser, useClerk } from "@clerk/clerk-expo"; +import { useRouter } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { LinearGradient } from "expo-linear-gradient"; import { theme } from "../../styles/theme"; @@ -9,6 +10,7 @@ import { GradientBackground } from "../../components/GradientBackground"; export default function ProfileScreen() { const { user } = useUser(); const { signOut } = useClerk(); + const router = useRouter(); const handleSignOut = async () => { try { @@ -57,7 +59,7 @@ export default function ProfileScreen() { Account - + router.push('/personal-details')}> - + router.push('/fitness-profile')}> ({}); - const genderOptions = [ - { label: "Male", value: "male" }, - { label: "Female", value: "female" }, - { label: "Other", value: "other" }, - { label: "Prefer not to say", value: "prefer_not_to_say" }, - ]; - - const fitnessGoalOptions = [ - { label: "Weight Loss", value: "weight_loss" }, - { label: "Muscle Gain", value: "muscle_gain" }, - { label: "Endurance", value: "endurance" }, - { label: "Flexibility", value: "flexibility" }, - { label: "General Fitness", value: "general_fitness" }, - ]; - - const activityLevelOptions = [ - { label: "Sedentary", value: "sedentary" }, - { label: "Lightly Active", value: "lightly_active" }, - { label: "Moderately Active", value: "moderately_active" }, - { label: "Very Active", value: "very_active" }, - { label: "Extremely Active", value: "extremely_active" }, - ]; - useEffect(() => { fetchProfile(); }, []); @@ -65,8 +66,7 @@ export default function FitnessProfileScreen() { try { setFetchingProfile(true); const token = await getToken(); - const apiUrl = `${API_BASE_URL}` || "http://localhost:3000"; - const response = await fetch(`${apiUrl}/api/fitness-profile`, { + const response = await fetch(`${API_BASE_URL}/api/profile/fitness?userId=${userId}`, { headers: { Authorization: `Bearer ${token}`, }, @@ -79,17 +79,21 @@ export default function FitnessProfileScreen() { height: data.profile.height, weight: data.profile.weight, age: data.profile.age, - gender: data.profile.gender || "", - fitnessGoal: data.profile.fitnessGoal || "", - activityLevel: data.profile.activityLevel || "", - medicalConditions: data.profile.medicalConditions || "", - allergies: data.profile.allergies || "", - injuries: data.profile.injuries || "", + gender: data.profile.gender || '', + fitnessGoal: Array.isArray(data.profile.fitnessGoals) + ? data.profile.fitnessGoals[0] + : (typeof data.profile.fitnessGoals === 'string' + ? JSON.parse(data.profile.fitnessGoals)[0] + : ''), + activityLevel: data.profile.activityLevel || '', + medicalConditions: data.profile.medicalConditions || '', + allergies: data.profile.allergies || '', + injuries: data.profile.injuries || '', }); } } } catch (error) { - console.error("Error fetching profile:", error); + console.error('Error fetching profile:', error); } finally { setFetchingProfile(false); } @@ -99,28 +103,41 @@ export default function FitnessProfileScreen() { try { setLoading(true); const token = await getToken(); - const apiUrl = `${API_BASE_URL}/api/fitness-profile` || "http://localhost:3000"; - const response = await fetch(`${apiUrl}`, { - method: "POST", + // Prepare data with userId and convert fitnessGoal to fitnessGoals array + const dataToSave = { + userId: userId, + height: profileData.height, + weight: profileData.weight, + age: profileData.age, + gender: profileData.gender, + fitnessGoals: profileData.fitnessGoal ? [profileData.fitnessGoal] : [], + activityLevel: profileData.activityLevel, + medicalConditions: profileData.medicalConditions, + allergies: profileData.allergies, + injuries: profileData.injuries, + }; + + const response = await fetch(`${API_BASE_URL}/api/profile/fitness`, { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - body: JSON.stringify(profileData), + body: JSON.stringify(dataToSave), }); if (response.ok) { - Alert.alert("Success", "Fitness profile saved successfully!", [ - { text: "OK", onPress: () => router.back() }, + Alert.alert('Success', 'Fitness profile saved successfully!', [ + { text: 'OK', onPress: () => router.back() }, ]); } else { const error = await response.json(); - Alert.alert("Error", error.message || "Failed to save profile"); + Alert.alert('Error', error.error || 'Failed to save profile'); } } catch (error) { - console.error("Error saving profile:", error); - Alert.alert("Error", "Failed to save fitness profile"); + console.error('Error saving profile:', error); + Alert.alert('Error', 'Failed to save fitness profile'); } finally { setLoading(false); } @@ -133,142 +150,233 @@ export default function FitnessProfileScreen() { if (fetchingProfile) { return ( - + ); } return ( - - router.back()} - style={styles.backButton} - > - + {/* Header */} + + router.back()}> + Fitness Profile - - + + - - - Basic Information - - - updateField("height", text ? parseFloat(text) : undefined) - } - keyboardType="decimal-pad" - placeholder="e.g., 175" - /> - - - updateField("weight", text ? parseFloat(text) : undefined) - } - keyboardType="decimal-pad" - placeholder="e.g., 70" - /> - - - updateField("age", text ? parseInt(text, 10) : undefined) - } - keyboardType="number-pad" - placeholder="e.g., 25" - /> - - updateField("gender", value)} - items={genderOptions} - /> + {/* Basic Information */} + + Basic Information + + + + Height (cm) + + + + updateField('height', text ? parseFloat(text) : undefined) + } + keyboardType="decimal-pad" + placeholder="175" + placeholderTextColor={theme.colors.gray400} + /> + + + + Weight (kg) + + + + updateField('weight', text ? parseFloat(text) : undefined) + } + keyboardType="decimal-pad" + placeholder="70" + placeholderTextColor={theme.colors.gray400} + /> + + + + + Age + + + + updateField('age', text ? parseInt(text, 10) : undefined) + } + keyboardType="number-pad" + placeholder="25" + placeholderTextColor={theme.colors.gray400} + /> + + + - - Fitness Goals - - updateField("fitnessGoal", value)} - items={fitnessGoalOptions} - /> - - updateField("activityLevel", value)} - items={activityLevelOptions} - /> + {/* Gender Selection */} + + Gender + + {GENDER_OPTIONS.map((option) => ( + updateField('gender', option.value)} + > + + + {option.label} + + + ))} + - - Health Information - - updateField("medicalConditions", text)} - placeholder="e.g., Asthma, diabetes..." - multiline - numberOfLines={3} - style={styles.textArea} - /> - - updateField("allergies", text)} - placeholder="e.g., Peanuts, latex..." - multiline - numberOfLines={3} - style={styles.textArea} - /> - - updateField("injuries", text)} - placeholder="e.g., Previous knee injury..." - multiline - numberOfLines={3} - style={styles.textArea} - /> + {/* Fitness Goal */} + + Primary Fitness Goal + + {FITNESS_GOAL_OPTIONS.map((option, index) => ( + + updateField('fitnessGoal', option.value)} + > + + + + {option.label} + {profileData.fitnessGoal === option.value && ( + + )} + + {index < FITNESS_GOAL_OPTIONS.length - 1 && } + + ))} + - + {/* Activity Level */} + + Activity Level + + {ACTIVITY_LEVEL_OPTIONS.map((option, index) => ( + + updateField('activityLevel', option.value)} + > + + {option.label} + {option.description} + + {profileData.activityLevel === option.value && ( + + )} + + {index < ACTIVITY_LEVEL_OPTIONS.length - 1 && } + + ))} + + + + {/* Health Information */} + + Health Information (Optional) + + + Medical Conditions + updateField('medicalConditions', text)} + placeholder="e.g., Asthma, diabetes..." + placeholderTextColor={theme.colors.gray400} + multiline + numberOfLines={3} + textAlignVertical="top" + /> + + + Allergies + updateField('allergies', text)} + placeholder="e.g., Peanuts, latex..." + placeholderTextColor={theme.colors.gray400} + multiline + numberOfLines={3} + textAlignVertical="top" + /> + + + Injuries + updateField('injuries', text)} + placeholder="e.g., Previous knee injury..." + placeholderTextColor={theme.colors.gray400} + multiline + numberOfLines={3} + textAlignVertical="top" + /> + + + + + + {/* Save Button */} + + + {loading ? ( - + ) : ( <> - + Save Profile )} - - - + + + ); } @@ -276,73 +384,185 @@ export default function FitnessProfileScreen() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#f9fafb", + backgroundColor: theme.colors.background, }, loadingContainer: { flex: 1, - justifyContent: "center", - alignItems: "center", - backgroundColor: "#f9fafb", + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.background, }, header: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingHorizontal: 16, - paddingTop: 60, - paddingBottom: 16, - backgroundColor: "white", - borderBottomWidth: 1, - borderBottomColor: "#e5e7eb", + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: Platform.OS === 'ios' ? 60 : 40, + paddingBottom: 20, + paddingHorizontal: 20, }, backButton: { - padding: 4, + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + justifyContent: 'center', + alignItems: 'center', }, headerTitle: { - fontSize: 18, - fontWeight: "600", - color: "#1f2937", - }, - headerSpacer: { - width: 32, + fontSize: theme.typography.fontSize['2xl'], + fontWeight: theme.typography.fontWeight.bold, + color: '#fff', }, scrollView: { flex: 1, }, - content: { + scrollContent: { padding: 20, + paddingBottom: 100, }, section: { marginBottom: 24, }, sectionTitle: { - fontSize: 16, - fontWeight: "600", - color: "#1f2937", + fontSize: theme.typography.fontSize.lg, + fontWeight: theme.typography.fontWeight.bold, + color: theme.colors.gray900, + marginBottom: 12, + }, + card: { + backgroundColor: '#fff', + borderRadius: theme.borderRadius.xl, + padding: 16, + ...theme.shadows.subtle, + borderWidth: 1, + borderColor: theme.colors.gray100, + }, + row: { + flexDirection: 'row', + gap: 12, marginBottom: 16, }, + inputGroup: { + flex: 1, + marginBottom: 16, + }, + label: { + fontSize: theme.typography.fontSize.sm, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray700, + marginBottom: 8, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.gray50, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.gray200, + paddingHorizontal: 12, + gap: 8, + }, + input: { + flex: 1, + paddingVertical: 12, + fontSize: theme.typography.fontSize.base, + color: theme.colors.gray900, + }, textArea: { - height: 80, - textAlignVertical: "top", - paddingTop: 12, + backgroundColor: theme.colors.gray50, + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.gray200, + padding: 12, + fontSize: theme.typography.fontSize.base, + color: theme.colors.gray900, + minHeight: 80, + }, + optionsRow: { + flexDirection: 'row', + gap: 12, + }, + optionCard: { + flex: 1, + backgroundColor: '#fff', + borderRadius: theme.borderRadius.lg, + padding: 16, + alignItems: 'center', + gap: 8, + borderWidth: 2, + borderColor: theme.colors.gray200, + ...theme.shadows.subtle, + }, + optionCardActive: { + borderColor: theme.colors.primary, + backgroundColor: `${theme.colors.primary}10`, + }, + optionLabel: { + fontSize: theme.typography.fontSize.sm, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.gray600, + }, + optionLabelActive: { + color: theme.colors.primary, + fontWeight: theme.typography.fontWeight.bold, + }, + listItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + gap: 12, + }, + iconCircle: { + width: 40, + height: 40, + borderRadius: 20, + justifyContent: 'center', + alignItems: 'center', + }, + listItemText: { + flex: 1, + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.medium, + color: theme.colors.gray900, + }, + listItemDescription: { + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray500, + marginTop: 2, + }, + divider: { + height: 1, + backgroundColor: theme.colors.gray100, + }, + footer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + padding: 20, + paddingBottom: Platform.OS === 'ios' ? 40 : 20, + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: theme.colors.gray100, + ...theme.shadows.medium, }, saveButton: { - backgroundColor: "#2563eb", - borderRadius: 8, - padding: 16, - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - marginTop: 8, - marginBottom: 40, + borderRadius: theme.borderRadius.lg, + overflow: 'hidden', + }, + saveButtonGradient: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 16, + gap: 8, }, saveButtonDisabled: { opacity: 0.6, }, saveButtonText: { - color: "white", - fontSize: 16, - fontWeight: "600", - marginLeft: 8, + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.bold, + color: '#fff', }, }); diff --git a/apps/mobile/src/app/personal-details.tsx b/apps/mobile/src/app/personal-details.tsx new file mode 100644 index 0000000..4ec3b51 --- /dev/null +++ b/apps/mobile/src/app/personal-details.tsx @@ -0,0 +1,266 @@ +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + TextInput, + Alert, + Platform, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { useUser } from '@clerk/clerk-expo'; +import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import { theme } from '../styles/theme'; + +export default function PersonalDetailsScreen() { + const router = useRouter(); + const { user } = useUser(); + const [loading, setLoading] = useState(false); + + // Initialize with current user data + const [formData, setFormData] = useState({ + firstName: user?.firstName || '', + lastName: user?.lastName || '', + email: user?.primaryEmailAddress?.emailAddress || '', + phone: user?.primaryPhoneNumber?.phoneNumber || '', + }); + + const handleSave = async () => { + setLoading(true); + try { + // Update user profile via Clerk + await user?.update({ + firstName: formData.firstName, + lastName: formData.lastName, + }); + + Alert.alert('Success', 'Personal details updated successfully', [ + { text: 'OK', onPress: () => router.back() }, + ]); + } catch (error) { + console.error('Error updating personal details:', error); + Alert.alert('Error', 'Failed to update personal details. Please try again.'); + } finally { + setLoading(false); + } + }; + + const updateField = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + return ( + + {/* Header */} + + router.back()} + > + + + Personal Details + + + + + {/* First Name */} + + First Name * + + + updateField('firstName', value)} + placeholder="Enter first name" + placeholderTextColor={theme.colors.gray400} + /> + + + + {/* Last Name */} + + Last Name * + + + updateField('lastName', value)} + placeholder="Enter last name" + placeholderTextColor={theme.colors.gray400} + /> + + + + {/* Email (Read-only) */} + + Email + + + + + + Email cannot be changed here + + + {/* Phone (Read-only for now) */} + + Phone Number + + + + + + Phone number cannot be changed here + + + + {/* Save Button */} + + + + + + {loading ? 'Saving...' : 'Save Changes'} + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: Platform.OS === 'ios' ? 60 : 40, + paddingBottom: 20, + paddingHorizontal: 20, + }, + backButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + justifyContent: 'center', + alignItems: 'center', + }, + headerTitle: { + fontSize: theme.typography.fontSize['2xl'], + fontWeight: theme.typography.fontWeight.bold, + color: '#fff', + }, + content: { + flex: 1, + }, + scrollContent: { + padding: 20, + paddingBottom: 100, + }, + field: { + marginBottom: 24, + }, + label: { + fontSize: theme.typography.fontSize.sm, + fontWeight: theme.typography.fontWeight.semibold, + color: theme.colors.gray700, + marginBottom: 8, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#fff', + borderRadius: theme.borderRadius.lg, + borderWidth: 1, + borderColor: theme.colors.gray200, + paddingHorizontal: 16, + ...theme.shadows.subtle, + }, + inputIcon: { + marginRight: 12, + }, + input: { + flex: 1, + paddingVertical: 16, + fontSize: theme.typography.fontSize.base, + color: theme.colors.gray900, + }, + disabledInput: { + backgroundColor: theme.colors.gray50, + }, + disabledText: { + color: theme.colors.gray500, + }, + helperText: { + fontSize: theme.typography.fontSize.xs, + color: theme.colors.gray500, + marginTop: 6, + marginLeft: 4, + }, + footer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + padding: 20, + paddingBottom: Platform.OS === 'ios' ? 40 : 20, + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: theme.colors.gray100, + ...theme.shadows.medium, + }, + saveButton: { + borderRadius: theme.borderRadius.lg, + overflow: 'hidden', + }, + saveButtonGradient: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 16, + gap: 8, + }, + saveButtonDisabled: { + opacity: 0.6, + }, + saveButtonText: { + fontSize: theme.typography.fontSize.base, + fontWeight: theme.typography.fontWeight.bold, + color: '#fff', + }, +});