welcome page
with profile form
This commit is contained in:
parent
3a554ba434
commit
e287e55f1f
66
apps/admin/src/app/api/profile/fitness/route.ts
Normal file
66
apps/admin/src/app/api/profile/fitness/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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 clients: Client[] = []
|
||||
export const fitnessProfiles: FitnessProfile[] = []
|
||||
@ -25,9 +25,31 @@ export default function LoginScreen() {
|
||||
|
||||
if (response.data.user) {
|
||||
await SecureStore.setItemAsync('user', JSON.stringify(response.data.user))
|
||||
Alert.alert('Success', 'Login successful!', [
|
||||
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
|
||||
])
|
||||
|
||||
// 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')
|
||||
|
||||
408
apps/mobile/src/app/welcome.tsx
Normal file
408
apps/mobile/src/app/welcome.tsx
Normal 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',
|
||||
},
|
||||
})
|
||||
@ -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
3
strategy.md
Normal file
@ -0,0 +1,3 @@
|
||||
## market penetration strategy
|
||||
|
||||
- from gyms -> end users
|
||||
Loading…
Reference in New Issue
Block a user