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
|
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
|
// In-memory database
|
||||||
export const users: User[] = []
|
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) {
|
if (response.data.user) {
|
||||||
await SecureStore.setItemAsync('user', JSON.stringify(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!', [
|
Alert.alert('Success', 'Login successful!', [
|
||||||
{ text: 'OK', onPress: () => router.replace('/(tabs)') }
|
{ 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) {
|
} catch (error: any) {
|
||||||
Alert.alert('Error', error.response?.data?.error || 'Login failed')
|
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',
|
LOGIN: '/api/auth/login',
|
||||||
REGISTER: '/api/auth/register',
|
REGISTER: '/api/auth/register',
|
||||||
},
|
},
|
||||||
|
PROFILE: {
|
||||||
|
FITNESS: '/api/profile/fitness',
|
||||||
|
},
|
||||||
CLIENTS: '/api/clients',
|
CLIENTS: '/api/clients',
|
||||||
USERS: '/api/users',
|
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