fitness profile fix

This commit is contained in:
echo 2026-03-18 00:35:21 +01:00
parent 0776517fb7
commit 7f22a39886
3 changed files with 314 additions and 216 deletions

Binary file not shown.

View File

@ -96,9 +96,13 @@ export default function OnboardingScreen() {
const fitnessData = {
userId: user.id,
height: parseFloat(fitnessProfile.height),
weight: parseFloat(fitnessProfile.weight),
age: parseInt(fitnessProfile.age),
height: fitnessProfile.height
? parseFloat(fitnessProfile.height)
: undefined,
weight: fitnessProfile.weight
? parseFloat(fitnessProfile.weight)
: undefined,
age: fitnessProfile.age ? parseInt(fitnessProfile.age, 10) : undefined,
fitnessGoals: fitnessProfile.goals ? [fitnessProfile.goals] : [],
medicalConditions: fitnessProfile.medicalConditions || undefined,
allergies: fitnessProfile.dietaryRestrictions || undefined,

View File

@ -13,8 +13,10 @@ import {
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 { useTheme } from "../contexts/ThemeContext";
import { MinimalCard } from "../components/MinimalCard";
import { MinimalButton } from "../components/MinimalButton";
import { IconContainer } from "../components/IconContainer";
import { API_BASE_URL } from "../config/api";
interface FitnessProfileData {
@ -30,13 +32,13 @@ 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-outline" },
{ label: "Female", value: "female", icon: "female-outline" },
{ label: "Other", value: "other", icon: "transgender-outline" },
{
label: "Prefer not to say",
value: "prefer_not_to_say",
icon: "help-circle",
icon: "help-circle-outline",
},
];
@ -45,31 +47,26 @@ const FITNESS_GOAL_OPTIONS = [
label: "Weight Loss",
value: "weight_loss",
icon: "trending-down",
color: theme.colors.danger,
color: "#FF3B3B",
},
{
label: "Muscle Gain",
value: "muscle_gain",
icon: "barbell",
color: theme.colors.primary,
},
{
label: "Endurance",
value: "endurance",
icon: "bicycle",
color: theme.colors.success,
color: "#0066FF",
},
{ label: "Endurance", value: "endurance", icon: "bicycle", color: "#00D26A" },
{
label: "Flexibility",
value: "flexibility",
icon: "body",
color: theme.colors.purple,
color: "#7B2CBF",
},
{
label: "General Fitness",
value: "general_fitness",
icon: "fitness",
color: theme.colors.warning,
color: "#FFB800",
},
];
@ -99,6 +96,7 @@ const ACTIVITY_LEVEL_OPTIONS = [
export default function FitnessProfileScreen() {
const router = useRouter();
const { colors, typography } = useTheme();
const { userId, getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [fetchingProfile, setFetchingProfile] = useState(true);
@ -124,7 +122,6 @@ export default function FitnessProfileScreen() {
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";
@ -159,8 +156,6 @@ export default function FitnessProfileScreen() {
setLoading(true);
const token = await getToken();
// Prepare data with userId and convert fitnessGoal to fitnessGoals array
// Convert empty strings to undefined for optional enum fields
const dataToSave = {
userId: userId,
height: profileData.height,
@ -205,8 +200,13 @@ export default function FitnessProfileScreen() {
if (fetchingProfile) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<View
style={[
styles.loadingContainer,
{ backgroundColor: colors.background },
]}
>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
@ -214,18 +214,28 @@ export default function FitnessProfileScreen() {
return (
<>
<Stack.Screen options={{ headerShown: false }} />
<View style={styles.container}>
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* Header */}
<LinearGradient colors={theme.gradients.primary} style={styles.header}>
<View
style={[
styles.header,
{
backgroundColor: colors.primary,
paddingTop: Platform.OS === "ios" ? 60 : 40,
},
]}
>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color="#fff" />
</TouchableOpacity>
<Text style={styles.headerTitle}>Fitness Profile</Text>
<Text style={[typography.h2, { color: "#fff" }]}>
Fitness Profile
</Text>
<View style={{ width: 40 }} />
</LinearGradient>
</View>
<ScrollView
style={styles.scrollView}
@ -234,19 +244,41 @@ export default function FitnessProfileScreen() {
>
{/* Basic Information */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Basic Information</Text>
<View style={styles.card}>
<View style={styles.row}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Basic Information
</Text>
<MinimalCard variant="elevated" style={styles.card}>
<View style={styles.inputRow}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Height (cm)</Text>
<View style={styles.inputContainer}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
HEIGHT (CM)
</Text>
<View
style={[
styles.inputContainer,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
},
]}
>
<Ionicons
name="resize-outline"
name="resize"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
<TextInput
style={styles.input}
style={[styles.input, { color: colors.textPrimary }]}
value={profileData.height?.toString() || ""}
onChangeText={(text) =>
updateField(
@ -256,20 +288,35 @@ export default function FitnessProfileScreen() {
}
keyboardType="decimal-pad"
placeholder="175"
placeholderTextColor={theme.colors.gray400}
placeholderTextColor={colors.textTertiary}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Weight (kg)</Text>
<View style={styles.inputContainer}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
WEIGHT (KG)
</Text>
<View
style={[
styles.inputContainer,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
},
]}
>
<Ionicons
name="scale-outline"
name="scale"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
<TextInput
style={styles.input}
style={[styles.input, { color: colors.textPrimary }]}
value={profileData.weight?.toString() || ""}
onChangeText={(text) =>
updateField(
@ -279,45 +326,75 @@ export default function FitnessProfileScreen() {
}
keyboardType="decimal-pad"
placeholder="70"
placeholderTextColor={theme.colors.gray400}
placeholderTextColor={colors.textTertiary}
/>
</View>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Age</Text>
<View style={styles.inputContainer}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
AGE
</Text>
<View
style={[
styles.inputContainer,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
},
]}
>
<Ionicons
name="calendar-outline"
name="calendar"
size={20}
color={theme.colors.gray400}
color={colors.textTertiary}
/>
<TextInput
style={styles.input}
style={[styles.input, { color: colors.textPrimary }]}
value={profileData.age?.toString() || ""}
onChangeText={(text) =>
updateField("age", text ? parseInt(text, 10) : undefined)
}
keyboardType="number-pad"
placeholder="25"
placeholderTextColor={theme.colors.gray400}
placeholderTextColor={colors.textTertiary}
/>
</View>
</View>
</View>
</MinimalCard>
</View>
{/* Gender Selection */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Gender</Text>
<View style={styles.optionsRow}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Gender
</Text>
<View style={styles.genderRow}>
{GENDER_OPTIONS.map((option) => (
<TouchableOpacity
key={option.value}
style={[
styles.optionCard,
profileData.gender === option.value &&
styles.optionCardActive,
styles.genderCard,
{
backgroundColor:
profileData.gender === option.value
? colors.primary
: colors.surfaceElevated,
borderColor:
profileData.gender === option.value
? colors.primary
: colors.border,
},
]}
onPress={() => updateField("gender", option.value)}
>
@ -326,15 +403,22 @@ export default function FitnessProfileScreen() {
size={24}
color={
profileData.gender === option.value
? theme.colors.primary
: theme.colors.gray400
? "#fff"
: colors.textTertiary
}
/>
<Text
style={[
styles.optionLabel,
profileData.gender === option.value &&
styles.optionLabelActive,
typography.caption,
{
color:
profileData.gender === option.value
? "#fff"
: colors.textSecondary,
fontWeight:
profileData.gender === option.value ? "700" : "500",
marginTop: 6,
},
]}
>
{option.label}
@ -346,8 +430,15 @@ export default function FitnessProfileScreen() {
{/* Fitness Goal */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Primary Fitness Goal</Text>
<View style={styles.card}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Primary Fitness Goal
</Text>
<MinimalCard variant="elevated" style={styles.card}>
{FITNESS_GOAL_OPTIONS.map((option, index) => (
<React.Fragment key={option.value}>
<TouchableOpacity
@ -366,27 +457,46 @@ export default function FitnessProfileScreen() {
color={option.color}
/>
</View>
<Text style={styles.listItemText}>{option.label}</Text>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary, flex: 1 },
]}
>
{option.label}
</Text>
{profileData.fitnessGoal === option.value && (
<Ionicons
name="checkmark-circle"
size={24}
color={theme.colors.primary}
color={colors.primary}
/>
)}
</TouchableOpacity>
{index < FITNESS_GOAL_OPTIONS.length - 1 && (
<View style={styles.divider} />
<View
style={[
styles.divider,
{ backgroundColor: colors.border },
]}
/>
)}
</React.Fragment>
))}
</View>
</MinimalCard>
</View>
{/* Activity Level */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Activity Level</Text>
<View style={styles.card}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Activity Level
</Text>
<MinimalCard variant="elevated" style={styles.card}>
{ACTIVITY_LEVEL_OPTIONS.map((option, index) => (
<React.Fragment key={option.value}>
<TouchableOpacity
@ -394,8 +504,20 @@ export default function FitnessProfileScreen() {
onPress={() => updateField("activityLevel", option.value)}
>
<View style={{ flex: 1 }}>
<Text style={styles.listItemText}>{option.label}</Text>
<Text style={styles.listItemDescription}>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary },
]}
>
{option.label}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{option.description}
</Text>
</View>
@ -403,90 +525,141 @@ export default function FitnessProfileScreen() {
<Ionicons
name="checkmark-circle"
size={24}
color={theme.colors.primary}
color={colors.primary}
/>
)}
</TouchableOpacity>
{index < ACTIVITY_LEVEL_OPTIONS.length - 1 && (
<View style={styles.divider} />
<View
style={[
styles.divider,
{ backgroundColor: colors.border },
]}
/>
)}
</React.Fragment>
))}
</View>
</MinimalCard>
</View>
{/* Health Information */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>
Health Information (Optional)
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Health Information
</Text>
<View style={styles.card}>
<MinimalCard variant="elevated" style={styles.card}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Medical Conditions</Text>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
MEDICAL CONDITIONS
</Text>
<TextInput
style={[styles.textArea]}
style={[
styles.textArea,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={profileData.medicalConditions || ""}
onChangeText={(text) =>
updateField("medicalConditions", text)
}
placeholder="e.g., Asthma, diabetes..."
placeholderTextColor={theme.colors.gray400}
placeholderTextColor={colors.textTertiary}
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Allergies</Text>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
ALLERGIES
</Text>
<TextInput
style={[styles.textArea]}
style={[
styles.textArea,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={profileData.allergies || ""}
onChangeText={(text) => updateField("allergies", text)}
placeholder="e.g., Peanuts, latex..."
placeholderTextColor={theme.colors.gray400}
placeholderTextColor={colors.textTertiary}
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Injuries</Text>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
INJURIES
</Text>
<TextInput
style={[styles.textArea]}
style={[
styles.textArea,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={profileData.injuries || ""}
onChangeText={(text) => updateField("injuries", text)}
placeholder="e.g., Previous knee injury..."
placeholderTextColor={theme.colors.gray400}
placeholderTextColor={colors.textTertiary}
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
</MinimalCard>
</View>
</View>
<View style={{ height: 120 }} />
</ScrollView>
{/* Save Button */}
<View style={styles.footer}>
<TouchableOpacity
style={[styles.saveButton, loading && styles.saveButtonDisabled]}
<View
style={[
styles.footer,
{
backgroundColor: colors.background,
borderTopColor: colors.border,
},
]}
>
<MinimalButton
title="Save Profile"
onPress={handleSave}
disabled={loading}
>
<LinearGradient
colors={theme.gradients.primary}
style={styles.saveButtonGradient}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.saveButtonText}>Save Profile</Text>
</>
)}
</LinearGradient>
</TouchableOpacity>
variant="primary"
size="xl"
fullWidth
loading={loading}
/>
</View>
</View>
</>
@ -496,35 +669,27 @@ export default function FitnessProfileScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
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,
borderRadius: 12,
backgroundColor: "rgba(255, 255, 255, 0.2)",
justifyContent: "center",
alignItems: "center",
},
headerTitle: {
fontSize: theme.typography.fontSize["2xl"],
fontWeight: theme.typography.fontWeight.bold,
color: "#fff",
},
scrollView: {
flex: 1,
},
@ -533,118 +698,69 @@ const styles = StyleSheet.create({
paddingBottom: 100,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray900,
marginBottom: 12,
marginBottom: 28,
},
card: {
backgroundColor: "#fff",
borderRadius: theme.borderRadius.xl,
padding: 16,
...theme.shadows.subtle,
borderWidth: 1,
borderColor: theme.colors.gray100,
padding: 4,
},
row: {
inputRow: {
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,
marginBottom: 4,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: theme.colors.gray50,
borderRadius: theme.borderRadius.lg,
borderWidth: 1,
borderColor: theme.colors.gray200,
paddingHorizontal: 12,
gap: 8,
borderRadius: 14,
borderWidth: 1.5,
paddingHorizontal: 14,
gap: 10,
},
input: {
flex: 1,
paddingVertical: 12,
fontSize: theme.typography.fontSize.base,
color: theme.colors.gray900,
paddingVertical: 14,
fontSize: 16,
fontWeight: "600",
},
textArea: {
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,
borderRadius: 14,
borderWidth: 1.5,
padding: 14,
fontSize: 16,
minHeight: 90,
},
optionsRow: {
genderRow: {
flexDirection: "row",
gap: 12,
gap: 10,
},
optionCard: {
genderCard: {
flex: 1,
backgroundColor: "#fff",
borderRadius: theme.borderRadius.lg,
padding: 16,
alignItems: "center",
gap: 8,
paddingVertical: 16,
paddingHorizontal: 8,
borderRadius: 14,
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,
alignItems: "center",
},
listItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
gap: 12,
paddingVertical: 14,
paddingHorizontal: 12,
gap: 14,
},
iconCircle: {
width: 40,
height: 40,
borderRadius: 20,
width: 44,
height: 44,
borderRadius: 12,
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,
marginLeft: 58,
},
footer: {
position: "absolute",
@ -652,29 +768,7 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
padding: 20,
paddingBottom: Platform.OS === "ios" ? 40 : 20,
backgroundColor: "#fff",
paddingBottom: 34,
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",
},
});