import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { api } from '../api' /* === Tooltip Component === */ function Tip({ text, children }) { const [show, setShow] = useState(false) return ( {children} setShow(true)} onMouseLeave={() => setShow(false)} onClick={(e) => { e.stopPropagation(); setShow(s => !s) }} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '18px', height: '18px', borderRadius: '50%', marginLeft: '6px', background: 'rgba(0,0,0,0.06)', color: 'var(--text-3)', cursor: 'help', fontSize: '0.7rem', fontWeight: 800, flexShrink: 0, }}>? {show && ( {text} )} ) } const GOALS = [ { value: 'lose_weight', label: 'Lose Weight', icon: '⚖️', tip: 'Focus on caloric deficit through cardio and portion-controlled nutrition.' }, { value: 'build_strength', label: 'Build Strength', icon: '💪', tip: 'Progressive overload training — gradually increasing weight or resistance.' }, { value: 'get_active', label: 'Get More Active', icon: '🏃', tip: 'Building a consistent movement habit. Walking counts!' }, { value: 'feel_better', label: 'Feel Better Overall', icon: '😊', tip: 'Mind-body balance — stress relief, sleep quality, energy levels.' }, ] const TTM_STAGES = [ { value: 'precontemplation', label: "I'm not exercising and haven't thought about starting", tip: 'Pre-contemplation: No intention to act. We\'ll start with awareness and gentle nudges.' }, { value: 'contemplation', label: "I've been thinking about getting more active but haven't started", tip: 'Contemplation: Weighing pros and cons. We\'ll help tip the balance toward action.' }, { value: 'preparation', label: 'I do some exercise but not consistently', tip: 'Preparation: Ready to commit. We\'ll help you build a sustainable routine.' }, { value: 'action', label: "I've been exercising regularly for a few months", tip: 'Action: Building the habit. We\'ll help you stay consistent and avoid burnout.' }, { value: 'maintenance', label: "I've been exercising regularly for 6+ months", tip: 'Maintenance: Solid habit. We\'ll help you progress and keep things interesting.' }, ] const ACTIVITIES = [ 'Walking', 'Hiking', 'Running', 'Cycling', 'Swimming', 'Bodyweight', 'Weights', 'Yoga', 'Martial Arts', 'Dance', 'Team Sports', 'Pilates', 'Rowing', 'Jump Rope', 'Stretching', ] const EQUIPMENT_OPTIONS = [ { value: 'bicycle', label: '🚲 Bicycle' }, { value: 'pool', label: '🏊 Pool' }, { value: 'free_weights', label: '🏋️ Free Weights' }, { value: 'squat_rack', label: '🦵 Squat Rack' }, { value: 'bench_press', label: '💺 Bench Press' }, { value: 'machines', label: '⚙️ Machines' }, { value: 'resistance_bands', label: '🔗 Resistance Bands' }, { value: 'pull_up_bar', label: '🔩 Pull-up Bar' }, { value: 'kettlebell', label: '🔔 Kettlebell' }, { value: 'jump_rope', label: '⏩ Jump Rope' }, { value: 'yoga_mat', label: '🧘 Yoga Mat' }, { value: 'treadmill', label: '🏃 Treadmill' }, { value: 'stationary_bike', label: '🚴 Stationary Bike' }, { value: 'rowing_machine', label: '🚣 Rowing Machine' }, ] const BREQ2_ITEMS = [ { key: 'ext', label: '"People important to me say I should exercise"', tip: 'External regulation: exercising because others push you to. Least self-determined motivation.' }, { key: 'intro', label: '"I feel bad about myself when I skip exercise"', tip: 'Introjected regulation: guilt or obligation as a driver. Better than external, but still fragile.' }, { key: 'ident', label: '"I value what exercise does for my health"', tip: 'Identified regulation: you see exercise as personally important. A strong, durable motivator.' }, { key: 'intr', label: '"I find exercise enjoyable and satisfying"', tip: 'Intrinsic motivation: you exercise because it\'s fun. The most sustainable form of motivation.' }, { key: 'amot', label: '"I don\'t really see why I should bother"', tip: 'Amotivation: no perceived reason to exercise. If this is high, we\'ll start with very small wins.' }, ] export default function Onboarding() { const [step, setStep] = useState(0) const [goal, setGoal] = useState('') const [ttm, setTtm] = useState('') const [age, setAge] = useState('') const [heightCm, setHeightCm] = useState('') const [weightKg, setWeightKg] = useState('') const [gender, setGender] = useState('') const [parq, setParq] = useState({ heart: false, joints: false, meds: false }) const [motivation, setMotivation] = useState({ ext: 0, intro: 0, ident: 0, intr: 0, amot: 0 }) const [activities, setActivities] = useState([]) const [equipmentList, setEquipmentList] = useState([]) const [daysPerWeek, setDaysPerWeek] = useState(3) const [minsPerSession, setMinsPerSession] = useState(30) const [rewards, setRewards] = useState([{ name: '', cost: 100 }]) const [recommendations, setRecommendations] = useState([]) const [loading, setLoading] = useState(false) const navigate = useNavigate() const totalSteps = 8 function Likert({ label, value, onChange, tip }) { return (
{tip ? {label} : label}
{[1, 2, 3, 4, 5].map(n => ( ))}
Not at allVery true
) } async function handlePhase1() { setLoading(true) try { await api.savePhase1({ primary_goal: goal, ttm_stage: ttm }) setStep(2) } catch (err) { alert(err.message) } finally { setLoading(false) } } async function handlePhase2() { setLoading(true) try { // Derive legacy equipment_access from equipment list let equipAccess = 'none' const gym = ['squat_rack', 'bench_press', 'machines'] const basic = ['free_weights', 'resistance_bands', 'pull_up_bar', 'kettlebell', 'yoga_mat', 'jump_rope'] if (equipmentList.some(e => gym.includes(e))) equipAccess = 'full_gym' else if (equipmentList.some(e => basic.includes(e))) equipAccess = 'basic_home' await api.savePhase2({ age: age ? parseInt(age) : null, height_cm: heightCm ? parseFloat(heightCm) : null, weight_kg: weightKg ? parseFloat(weightKg) : null, gender: gender || null, parq_heart_condition: parq.heart, parq_joint_issues: parq.joints, parq_medications: parq.meds, motivation_external: motivation.ext || null, motivation_introjected: motivation.intro || null, motivation_identified: motivation.ident || null, motivation_intrinsic: motivation.intr || null, motivation_amotivation: motivation.amot || null, activity_preferences: activities.map(a => a.toLowerCase().replace(/ /g, '_')), equipment_access: equipAccess, equipment_list: equipmentList, days_per_week: daysPerWeek, minutes_per_session: minsPerSession, }) for (const r of rewards) { if (r.name.trim()) { await api.createReward({ name: r.name, point_cost: r.cost || 100 }) } } const recs = await api.getRecommendations() setRecommendations(recs.recommendations || []) setStep(7) } catch (err) { alert(err.message) } finally { setLoading(false) } } async function handleCommit(rec) { try { const today = new Date() const monday = new Date(today) monday.setDate(today.getDate() + ((8 - today.getDay()) % 7 || 7)) const startDate = monday.toISOString().split('T')[0] await api.createProgram({ name: rec.name, source: rec.source, source_url: rec.url, start_date: startDate, duration_days: rec.duration_days || 90, }) navigate('/') } catch (err) { alert(err.message) } } function toggleActivity(a) { setActivities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a]) } function toggleEquipment(val) { setEquipmentList(prev => prev.includes(val) ? prev.filter(x => x !== val) : [...prev, val]) } const progress = Math.round(((step + 1) / totalSteps) * 100) const h2Style = { fontFamily: 'var(--font-display)', fontWeight: 900, letterSpacing: '-0.02em', marginBottom: '10px', fontSize: '1.4rem' } return (
{/* Step 0: Goal */} {step === 0 && (

What matters most to you?

Pick one — you can change this anytime.

{GOALS.map(g => (
setGoal(g.value)}>
{g.icon}
{g.label}
))}
)} {/* Step 1: TTM Stage */} {step === 1 && (

Where are you now?

Be honest — there's no wrong answer.

{TTM_STAGES.map(s => (
setTtm(s.value)} > {s.label}
))}
)} {/* Step 2: Safety (PAR-Q+) */} {step === 2 && (

Quick health check

For your safety — this takes 10 seconds.

{[ { key: 'heart', label: 'I have a heart condition or high blood pressure' }, { key: 'joints', label: 'I have bone or joint problems that could worsen with exercise' }, { key: 'meds', label: 'I take prescription medications for a chronic condition' }, ].map(q => ( ))} {(parq.heart || parq.joints || parq.meds) && (
⚠️ We recommend checking with your doctor before starting. This won't stop you — just be mindful.
)}
)} {/* Step 3: Body Metrics */} {step === 3 && (

About you

Optional — helps estimate nutrition needs.

setAge(e.target.value)} placeholder="e.g. 35" />
setHeightCm(e.target.value)} placeholder="175" />
setWeightKg(e.target.value)} placeholder="80" />
)} {/* Step 4: Motivation (BREQ-2) */} {step === 4 && (

What drives you?

Rate each honestly — helps us calibrate your experience.

{BREQ2_ITEMS.map(item => ( setMotivation({ ...motivation, [item.key]: v })} /> ))}
)} {/* Step 5: Activities + Equipment */} {step === 5 && (

What sounds fun?

Pick all that interest you.

{ACTIVITIES.map(a => (
toggleActivity(a)}>{a}
))}
{EQUIPMENT_OPTIONS.map(e => (
toggleEquipment(e.value)}> {e.label}
))}
{equipmentList.length === 0 && (

No selection = bodyweight only. That's perfectly fine!

)}
)} {/* Step 6: Define Rewards */} {step === 6 && (

Define your rewards

What guilty pleasures do you want to earn?

{rewards.map((r, i) => (
{ const next = [...rewards]; next[i] = { ...r, name: e.target.value }; setRewards(next) }} /> { const next = [...rewards]; next[i] = { ...r, cost: parseInt(e.target.value) || 0 }; setRewards(next) }} />
))}
)} {/* Step 7: Recommendations + Commit */} {step === 7 && (

Recommended for you

Pick a program and commit to 90 days.

{recommendations.map(rec => (
{rec.name}
{rec.difficulty} • {rec.duration_days ? `${rec.duration_days} days` : 'Ongoing'} • {rec.source}

{rec.description}

View ↗
))}
)}
) }