Add ProfilePage and ProgressPage
ProfilePage: - View/edit user info (name, age, height, goal, level) - Show current measurements (weight, body fat, waist, neck) - Show strength records (bench/squat/deadlift 1RM) ProgressPage: - Tab navigation (weight, body fat, strength) - SVG line charts for progress visualization - Stats showing current, first, and change - Trend indicators (up/down) Dashboard: - Navigation icons for profile (👤) and progress (📊) - Connected navigation to App.jsx routing
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
const API_URL = '/api'
|
||||
|
||||
function ProfilePage({ onBack }) {
|
||||
const { user, logout } = useAuth()
|
||||
const [profile, setProfile] = useState(null)
|
||||
const [measurements, setMeasurements] = useState(null)
|
||||
const [strength, setStrength] = useState(null)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Edit form state
|
||||
const [form, setForm] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [profileRes, measurementsRes, strengthRes] = await Promise.all([
|
||||
fetch(`${API_URL}/user/profile/${user?.id || 1}`),
|
||||
fetch(`${API_URL}/user/measurements/${user?.id || 1}`),
|
||||
fetch(`${API_URL}/user/strength/${user?.id || 1}`)
|
||||
])
|
||||
|
||||
const profileData = await profileRes.json()
|
||||
const measurementsData = await measurementsRes.json()
|
||||
const strengthData = await strengthRes.json()
|
||||
|
||||
setProfile(profileData)
|
||||
setMeasurements(measurementsData)
|
||||
setStrength(strengthData)
|
||||
setForm(profileData)
|
||||
setLoading(false)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch profile:', err)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/user/profile/${user?.id || 1}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(form)
|
||||
})
|
||||
const updated = await res.json()
|
||||
setProfile(updated)
|
||||
setEditing(false)
|
||||
} catch (err) {
|
||||
console.error('Failed to save profile:', err)
|
||||
}
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setForm(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="profile-page loading">
|
||||
<div className="spinner"></div>
|
||||
<p>Laddar profil...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const latestMeasurement = measurements?.[0]
|
||||
const latestStrength = strength?.[0]
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
<header className="page-header">
|
||||
<button className="back-btn" onClick={onBack}>← Tillbaka</button>
|
||||
<h1>Min profil</h1>
|
||||
<button className="logout-btn" onClick={logout}>↪</button>
|
||||
</header>
|
||||
|
||||
<main className="page-main">
|
||||
{/* Profile Info */}
|
||||
<section className="profile-section">
|
||||
<div className="section-header">
|
||||
<h2>Personuppgifter</h2>
|
||||
{!editing && (
|
||||
<button className="edit-btn" onClick={() => setEditing(true)}>
|
||||
✏️ Redigera
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="edit-form">
|
||||
<div className="form-group">
|
||||
<label>Namn</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name || ''}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>Ålder</label>
|
||||
<input
|
||||
type="number"
|
||||
value={form.age || ''}
|
||||
onChange={(e) => handleChange('age', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Kön</label>
|
||||
<select
|
||||
value={form.gender || ''}
|
||||
onChange={(e) => handleChange('gender', e.target.value)}
|
||||
>
|
||||
<option value="male">Man</option>
|
||||
<option value="female">Kvinna</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>Längd (cm)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={form.height_cm || ''}
|
||||
onChange={(e) => handleChange('height_cm', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Pass/vecka</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="7"
|
||||
value={form.workouts_per_week || ''}
|
||||
onChange={(e) => handleChange('workouts_per_week', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Mål</label>
|
||||
<select
|
||||
value={form.goal || ''}
|
||||
onChange={(e) => handleChange('goal', e.target.value)}
|
||||
>
|
||||
<option value="muscle">Bygga muskler</option>
|
||||
<option value="strength">Öka styrka</option>
|
||||
<option value="fat_loss">Fettförbränning</option>
|
||||
<option value="general">Allmän fitness</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Erfarenhetsnivå</label>
|
||||
<select
|
||||
value={form.experience_level || ''}
|
||||
onChange={(e) => handleChange('experience_level', e.target.value)}
|
||||
>
|
||||
<option value="beginner">Nybörjare</option>
|
||||
<option value="intermediate">Medel</option>
|
||||
<option value="advanced">Avancerad</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-actions">
|
||||
<button className="cancel-btn" onClick={() => { setEditing(false); setForm(profile) }}>
|
||||
Avbryt
|
||||
</button>
|
||||
<button className="save-btn" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Sparar...' : 'Spara'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="profile-info">
|
||||
<div className="info-grid">
|
||||
<div className="info-item">
|
||||
<span className="info-label">Namn</span>
|
||||
<span className="info-value">{profile?.name || '-'}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Ålder</span>
|
||||
<span className="info-value">{profile?.age || '-'} år</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Längd</span>
|
||||
<span className="info-value">{profile?.height_cm || '-'} cm</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Kön</span>
|
||||
<span className="info-value">{profile?.gender === 'male' ? 'Man' : 'Kvinna'}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Mål</span>
|
||||
<span className="info-value">{getGoalLabel(profile?.goal)}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Nivå</span>
|
||||
<span className="info-value">{getLevelLabel(profile?.experience_level)}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Pass/vecka</span>
|
||||
<span className="info-value">{profile?.workouts_per_week || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Current Measurements */}
|
||||
<section className="profile-section">
|
||||
<h2>Aktuella mätningar</h2>
|
||||
{latestMeasurement ? (
|
||||
<div className="measurements-grid">
|
||||
<div className="measurement-card">
|
||||
<span className="measurement-icon">⚖️</span>
|
||||
<span className="measurement-value">{latestMeasurement.weight} kg</span>
|
||||
<span className="measurement-label">Vikt</span>
|
||||
</div>
|
||||
{latestMeasurement.body_fat_pct && (
|
||||
<div className="measurement-card">
|
||||
<span className="measurement-icon">📊</span>
|
||||
<span className="measurement-value">{latestMeasurement.body_fat_pct}%</span>
|
||||
<span className="measurement-label">Kroppsfett</span>
|
||||
</div>
|
||||
)}
|
||||
{latestMeasurement.waist_cm && (
|
||||
<div className="measurement-card">
|
||||
<span className="measurement-icon">📏</span>
|
||||
<span className="measurement-value">{latestMeasurement.waist_cm} cm</span>
|
||||
<span className="measurement-label">Midja</span>
|
||||
</div>
|
||||
)}
|
||||
{latestMeasurement.neck_cm && (
|
||||
<div className="measurement-card">
|
||||
<span className="measurement-icon">📏</span>
|
||||
<span className="measurement-value">{latestMeasurement.neck_cm} cm</span>
|
||||
<span className="measurement-label">Nacke</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="no-data">Inga mätningar registrerade</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Strength Records */}
|
||||
<section className="profile-section">
|
||||
<h2>Styrkerekord (1RM)</h2>
|
||||
{latestStrength ? (
|
||||
<div className="strength-grid">
|
||||
<div className="strength-card">
|
||||
<span className="strength-exercise">Bänkpress</span>
|
||||
<span className="strength-value">{latestStrength.bench_1rm || '-'} kg</span>
|
||||
</div>
|
||||
<div className="strength-card">
|
||||
<span className="strength-exercise">Knäböj</span>
|
||||
<span className="strength-value">{latestStrength.squat_1rm || '-'} kg</span>
|
||||
</div>
|
||||
<div className="strength-card">
|
||||
<span className="strength-exercise">Marklyft</span>
|
||||
<span className="strength-value">{latestStrength.deadlift_1rm || '-'} kg</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="no-data">Inga styrkerekord registrerade</p>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function getGoalLabel(goal) {
|
||||
const labels = {
|
||||
muscle: 'Bygga muskler',
|
||||
strength: 'Öka styrka',
|
||||
fat_loss: 'Fettförbränning',
|
||||
general: 'Allmän fitness'
|
||||
}
|
||||
return labels[goal] || goal || '-'
|
||||
}
|
||||
|
||||
function getLevelLabel(level) {
|
||||
const labels = {
|
||||
beginner: 'Nybörjare',
|
||||
intermediate: 'Medel',
|
||||
advanced: 'Avancerad'
|
||||
}
|
||||
return labels[level] || level || '-'
|
||||
}
|
||||
|
||||
export default ProfilePage
|
||||
Reference in New Issue
Block a user