From 254a30ff93bce881ade7116ae1c33b765c1cd5a7 Mon Sep 17 00:00:00 2001 From: echo Date: Thu, 12 Mar 2026 16:45:38 +0100 Subject: [PATCH] fitness profile validation error fixed --- apps/mobile/src/app/fitness-profile.tsx | 308 ++++++++++++++++-------- 1 file changed, 208 insertions(+), 100 deletions(-) diff --git a/apps/mobile/src/app/fitness-profile.tsx b/apps/mobile/src/app/fitness-profile.tsx index dfcbc12..4252097 100644 --- a/apps/mobile/src/app/fitness-profile.tsx +++ b/apps/mobile/src/app/fitness-profile.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; import { View, Text, @@ -9,13 +9,13 @@ import { Alert, TextInput, Platform, -} from 'react-native'; -import { useRouter, Stack } from 'expo-router'; -import { useAuth } from '@clerk/clerk-expo'; -import { Ionicons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; -import { theme } from '../styles/theme'; -import { API_BASE_URL } from '../config/api'; +} from "react-native"; +import { useRouter, Stack } from "expo-router"; +import { useAuth } from "@clerk/clerk-expo"; +import { Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; +import { theme } from "../styles/theme"; +import { API_BASE_URL } from "../config/api"; interface FitnessProfileData { height?: number; @@ -30,25 +30,71 @@ interface FitnessProfileData { } const GENDER_OPTIONS = [ - { label: 'Male', value: 'male', icon: 'male' }, - { label: 'Female', value: 'female', icon: 'female' }, - { label: 'Other', value: 'other', icon: 'transgender' }, + { label: "Male", value: "male", icon: "male" }, + { label: "Female", value: "female", icon: "female" }, + { label: "Other", value: "other", icon: "transgender" }, + { + label: "Prefer not to say", + value: "prefer_not_to_say", + icon: "help-circle", + }, ]; const FITNESS_GOAL_OPTIONS = [ - { label: 'Weight Loss', value: 'weight_loss', icon: 'trending-down', color: theme.colors.danger }, - { label: 'Muscle Gain', value: 'muscle_gain', icon: 'barbell', color: theme.colors.primary }, - { label: 'Endurance', value: 'endurance', icon: 'bicycle', color: theme.colors.success }, - { label: 'Flexibility', value: 'flexibility', icon: 'body', color: theme.colors.purple }, - { label: 'General Fitness', value: 'general_fitness', icon: 'fitness', color: theme.colors.warning }, + { + label: "Weight Loss", + value: "weight_loss", + icon: "trending-down", + color: theme.colors.danger, + }, + { + label: "Muscle Gain", + value: "muscle_gain", + icon: "barbell", + color: theme.colors.primary, + }, + { + label: "Endurance", + value: "endurance", + icon: "bicycle", + color: theme.colors.success, + }, + { + label: "Flexibility", + value: "flexibility", + icon: "body", + color: theme.colors.purple, + }, + { + label: "General Fitness", + value: "general_fitness", + icon: "fitness", + color: theme.colors.warning, + }, ]; const ACTIVITY_LEVEL_OPTIONS = [ - { label: 'Sedentary', value: 'sedentary', description: 'Little to no exercise' }, - { label: 'Light', value: 'light', description: '1-3 days/week' }, - { label: 'Moderate', value: 'moderate', description: '3-5 days/week' }, - { label: 'Active', value: 'active', description: '6-7 days/week' }, - { label: 'Very Active', value: 'very_active', description: 'Intense daily training' }, + { + label: "Sedentary", + value: "sedentary", + description: "Little to no exercise", + }, + { + label: "Lightly Active", + value: "lightly_active", + description: "1-3 days/week", + }, + { + label: "Moderately Active", + value: "moderately_active", + description: "3-5 days/week", + }, + { label: "Very Active", value: "very_active", description: "6-7 days/week" }, + { + label: "Extremely Active", + value: "extremely_active", + description: "Intense daily training", + }, ]; export default function FitnessProfileScreen() { @@ -66,34 +112,43 @@ export default function FitnessProfileScreen() { try { setFetchingProfile(true); const token = await getToken(); - const response = await fetch(`${API_BASE_URL}/api/profile/fitness?userId=${userId}`, { - headers: { - Authorization: `Bearer ${token}`, + const response = await fetch( + `${API_BASE_URL}/api/profile/fitness?userId=${userId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, }, - }); + ); if (response.ok) { const data = await response.json(); if (data.profile) { + // Normalize old activity level values to new schema + let activityLevel = data.profile.activityLevel || ""; + if (activityLevel === "light") activityLevel = "lightly_active"; + if (activityLevel === "moderate") activityLevel = "moderately_active"; + if (activityLevel === "active") activityLevel = "very_active"; + setProfileData({ height: data.profile.height, weight: data.profile.weight, age: data.profile.age, - gender: data.profile.gender || '', + gender: data.profile.gender || "", fitnessGoal: Array.isArray(data.profile.fitnessGoals) ? data.profile.fitnessGoals[0] - : (typeof data.profile.fitnessGoals === 'string' + : 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 || '', + : "", + activityLevel: 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); } @@ -119,25 +174,25 @@ export default function FitnessProfileScreen() { }; const response = await fetch(`${API_BASE_URL}/api/profile/fitness`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, 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.error || '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); } @@ -161,7 +216,10 @@ export default function FitnessProfileScreen() { {/* Header */} - router.back()}> + router.back()} + > Fitness Profile @@ -181,12 +239,19 @@ export default function FitnessProfileScreen() { Height (cm) - + - updateField('height', text ? parseFloat(text) : undefined) + updateField( + "height", + text ? parseFloat(text) : undefined, + ) } keyboardType="decimal-pad" placeholder="175" @@ -197,12 +262,19 @@ export default function FitnessProfileScreen() { Weight (kg) - + - updateField('weight', text ? parseFloat(text) : undefined) + updateField( + "weight", + text ? parseFloat(text) : undefined, + ) } keyboardType="decimal-pad" placeholder="70" @@ -214,12 +286,16 @@ export default function FitnessProfileScreen() { Age - + - updateField('age', text ? parseInt(text, 10) : undefined) + updateField("age", text ? parseInt(text, 10) : undefined) } keyboardType="number-pad" placeholder="25" @@ -239,9 +315,10 @@ export default function FitnessProfileScreen() { key={option.value} style={[ styles.optionCard, - profileData.gender === option.value && styles.optionCardActive, + profileData.gender === option.value && + styles.optionCardActive, ]} - onPress={() => updateField('gender', option.value)} + onPress={() => updateField("gender", option.value)} > {option.label} @@ -273,17 +351,32 @@ export default function FitnessProfileScreen() { updateField('fitnessGoal', option.value)} + onPress={() => updateField("fitnessGoal", option.value)} > - - + + {option.label} {profileData.fitnessGoal === option.value && ( - + )} - {index < FITNESS_GOAL_OPTIONS.length - 1 && } + {index < FITNESS_GOAL_OPTIONS.length - 1 && ( + + )} ))} @@ -297,17 +390,25 @@ export default function FitnessProfileScreen() { updateField('activityLevel', option.value)} + onPress={() => updateField("activityLevel", option.value)} > {option.label} - {option.description} + + {option.description} + {profileData.activityLevel === option.value && ( - + )} - {index < ACTIVITY_LEVEL_OPTIONS.length - 1 && } + {index < ACTIVITY_LEVEL_OPTIONS.length - 1 && ( + + )} ))} @@ -315,14 +416,18 @@ export default function FitnessProfileScreen() { {/* Health Information */} - Health Information (Optional) + + Health Information (Optional) + Medical Conditions updateField('medicalConditions', text)} + value={profileData.medicalConditions || ""} + onChangeText={(text) => + updateField("medicalConditions", text) + } placeholder="e.g., Asthma, diabetes..." placeholderTextColor={theme.colors.gray400} multiline @@ -334,8 +439,8 @@ export default function FitnessProfileScreen() { Allergies updateField('allergies', text)} + value={profileData.allergies || ""} + onChangeText={(text) => updateField("allergies", text)} placeholder="e.g., Peanuts, latex..." placeholderTextColor={theme.colors.gray400} multiline @@ -347,8 +452,8 @@ export default function FitnessProfileScreen() { Injuries updateField('injuries', text)} + value={profileData.injuries || ""} + onChangeText={(text) => updateField("injuries", text)} placeholder="e.g., Previous knee injury..." placeholderTextColor={theme.colors.gray400} multiline @@ -367,7 +472,10 @@ export default function FitnessProfileScreen() { onPress={handleSave} disabled={loading} > - + {loading ? ( ) : ( @@ -391,15 +499,15 @@ const styles = StyleSheet.create({ }, loadingContainer: { flex: 1, - justifyContent: 'center', - alignItems: 'center', + justifyContent: "center", + alignItems: "center", backgroundColor: theme.colors.background, }, header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingTop: Platform.OS === 'ios' ? 60 : 40, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingTop: Platform.OS === "ios" ? 60 : 40, paddingBottom: 20, paddingHorizontal: 20, }, @@ -407,14 +515,14 @@ const styles = StyleSheet.create({ width: 40, height: 40, borderRadius: 20, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - justifyContent: 'center', - alignItems: 'center', + backgroundColor: "rgba(255, 255, 255, 0.2)", + justifyContent: "center", + alignItems: "center", }, headerTitle: { - fontSize: theme.typography.fontSize['2xl'], + fontSize: theme.typography.fontSize["2xl"], fontWeight: theme.typography.fontWeight.bold, - color: '#fff', + color: "#fff", }, scrollView: { flex: 1, @@ -433,7 +541,7 @@ const styles = StyleSheet.create({ marginBottom: 12, }, card: { - backgroundColor: '#fff', + backgroundColor: "#fff", borderRadius: theme.borderRadius.xl, padding: 16, ...theme.shadows.subtle, @@ -441,7 +549,7 @@ const styles = StyleSheet.create({ borderColor: theme.colors.gray100, }, row: { - flexDirection: 'row', + flexDirection: "row", gap: 12, marginBottom: 16, }, @@ -456,8 +564,8 @@ const styles = StyleSheet.create({ marginBottom: 8, }, inputContainer: { - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", backgroundColor: theme.colors.gray50, borderRadius: theme.borderRadius.lg, borderWidth: 1, @@ -482,15 +590,15 @@ const styles = StyleSheet.create({ minHeight: 80, }, optionsRow: { - flexDirection: 'row', + flexDirection: "row", gap: 12, }, optionCard: { flex: 1, - backgroundColor: '#fff', + backgroundColor: "#fff", borderRadius: theme.borderRadius.lg, padding: 16, - alignItems: 'center', + alignItems: "center", gap: 8, borderWidth: 2, borderColor: theme.colors.gray200, @@ -510,8 +618,8 @@ const styles = StyleSheet.create({ fontWeight: theme.typography.fontWeight.bold, }, listItem: { - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", paddingVertical: 12, gap: 12, }, @@ -519,8 +627,8 @@ const styles = StyleSheet.create({ width: 40, height: 40, borderRadius: 20, - justifyContent: 'center', - alignItems: 'center', + justifyContent: "center", + alignItems: "center", }, listItemText: { flex: 1, @@ -538,25 +646,25 @@ const styles = StyleSheet.create({ backgroundColor: theme.colors.gray100, }, footer: { - position: 'absolute', + position: "absolute", bottom: 0, left: 0, right: 0, padding: 20, - paddingBottom: Platform.OS === 'ios' ? 40 : 20, - backgroundColor: '#fff', + paddingBottom: Platform.OS === "ios" ? 40 : 20, + backgroundColor: "#fff", borderTopWidth: 1, borderTopColor: theme.colors.gray100, ...theme.shadows.medium, }, saveButton: { borderRadius: theme.borderRadius.lg, - overflow: 'hidden', + overflow: "hidden", }, saveButtonGradient: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', + flexDirection: "row", + alignItems: "center", + justifyContent: "center", paddingVertical: 16, gap: 8, }, @@ -566,6 +674,6 @@ const styles = StyleSheet.create({ saveButtonText: { fontSize: theme.typography.fontSize.base, fontWeight: theme.typography.fontWeight.bold, - color: '#fff', + color: "#fff", }, });