diff --git a/apps/admin/src/app/api/profile/fitness/route.ts b/apps/admin/src/app/api/profile/fitness/route.ts new file mode 100644 index 0000000..bb78ed5 --- /dev/null +++ b/apps/admin/src/app/api/profile/fitness/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/apps/admin/src/lib/database.ts b/apps/admin/src/lib/database.ts index b9d86cc..4d88b05 100644 --- a/apps/admin/src/lib/database.ts +++ b/apps/admin/src/lib/database.ts @@ -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[] = [] \ No newline at end of file +export const clients: Client[] = [] +export const fitnessProfiles: FitnessProfile[] = [] \ No newline at end of file diff --git a/apps/mobile/src/app/login.tsx b/apps/mobile/src/app/login.tsx index fc68429..b55c6e6 100644 --- a/apps/mobile/src/app/login.tsx +++ b/apps/mobile/src/app/login.tsx @@ -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') diff --git a/apps/mobile/src/app/welcome.tsx b/apps/mobile/src/app/welcome.tsx new file mode 100644 index 0000000..35c9e8c --- /dev/null +++ b/apps/mobile/src/app/welcome.tsx @@ -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({ + 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 ( + + + Welcome to FitAI! + Let's set up your fitness profile + + + Basic Information + + + + Height (cm) + setProfile({ ...profile, height: text })} + keyboardType="numeric" + placeholder="170" + /> + + + + Weight (kg) + setProfile({ ...profile, weight: text })} + keyboardType="numeric" + placeholder="70" + /> + + + + + + Age + setProfile({ ...profile, age: text })} + keyboardType="numeric" + placeholder="25" + /> + + + + Gender + + {(['male', 'female', 'other'] as const).map((gender) => ( + setProfile({ ...profile, gender })} + > + + {gender.charAt(0).toUpperCase() + gender.slice(1)} + + + ))} + + + + + + + Activity Level + {activityLevels.map((level) => ( + setProfile({ ...profile, activityLevel: level.value as any })} + > + + {level.label} + + + ))} + + + + Fitness Goals + Select all that apply + + {fitnessGoalsOptions.map((goal) => ( + toggleGoal(goal)} + > + + {goal} + + + ))} + + + + + Exercise Habits + setProfile({ ...profile, exerciseHabits: text })} + placeholder="Describe your current exercise routine..." + multiline + numberOfLines={3} + /> + + + + Diet Habits + setProfile({ ...profile, dietHabits: text })} + placeholder="Describe your current eating habits..." + multiline + numberOfLines={3} + /> + + + + Medical Conditions (Optional) + setProfile({ ...profile, medicalConditions: text })} + placeholder="Any medical conditions we should know about..." + multiline + numberOfLines={3} + /> + + + + + {loading ? 'Saving...' : 'Complete Profile'} + + + + + ) +} + +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', + }, +}) \ No newline at end of file diff --git a/apps/mobile/src/config/api.ts b/apps/mobile/src/config/api.ts index b3b99a6..a9f6519 100644 --- a/apps/mobile/src/config/api.ts +++ b/apps/mobile/src/config/api.ts @@ -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', } \ No newline at end of file diff --git a/strategy.md b/strategy.md new file mode 100644 index 0000000..2c71a4b --- /dev/null +++ b/strategy.md @@ -0,0 +1,3 @@ +## market penetration strategy + +- from gyms -> end users