fitaiProto/apps/mobile/src/app/(auth)/onboarding.tsx

459 lines
13 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
Alert,
ActivityIndicator,
} from "react-native";
import { useUser, useAuth } from "@clerk/clerk-expo";
import { useRouter } from "expo-router";
import { fitnessProfileApi } from "@/api/fitnessProfile";
import { gymsApi, type Gym } from "@/api/gyms";
import log from "../../utils/logger";
export default function OnboardingScreen() {
const { user } = useUser();
const { getToken } = useAuth();
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [gyms, setGyms] = useState<Gym[]>([]);
const [gymsLoading, setGymsLoading] = useState<boolean>(false);
const [selectedGymId, setSelectedGymId] = useState<string | null>(null);
useEffect(() => {
const loadGyms = async () => {
try {
setGymsLoading(true);
const token = await getToken();
const data = await gymsApi.getGyms(token);
setGyms(data);
} catch (e) {
log.error("Failed to fetch gyms", e);
} finally {
setGymsLoading(false);
}
};
loadGyms();
}, []);
const [fitnessProfile, setFitnessProfile] = useState({
height: "",
weight: "",
age: "",
goals: "",
fitnessLevel: "beginner",
medicalConditions: "",
dietaryRestrictions: "",
preferredWorkoutTime: "morning",
workoutFrequency: "3",
});
const handleSubmit = async () => {
if (!user?.id) return;
try {
setIsSubmitting(true);
// Validate required fields
if (
!fitnessProfile.height ||
!fitnessProfile.weight ||
!fitnessProfile.age
) {
Alert.alert("Error", "Please enter your height, weight, and age");
return;
}
const token = await getToken();
if (!token) {
throw new Error("Authentication token not available");
}
// If gym was selected or cleared, patch user's gym selection first
// selectedGymId: string gym id, or null to proceed without gym
try {
await gymsApi.updateUserGym(selectedGymId, token);
} catch (e) {
log.warn("Failed to update gym selection", { gymId: selectedGymId });
}
const fitnessData = {
userId: user.id,
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,
};
await fitnessProfileApi.createFitnessProfile(fitnessData, token);
router.replace("/(tabs)");
} catch (error) {
log.error("Failed to create fitness profile", error);
Alert.alert(
"Error",
"Failed to create fitness profile. Please try again.",
);
} finally {
setIsSubmitting(false);
}
};
const handleLevelSelect = (level: string) => {
setFitnessProfile({ ...fitnessProfile, fitnessLevel: level });
};
const handleTimeSelect = (time: string) => {
setFitnessProfile({ ...fitnessProfile, preferredWorkoutTime: time });
};
// Calculate progress based on filled fields
const calculateProgress = () => {
const fields = [
fitnessProfile.height,
fitnessProfile.weight,
fitnessProfile.age,
fitnessProfile.goals,
fitnessProfile.medicalConditions || "n/a", // Optional fields count as filled
fitnessProfile.dietaryRestrictions || "n/a",
];
const filledFields = fields.filter(
(field) => field && field.trim() !== "",
).length;
return Math.round((filledFields / fields.length) * 100);
};
const progress = calculateProgress();
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>Set Up Your Fitness Profile</Text>
<Text style={styles.subtitle}>
Help us personalize your fitness journey
</Text>
{/* Progress indicator */}
<View style={styles.progressContainer}>
<View style={styles.progressBarBackground}>
<View style={[styles.progressBarFill, { width: `${progress}%` }]} />
</View>
<Text style={styles.progressText}>{progress}% Complete</Text>
</View>
<View style={styles.form}>
<Text style={styles.label}>Height (cm)</Text>
<TextInput
style={styles.input}
value={fitnessProfile.height}
onChangeText={(value) =>
setFitnessProfile({ ...fitnessProfile, height: value })
}
keyboardType="numeric"
placeholder="Enter height in cm"
/>
<Text style={styles.label}>Weight (kg)</Text>
<TextInput
style={styles.input}
value={fitnessProfile.weight}
onChangeText={(value) =>
setFitnessProfile({ ...fitnessProfile, weight: value })
}
keyboardType="numeric"
placeholder="Enter weight in kg"
/>
<Text style={styles.label}>Age</Text>
<TextInput
style={styles.input}
value={fitnessProfile.age}
onChangeText={(value) =>
setFitnessProfile({ ...fitnessProfile, age: value })
}
keyboardType="numeric"
placeholder="Enter your age"
/>
<Text style={styles.label}>Fitness Goals</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={fitnessProfile.goals}
onChangeText={(value) =>
setFitnessProfile({ ...fitnessProfile, goals: value })
}
multiline
numberOfLines={3}
placeholder="What are your fitness goals?"
/>
<Text style={styles.label}>Fitness Level</Text>
<View style={styles.buttonGroup}>
{["beginner", "intermediate", "advanced"].map((level) => (
<TouchableOpacity
key={level}
style={[
styles.levelButton,
fitnessProfile.fitnessLevel === level && styles.selectedButton,
]}
onPress={() => handleLevelSelect(level)}
>
<Text
style={[
styles.levelButtonText,
fitnessProfile.fitnessLevel === level &&
styles.selectedButtonText,
]}
>
{level.charAt(0).toUpperCase() + level.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.label}>Medical Conditions</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={fitnessProfile.medicalConditions}
onChangeText={(value) =>
setFitnessProfile({ ...fitnessProfile, medicalConditions: value })
}
multiline
numberOfLines={3}
placeholder="Any medical conditions we should know about?"
/>
<Text style={styles.label}>Dietary Restrictions</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={fitnessProfile.dietaryRestrictions}
onChangeText={(value) =>
setFitnessProfile({
...fitnessProfile,
dietaryRestrictions: value,
})
}
multiline
numberOfLines={3}
placeholder="Any dietary restrictions?"
/>
<Text style={styles.label}>Preferred Workout Time</Text>
<View style={styles.buttonGroup}>
{["morning", "afternoon", "evening"].map((time) => (
<TouchableOpacity
key={time}
style={[
styles.timeButton,
fitnessProfile.preferredWorkoutTime === time &&
styles.selectedButton,
]}
onPress={() => handleTimeSelect(time)}
>
<Text
style={[
styles.timeButtonText,
fitnessProfile.preferredWorkoutTime === time &&
styles.selectedButtonText,
]}
>
{time.charAt(0).toUpperCase() + time.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.label}>Workouts per Week</Text>
<TextInput
style={styles.input}
value={fitnessProfile.workoutFrequency}
onChangeText={(value) =>
setFitnessProfile({ ...fitnessProfile, workoutFrequency: value })
}
keyboardType="numeric"
placeholder="Number of workouts per week"
/>
<Text style={styles.label}>Select a Gym</Text>
{gymsLoading ? (
<ActivityIndicator />
) : (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ marginBottom: 16 }}
>
<View style={{ flexDirection: "row" }}>
<TouchableOpacity
style={[
styles.levelButton,
selectedGymId === null && styles.selectedButton,
]}
onPress={() => setSelectedGymId(null)}
>
<Text
style={[
styles.levelButtonText,
selectedGymId === null && styles.selectedButtonText,
]}
>
Proceed without gym
</Text>
</TouchableOpacity>
{gyms.map((gym) => (
<TouchableOpacity
key={gym.id}
style={[
styles.levelButton,
selectedGymId === gym.id && styles.selectedButton,
]}
onPress={() => setSelectedGymId(gym.id)}
>
<Text
style={[
styles.levelButtonText,
selectedGymId === gym.id && styles.selectedButtonText,
]}
>
{gym.name}
</Text>
</TouchableOpacity>
))}
</View>
</ScrollView>
)}
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.submitButtonText}>Complete Setup</Text>
)}
</TouchableOpacity>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f5f5f5",
},
title: {
fontSize: 24,
fontWeight: "bold",
textAlign: "center",
marginTop: 40,
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: "#666",
textAlign: "center",
marginBottom: 32,
},
form: {
padding: 20,
},
label: {
fontSize: 14,
fontWeight: "600",
color: "#374151",
marginBottom: 4,
},
input: {
borderWidth: 1,
borderColor: "#ddd",
borderRadius: 8,
padding: 12,
marginBottom: 16,
backgroundColor: "white",
},
textArea: {
height: 80,
textAlignVertical: "top",
},
buttonGroup: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 16,
},
levelButton: {
flex: 1,
backgroundColor: "#f3f4f6",
padding: 10,
borderRadius: 8,
marginHorizontal: 4,
alignItems: "center",
},
timeButton: {
flex: 1,
backgroundColor: "#f3f4f6",
padding: 10,
borderRadius: 8,
marginHorizontal: 4,
alignItems: "center",
},
selectedButton: {
backgroundColor: "#3b82f6",
},
levelButtonText: {
color: "#374151",
fontSize: 14,
fontWeight: "500",
},
timeButtonText: {
color: "#374151",
fontSize: 14,
fontWeight: "500",
},
selectedButtonText: {
color: "white",
},
submitButton: {
backgroundColor: "#3b82f6",
padding: 16,
borderRadius: 8,
alignItems: "center",
marginTop: 24,
},
submitButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
progressContainer: {
paddingHorizontal: 20,
marginBottom: 16,
},
progressBarBackground: {
height: 8,
backgroundColor: "#e5e7eb",
borderRadius: 4,
overflow: "hidden",
marginBottom: 8,
},
progressBarFill: {
height: "100%",
backgroundColor: "#3b82f6",
borderRadius: 4,
},
progressText: {
fontSize: 12,
color: "#6b7280",
textAlign: "center",
},
});