fitaiProto/apps/mobile/src/app/fitness-profile.tsx

760 lines
22 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
ActivityIndicator,
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 { useTheme } from "../contexts/ThemeContext";
import { MinimalCard } from "../components/MinimalCard";
import { MinimalButton } from "../components/MinimalButton";
import { fitnessProfileApi, type FitnessProfile } from "../api/fitnessProfile";
interface FitnessProfileData {
height?: number;
weight?: number;
age?: number;
gender?: string;
fitnessGoal?: string;
activityLevel?: string;
medicalConditions?: string;
allergies?: string;
injuries?: string;
}
const GENDER_OPTIONS = [
{ 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-outline",
},
];
const FITNESS_GOAL_OPTIONS = [
{
label: "Weight Loss",
value: "weight_loss",
icon: "trending-down",
color: "#FF3B3B",
},
{
label: "Muscle Gain",
value: "muscle_gain",
icon: "barbell",
color: "#0066FF",
},
{ label: "Endurance", value: "endurance", icon: "bicycle", color: "#00D26A" },
{
label: "Flexibility",
value: "flexibility",
icon: "body",
color: "#7B2CBF",
},
{
label: "General Fitness",
value: "general_fitness",
icon: "fitness",
color: "#FFB800",
},
];
const ACTIVITY_LEVEL_OPTIONS = [
{
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() {
const router = useRouter();
const { colors, typography } = useTheme();
const { userId, getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [fetchingProfile, setFetchingProfile] = useState(true);
const [profileData, setProfileData] = useState<FitnessProfileData>({});
useEffect(() => {
fetchProfile();
}, []);
const fetchProfile = async () => {
try {
setFetchingProfile(true);
const token = await getToken();
if (!token || !userId) {
return;
}
const profile = await fitnessProfileApi.getFitnessProfile(token);
if (profile) {
let activityLevel = profile.activityLevel || "";
if (activityLevel === "light") activityLevel = "lightly_active";
if (activityLevel === "moderate") activityLevel = "moderately_active";
if (activityLevel === "active") activityLevel = "very_active";
setProfileData({
height: profile.height,
weight: profile.weight,
age: profile.age,
gender: profile.gender || "",
fitnessGoal: Array.isArray(profile.fitnessGoals)
? profile.fitnessGoals[0]
: "",
activityLevel: activityLevel,
medicalConditions: profile.medicalConditions || "",
allergies: profile.allergies || "",
injuries: profile.injuries || "",
});
}
} catch (error) {
console.error("Error fetching profile:", error);
} finally {
setFetchingProfile(false);
}
};
const handleSave = async () => {
try {
setLoading(true);
const token = await getToken();
if (!token || !userId) {
Alert.alert("Error", "Authentication required");
return;
}
const dataToSave: Omit<FitnessProfile, "id"> = {
userId: userId,
height: profileData.height,
weight: profileData.weight,
age: profileData.age,
gender: (profileData.gender as FitnessProfile["gender"]) || undefined,
fitnessGoals: profileData.fitnessGoal ? [profileData.fitnessGoal] : [],
activityLevel:
(profileData.activityLevel as FitnessProfile["activityLevel"]) ||
undefined,
medicalConditions: profileData.medicalConditions,
allergies: profileData.allergies,
injuries: profileData.injuries,
};
await fitnessProfileApi.createFitnessProfile(dataToSave, token);
Alert.alert("Success", "Fitness profile saved successfully!", [
{ text: "OK", onPress: () => router.back() },
]);
} catch (error) {
console.error("Error saving profile:", error);
Alert.alert("Error", "Failed to save fitness profile");
} finally {
setLoading(false);
}
};
const updateField = (field: keyof FitnessProfileData, value: any) => {
setProfileData((prev) => ({ ...prev, [field]: value }));
};
if (fetchingProfile) {
return (
<View
style={[
styles.loadingContainer,
{ backgroundColor: colors.background },
]}
>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
return (
<>
<Stack.Screen options={{ headerShown: false }} />
<View style={[styles.container, { backgroundColor: colors.background }]}>
{/* 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={[typography.h2, { color: "#fff" }]}>
Fitness Profile
</Text>
<View style={{ width: 40 }} />
</View>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Basic Information */}
<View style={styles.section}>
<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={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
HEIGHT (CM)
</Text>
<View
style={[
styles.inputContainer,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
},
]}
>
<Ionicons
name="resize"
size={20}
color={colors.textTertiary}
/>
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
value={profileData.height?.toString() || ""}
onChangeText={(text) =>
updateField(
"height",
text ? parseFloat(text) : undefined,
)
}
keyboardType="decimal-pad"
placeholder="175"
placeholderTextColor={colors.textTertiary}
/>
</View>
</View>
<View style={styles.inputGroup}>
<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"
size={20}
color={colors.textTertiary}
/>
<TextInput
style={[styles.input, { color: colors.textPrimary }]}
value={profileData.weight?.toString() || ""}
onChangeText={(text) =>
updateField(
"weight",
text ? parseFloat(text) : undefined,
)
}
keyboardType="decimal-pad"
placeholder="70"
placeholderTextColor={colors.textTertiary}
/>
</View>
</View>
</View>
<View style={styles.inputGroup}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
AGE
</Text>
<View
style={[
styles.inputContainer,
{
backgroundColor: colors.surfaceElevated,
borderColor: colors.border,
},
]}
>
<Ionicons
name="calendar"
size={20}
color={colors.textTertiary}
/>
<TextInput
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={colors.textTertiary}
/>
</View>
</View>
</MinimalCard>
</View>
{/* Gender Selection */}
<View style={styles.section}>
<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.genderCard,
{
backgroundColor:
profileData.gender === option.value
? colors.primary
: colors.surfaceElevated,
borderColor:
profileData.gender === option.value
? colors.primary
: colors.border,
},
]}
onPress={() => updateField("gender", option.value)}
>
<Ionicons
name={option.icon as any}
size={24}
color={
profileData.gender === option.value
? "#fff"
: colors.textTertiary
}
/>
<Text
style={[
typography.caption,
{
color:
profileData.gender === option.value
? "#fff"
: colors.textSecondary,
fontWeight:
profileData.gender === option.value ? "700" : "500",
marginTop: 6,
},
]}
>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Fitness Goal */}
<View style={styles.section}>
<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
style={styles.listItem}
onPress={() => updateField("fitnessGoal", option.value)}
>
<View
style={[
styles.iconCircle,
{ backgroundColor: `${option.color}20` },
]}
>
<Ionicons
name={option.icon as any}
size={20}
color={option.color}
/>
</View>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary, flex: 1 },
]}
>
{option.label}
</Text>
{profileData.fitnessGoal === option.value && (
<Ionicons
name="checkmark-circle"
size={24}
color={colors.primary}
/>
)}
</TouchableOpacity>
{index < FITNESS_GOAL_OPTIONS.length - 1 && (
<View
style={[
styles.divider,
{ backgroundColor: colors.border },
]}
/>
)}
</React.Fragment>
))}
</MinimalCard>
</View>
{/* Activity Level */}
<View style={styles.section}>
<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
style={styles.listItem}
onPress={() => updateField("activityLevel", option.value)}
>
<View style={{ flex: 1 }}>
<Text
style={[
typography.bodyEmphasis,
{ color: colors.textPrimary },
]}
>
{option.label}
</Text>
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
>
{option.description}
</Text>
</View>
{profileData.activityLevel === option.value && (
<Ionicons
name="checkmark-circle"
size={24}
color={colors.primary}
/>
)}
</TouchableOpacity>
{index < ACTIVITY_LEVEL_OPTIONS.length - 1 && (
<View
style={[
styles.divider,
{ backgroundColor: colors.border },
]}
/>
)}
</React.Fragment>
))}
</MinimalCard>
</View>
{/* Health Information */}
<View style={styles.section}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary, marginBottom: 16 },
]}
>
Health Information
</Text>
<MinimalCard variant="elevated" style={styles.card}>
<View style={styles.inputGroup}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
MEDICAL CONDITIONS
</Text>
<TextInput
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={colors.textTertiary}
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
<View style={styles.inputGroup}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
ALLERGIES
</Text>
<TextInput
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={colors.textTertiary}
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
<View style={styles.inputGroup}>
<Text
style={[
typography.label,
{ color: colors.textSecondary, marginBottom: 8 },
]}
>
INJURIES
</Text>
<TextInput
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={colors.textTertiary}
multiline
numberOfLines={3}
textAlignVertical="top"
/>
</View>
</MinimalCard>
</View>
<View style={{ height: 120 }} />
</ScrollView>
{/* Save Button */}
<View
style={[
styles.footer,
{
backgroundColor: colors.background,
borderTopColor: colors.border,
},
]}
>
<MinimalButton
title="Save Profile"
onPress={handleSave}
variant="primary"
size="xl"
fullWidth
loading={loading}
/>
</View>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingBottom: 20,
paddingHorizontal: 20,
},
backButton: {
width: 40,
height: 40,
borderRadius: 12,
backgroundColor: "rgba(255, 255, 255, 0.2)",
justifyContent: "center",
alignItems: "center",
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: 20,
paddingBottom: 100,
},
section: {
marginBottom: 28,
},
card: {
padding: 4,
},
inputRow: {
flexDirection: "row",
gap: 12,
},
inputGroup: {
flex: 1,
marginBottom: 4,
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
borderRadius: 14,
borderWidth: 1.5,
paddingHorizontal: 14,
gap: 10,
},
input: {
flex: 1,
paddingVertical: 14,
fontSize: 16,
fontWeight: "600",
},
textArea: {
borderRadius: 14,
borderWidth: 1.5,
padding: 14,
fontSize: 16,
minHeight: 90,
},
genderRow: {
flexDirection: "row",
gap: 10,
},
genderCard: {
flex: 1,
paddingVertical: 16,
paddingHorizontal: 8,
borderRadius: 14,
borderWidth: 2,
alignItems: "center",
},
listItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 14,
paddingHorizontal: 12,
gap: 14,
},
iconCircle: {
width: 44,
height: 44,
borderRadius: 12,
justifyContent: "center",
alignItems: "center",
},
divider: {
height: 1,
marginLeft: 58,
},
footer: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
padding: 20,
paddingBottom: 34,
borderTopWidth: 1,
},
});