welcome page

with profile form
This commit is contained in:
echo 2025-11-07 21:16:03 +01:00
parent 3a554ba434
commit e287e55f1f
6 changed files with 520 additions and 4 deletions

View File

@ -0,0 +1,66 @@
import { NextRequest, NextResponse } from 'next/server'
import { users, fitnessProfiles } from '../../../../lib/database'
export async function POST(request: NextRequest) {
try {
const profileData: FitnessProfile = await request.json()
if (!profileData.userId || !profileData.height || !profileData.weight || !profileData.age) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
// For demo purposes, we'll allow profile creation without strict user validation
// In production, you'd validate against a persistent database
console.log('Creating fitness profile for user ID:', profileData.userId)
const existingProfile = fitnessProfiles.find(p => p.userId === profileData.userId)
if (existingProfile) {
Object.assign(existingProfile, profileData)
} else {
fitnessProfiles.push(profileData)
}
return NextResponse.json(
{
message: 'Fitness profile saved successfully',
profile: profileData
},
{ status: 201 }
)
} catch (error) {
console.error('Fitness profile error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
if (userId) {
const profile = fitnessProfiles.find(p => p.userId === userId)
if (!profile) {
return NextResponse.json(
{ error: 'Profile not found' },
{ status: 404 }
)
}
return NextResponse.json({ profile })
}
return NextResponse.json({ profiles: fitnessProfiles })
} catch (error) {
console.error('Get fitness profiles error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@ -18,6 +18,20 @@ export interface Client {
joinDate: Date
}
export interface FitnessProfile {
userId: string
height: string
weight: string
age: string
gender: 'male' | 'female' | 'other'
activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'
fitnessGoals: string[]
exerciseHabits: string
dietHabits: string
medicalConditions: string
}
// In-memory database
export const users: User[] = []
export const clients: Client[] = []
export const fitnessProfiles: FitnessProfile[] = []

View File

@ -25,9 +25,31 @@ export default function LoginScreen() {
if (response.data.user) {
await SecureStore.setItemAsync('user', JSON.stringify(response.data.user))
// Check if user has completed fitness profile
try {
const profileResponse = await axios.get(
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}?userId=${response.data.user.id}`
)
if (profileResponse.data.profile) {
// User has profile, go to main app
Alert.alert('Success', 'Login successful!', [
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
])
} else {
// New user, go to welcome page
Alert.alert('Welcome!', 'Let\'s set up your fitness profile', [
{ text: 'OK', onPress: () => router.replace('/welcome') }
])
}
} catch (profileError) {
// Profile doesn't exist or server error, treat as new user
console.log('Profile check failed:', profileError)
Alert.alert('Welcome!', 'Let\'s set up your fitness profile', [
{ text: 'OK', onPress: () => router.replace('/welcome') }
])
}
}
} catch (error: any) {
Alert.alert('Error', error.response?.data?.error || 'Login failed')

View File

@ -0,0 +1,408 @@
import React, { useState } from 'react'
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ScrollView } from 'react-native'
import { useRouter } from 'expo-router'
import axios from 'axios'
import * as SecureStore from 'expo-secure-store'
import { API_BASE_URL, API_ENDPOINTS } from '../config/api'
interface FitnessProfile {
height: string
weight: string
age: string
gender: 'male' | 'female' | 'other'
activityLevel: 'sedentary' | 'light' | 'moderate' | 'active' | 'very_active'
fitnessGoals: string[]
exerciseHabits: string
dietHabits: string
medicalConditions: string
}
export default function WelcomeScreen() {
const [profile, setProfile] = useState<FitnessProfile>({
height: '',
weight: '',
age: '',
gender: 'male',
activityLevel: 'moderate',
fitnessGoals: [],
exerciseHabits: '',
dietHabits: '',
medicalConditions: '',
})
const [loading, setLoading] = useState(false)
const router = useRouter()
const fitnessGoalsOptions = [
'Weight Loss',
'Muscle Gain',
'Improve Endurance',
'Better Flexibility',
'General Fitness',
'Strength Training',
'Cardio Health'
]
const activityLevels = [
{ value: 'sedentary', label: 'Sedentary (little or no exercise)' },
{ value: 'light', label: 'Light (1-3 days/week)' },
{ value: 'moderate', label: 'Moderate (3-5 days/week)' },
{ value: 'active', label: 'Active (6-7 days/week)' },
{ value: 'very_active', label: 'Very Active (twice per day)' }
]
const toggleGoal = (goal: string) => {
setProfile(prev => ({
...prev,
fitnessGoals: prev.fitnessGoals.includes(goal)
? prev.fitnessGoals.filter(g => g !== goal)
: [...prev.fitnessGoals, goal]
}))
}
const handleSubmit = async () => {
if (!profile.height || !profile.weight || !profile.age) {
Alert.alert('Error', 'Please fill in all required fields')
return
}
setLoading(true)
try {
const user = await SecureStore.getItemAsync('user')
if (!user) {
throw new Error('No user found')
}
const userData = JSON.parse(user)
const response = await axios.post(
`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`,
{
userId: userData.id,
...profile
}
)
if (response.status === 201) {
Alert.alert('Success', 'Profile completed successfully!', [
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
])
}
} catch (error: any) {
console.log('Profile save error:', error)
Alert.alert('Error', error.response?.data?.error || 'Failed to save profile. Please try again.')
} finally {
setLoading(false)
}
}
return (
<ScrollView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Welcome to FitAI!</Text>
<Text style={styles.subtitle}>Let's set up your fitness profile</Text>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Basic Information</Text>
<View style={styles.row}>
<View style={[styles.inputContainer, { flex: 1, marginRight: 8 }]}>
<Text style={styles.label}>Height (cm)</Text>
<TextInput
style={styles.input}
value={profile.height}
onChangeText={(text) => setProfile({ ...profile, height: text })}
keyboardType="numeric"
placeholder="170"
/>
</View>
<View style={[styles.inputContainer, { flex: 1, marginLeft: 8 }]}>
<Text style={styles.label}>Weight (kg)</Text>
<TextInput
style={styles.input}
value={profile.weight}
onChangeText={(text) => setProfile({ ...profile, weight: text })}
keyboardType="numeric"
placeholder="70"
/>
</View>
</View>
<View style={styles.row}>
<View style={[styles.inputContainer, { flex: 1, marginRight: 8 }]}>
<Text style={styles.label}>Age</Text>
<TextInput
style={styles.input}
value={profile.age}
onChangeText={(text) => setProfile({ ...profile, age: text })}
keyboardType="numeric"
placeholder="25"
/>
</View>
<View style={[styles.inputContainer, { flex: 1, marginLeft: 8 }]}>
<Text style={styles.label}>Gender</Text>
<View style={styles.genderRow}>
{(['male', 'female', 'other'] as const).map((gender) => (
<TouchableOpacity
key={gender}
style={[
styles.genderButton,
profile.gender === gender && styles.genderButtonSelected
]}
onPress={() => setProfile({ ...profile, gender })}
>
<Text style={[
styles.genderButtonText,
profile.gender === gender && styles.genderButtonTextSelected
]}>
{gender.charAt(0).toUpperCase() + gender.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Activity Level</Text>
{activityLevels.map((level) => (
<TouchableOpacity
key={level.value}
style={[
styles.activityOption,
profile.activityLevel === level.value && styles.activityOptionSelected
]}
onPress={() => setProfile({ ...profile, activityLevel: level.value as any })}
>
<Text style={[
styles.activityText,
profile.activityLevel === level.value && styles.activityTextSelected
]}>
{level.label}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Fitness Goals</Text>
<Text style={styles.sectionSubtitle}>Select all that apply</Text>
<View style={styles.goalsContainer}>
{fitnessGoalsOptions.map((goal) => (
<TouchableOpacity
key={goal}
style={[
styles.goalButton,
profile.fitnessGoals.includes(goal) && styles.goalButtonSelected
]}
onPress={() => toggleGoal(goal)}
>
<Text style={[
styles.goalButtonText,
profile.fitnessGoals.includes(goal) && styles.goalButtonTextSelected
]}>
{goal}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Exercise Habits</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={profile.exerciseHabits}
onChangeText={(text) => setProfile({ ...profile, exerciseHabits: text })}
placeholder="Describe your current exercise routine..."
multiline
numberOfLines={3}
/>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Diet Habits</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={profile.dietHabits}
onChangeText={(text) => setProfile({ ...profile, dietHabits: text })}
placeholder="Describe your current eating habits..."
multiline
numberOfLines={3}
/>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Medical Conditions (Optional)</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={profile.medicalConditions}
onChangeText={(text) => setProfile({ ...profile, medicalConditions: text })}
placeholder="Any medical conditions we should know about..."
multiline
numberOfLines={3}
/>
</View>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSubmit}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Saving...' : 'Complete Profile'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
textAlign: 'center',
},
subtitle: {
fontSize: 16,
color: '#666',
marginBottom: 32,
textAlign: 'center',
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
color: '#333',
},
sectionSubtitle: {
fontSize: 14,
color: '#666',
marginBottom: 12,
},
row: {
flexDirection: 'row',
marginBottom: 16,
},
inputContainer: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
marginBottom: 6,
color: '#333',
},
input: {
backgroundColor: 'white',
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
fontSize: 16,
},
textArea: {
height: 80,
textAlignVertical: 'top',
},
genderRow: {
flexDirection: 'row',
},
genderButton: {
flex: 1,
paddingVertical: 12,
paddingHorizontal: 8,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
alignItems: 'center',
marginRight: 4,
},
genderButtonSelected: {
backgroundColor: '#3b82f6',
borderColor: '#3b82f6',
},
genderButtonText: {
fontSize: 12,
color: '#666',
},
genderButtonTextSelected: {
color: 'white',
},
activityOption: {
paddingVertical: 12,
paddingHorizontal: 16,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
marginBottom: 8,
backgroundColor: 'white',
},
activityOptionSelected: {
backgroundColor: '#3b82f6',
borderColor: '#3b82f6',
},
activityText: {
fontSize: 14,
color: '#333',
},
activityTextSelected: {
color: 'white',
},
goalsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginHorizontal: -4,
},
goalButton: {
backgroundColor: 'white',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 8,
margin: 4,
},
goalButtonSelected: {
backgroundColor: '#3b82f6',
borderColor: '#3b82f6',
},
goalButtonText: {
fontSize: 12,
color: '#666',
},
goalButtonTextSelected: {
color: 'white',
},
button: {
backgroundColor: '#3b82f6',
paddingVertical: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 16,
},
buttonDisabled: {
backgroundColor: '#9ca3af',
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
})

View File

@ -7,6 +7,9 @@ export const API_ENDPOINTS = {
LOGIN: '/api/auth/login',
REGISTER: '/api/auth/register',
},
PROFILE: {
FITNESS: '/api/profile/fitness',
},
CLIENTS: '/api/clients',
USERS: '/api/users',
}

3
strategy.md Normal file
View File

@ -0,0 +1,3 @@
## market penetration strategy
- from gyms -> end users