diff --git a/frontend/src/App.css b/frontend/src/App.css index 4eee35c..f451209 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -773,3 +773,380 @@ .upcoming-arrow { color: var(--text-muted); } + +/* ============================================ + PROFILE PAGE STYLES + ============================================ */ + +.profile-page, +.progress-page { + min-height: 100vh; + background: var(--bg); +} + +.profile-page.loading, +.progress-page.loading { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1rem; +} + +/* Page Header */ +.page-header { + background: var(--bg-secondary); + padding: 1rem 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; +} + +.page-header h1 { + font-size: 1.25rem; + font-weight: 600; +} + +.back-btn { + background: transparent; + border: none; + color: var(--accent); + font-size: 1rem; + cursor: pointer; + padding: 0.5rem; +} + +/* Page Main */ +.page-main { + padding: 1rem; + max-width: 600px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Profile Section */ +.profile-section { + background: var(--bg-secondary); + border-radius: 16px; + padding: 1.25rem; + border: 1px solid var(--border); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.section-header h2 { + font-size: 1.1rem; + font-weight: 600; +} + +.edit-btn { + background: var(--bg); + border: 1px solid var(--border); + padding: 0.5rem 0.75rem; + border-radius: 8px; + cursor: pointer; + font-size: 0.85rem; + color: var(--text); +} + +/* Info Grid */ +.info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.info-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; +} + +.info-value { + font-size: 1rem; + font-weight: 500; +} + +/* Edit Form */ +.edit-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.85rem; + color: var(--text-muted); +} + +.form-group input, +.form-group select { + padding: 0.75rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + color: var(--text); + font-size: 1rem; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: var(--accent); +} + +.form-actions { + display: flex; + gap: 1rem; + margin-top: 0.5rem; +} + +.cancel-btn { + flex: 1; + padding: 0.75rem; + border: 1px solid var(--border); + background: var(--bg); + border-radius: 8px; + cursor: pointer; + color: var(--text); +} + +.save-btn { + flex: 1; + padding: 0.75rem; + border: none; + background: var(--accent); + color: white; + border-radius: 8px; + cursor: pointer; + font-weight: 600; +} + +.save-btn:disabled { + opacity: 0.7; +} + +/* Measurements Grid */ +.measurements-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.measurement-card { + background: var(--bg); + padding: 1rem; + border-radius: 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} + +.measurement-icon { + font-size: 1.5rem; +} + +.measurement-value { + font-size: 1.25rem; + font-weight: 700; + color: var(--accent); +} + +.measurement-label { + font-size: 0.75rem; + color: var(--text-muted); +} + +/* Strength Grid */ +.strength-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.strength-card { + display: flex; + justify-content: space-between; + padding: 1rem; + background: var(--bg); + border-radius: 12px; +} + +.strength-exercise { + font-weight: 500; +} + +.strength-value { + font-weight: 700; + color: var(--accent); +} + +.no-data { + color: var(--text-muted); + text-align: center; + padding: 1rem; +} + +/* ============================================ + PROGRESS PAGE STYLES + ============================================ */ + +.progress-tabs { + display: flex; + gap: 0.5rem; + background: var(--bg-secondary); + padding: 0.5rem; + border-radius: 12px; + border: 1px solid var(--border); +} + +.tab-btn { + flex: 1; + padding: 0.75rem; + border: none; + background: transparent; + border-radius: 8px; + cursor: pointer; + font-size: 0.9rem; + color: var(--text-muted); + transition: all 0.2s; +} + +.tab-btn.active { + background: var(--accent); + color: white; +} + +.tab-btn:hover:not(.active) { + background: var(--bg); +} + +/* Chart Section */ +.chart-section { + background: var(--bg-secondary); + border-radius: 16px; + padding: 1.25rem; + border: 1px solid var(--border); +} + +.chart-section h2 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.chart-container { + margin-bottom: 1rem; +} + +.line-chart { + width: 100%; + max-width: 100%; +} + +.chart-labels { + display: flex; + justify-content: space-between; + font-size: 0.75rem; + color: var(--text-muted); + padding: 0 0.5rem; +} + +/* Strength Charts */ +.strength-charts { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.strength-chart-item h3 { + font-size: 1rem; + margin-bottom: 0.75rem; +} + +/* Progress Stats */ +.progress-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--border); +} + +.progress-stats .stat-item { + text-align: center; +} + +.progress-stats .stat-label { + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; +} + +.progress-stats .stat-value { + font-size: 0.9rem; + font-weight: 600; +} + +.trend-up { + color: #10b981; +} + +.trend-down { + color: #ef4444; +} + +.trend-neutral { + color: var(--text-muted); +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 2rem; +} + +.empty-icon { + font-size: 3rem; + display: block; + margin-bottom: 1rem; +} + +.empty-state p { + color: var(--text-muted); +} + +.empty-hint { + font-size: 0.85rem; + margin-top: 0.5rem; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a7f01db..0ae3f18 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react' import { useAuth } from './context/AuthContext' import Dashboard from './pages/Dashboard' +import ProfilePage from './pages/ProfilePage' +import ProgressPage from './pages/ProgressPage' import './App.css' const API_URL = '/api' @@ -95,7 +97,22 @@ function App() { // Dashboard view (default after login) if (view === 'dashboard') { - return + return ( + + ) + } + + // Profile page + if (view === 'profile') { + return setView('dashboard')} /> + } + + // Progress page + if (view === 'progress') { + return setView('dashboard')} /> } // Workout view diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 550796c..72a7ad9 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -30,7 +30,7 @@ const getCoachGreeting = (user, todayWorkout) => { // Get weekday names const weekdays = ['Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör', 'Sön'] -function Dashboard({ onStartWorkout }) { +function Dashboard({ onStartWorkout, onNavigate }) { const { user, logout } = useAuth() const [program, setProgram] = useState(null) const [todayWorkout, setTodayWorkout] = useState(null) @@ -81,8 +81,8 @@ function Dashboard({ onStartWorkout }) {

🏋️ Gravl

diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx new file mode 100644 index 0000000..bae2aae --- /dev/null +++ b/frontend/src/pages/ProfilePage.jsx @@ -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 ( +
+
+

Laddar profil...

+
+ ) + } + + const latestMeasurement = measurements?.[0] + const latestStrength = strength?.[0] + + return ( +
+
+ +

Min profil

+ +
+ +
+ {/* Profile Info */} +
+
+

Personuppgifter

+ {!editing && ( + + )} +
+ + {editing ? ( +
+
+ + handleChange('name', e.target.value)} + /> +
+
+
+ + handleChange('age', parseInt(e.target.value))} + /> +
+
+ + +
+
+
+
+ + handleChange('height_cm', parseFloat(e.target.value))} + /> +
+
+ + handleChange('workouts_per_week', parseInt(e.target.value))} + /> +
+
+
+ + +
+
+ + +
+
+ + +
+
+ ) : ( +
+
+
+ Namn + {profile?.name || '-'} +
+
+ Ålder + {profile?.age || '-'} år +
+
+ Längd + {profile?.height_cm || '-'} cm +
+
+ Kön + {profile?.gender === 'male' ? 'Man' : 'Kvinna'} +
+
+ Mål + {getGoalLabel(profile?.goal)} +
+
+ Nivå + {getLevelLabel(profile?.experience_level)} +
+
+ Pass/vecka + {profile?.workouts_per_week || '-'} +
+
+
+ )} +
+ + {/* Current Measurements */} +
+

Aktuella mätningar

+ {latestMeasurement ? ( +
+
+ ⚖️ + {latestMeasurement.weight} kg + Vikt +
+ {latestMeasurement.body_fat_pct && ( +
+ 📊 + {latestMeasurement.body_fat_pct}% + Kroppsfett +
+ )} + {latestMeasurement.waist_cm && ( +
+ 📏 + {latestMeasurement.waist_cm} cm + Midja +
+ )} + {latestMeasurement.neck_cm && ( +
+ 📏 + {latestMeasurement.neck_cm} cm + Nacke +
+ )} +
+ ) : ( +

Inga mätningar registrerade

+ )} +
+ + {/* Strength Records */} +
+

Styrkerekord (1RM)

+ {latestStrength ? ( +
+
+ Bänkpress + {latestStrength.bench_1rm || '-'} kg +
+
+ Knäböj + {latestStrength.squat_1rm || '-'} kg +
+
+ Marklyft + {latestStrength.deadlift_1rm || '-'} kg +
+
+ ) : ( +

Inga styrkerekord registrerade

+ )} +
+
+
+ ) +} + +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 diff --git a/frontend/src/pages/ProgressPage.jsx b/frontend/src/pages/ProgressPage.jsx new file mode 100644 index 0000000..1e8bb7d --- /dev/null +++ b/frontend/src/pages/ProgressPage.jsx @@ -0,0 +1,327 @@ +import { useState, useEffect } from 'react' +import { useAuth } from '../context/AuthContext' + +const API_URL = '/api' + +function ProgressPage({ onBack }) { + const { user } = useAuth() + const [measurements, setMeasurements] = useState([]) + const [strength, setStrength] = useState([]) + const [loading, setLoading] = useState(true) + const [activeTab, setActiveTab] = useState('weight') + + useEffect(() => { + fetchData() + }, []) + + const fetchData = async () => { + try { + const [measurementsRes, strengthRes] = await Promise.all([ + fetch(`${API_URL}/user/measurements/${user?.id || 1}`), + fetch(`${API_URL}/user/strength/${user?.id || 1}`) + ]) + + const measurementsData = await measurementsRes.json() + const strengthData = await strengthRes.json() + + // Sort by date ascending for charts + setMeasurements([...measurementsData].reverse()) + setStrength([...strengthData].reverse()) + setLoading(false) + } catch (err) { + console.error('Failed to fetch progress:', err) + setLoading(false) + } + } + + if (loading) { + return ( +
+
+

Laddar progress...

+
+ ) + } + + return ( +
+
+ +

Min progress

+
+
+ +
+ {/* Tab Navigation */} +
+ + + +
+ + {/* Weight Chart */} + {activeTab === 'weight' && ( +
+

Viktutveckling

+ {measurements.length > 0 ? ( + <> + + + + ) : ( + + )} +
+ )} + + {/* Body Fat Chart */} + {activeTab === 'bodyfat' && ( +
+

Kroppsfett

+ {measurements.filter(m => m.body_fat_pct).length > 0 ? ( + <> + m.body_fat_pct)} + valueKey="body_fat_pct" + unit="%" + color="#10b981" + /> + m.body_fat_pct)} + valueKey="body_fat_pct" + unit="%" + label="Kroppsfett" + /> + + ) : ( + + )} +
+ )} + + {/* Strength Charts */} + {activeTab === 'strength' && ( +
+

Styrkerekord (1RM)

+ {strength.length > 0 ? ( +
+
+

🏋️ Bänkpress

+ s.bench_1rm)} + valueKey="bench_1rm" + unit="kg" + color="#f59e0b" + /> + s.bench_1rm)} + valueKey="bench_1rm" + unit="kg" + label="Bänkpress" + /> +
+
+

🦵 Knäböj

+ s.squat_1rm)} + valueKey="squat_1rm" + unit="kg" + color="#8b5cf6" + /> + s.squat_1rm)} + valueKey="squat_1rm" + unit="kg" + label="Knäböj" + /> +
+
+

💀 Marklyft

+ s.deadlift_1rm)} + valueKey="deadlift_1rm" + unit="kg" + color="#ef4444" + /> + s.deadlift_1rm)} + valueKey="deadlift_1rm" + unit="kg" + label="Marklyft" + /> +
+
+ ) : ( + + )} +
+ )} +
+
+ ) +} + +// Simple SVG Line Chart Component +function SimpleLineChart({ data, valueKey, unit, color }) { + if (!data || data.length === 0) return null + + const values = data.map(d => d[valueKey]).filter(v => v != null) + if (values.length === 0) return null + + const min = Math.min(...values) * 0.95 + const max = Math.max(...values) * 1.05 + const range = max - min || 1 + + const width = 320 + const height = 160 + const padding = { top: 20, right: 20, bottom: 30, left: 50 } + const chartWidth = width - padding.left - padding.right + const chartHeight = height - padding.top - padding.bottom + + // Generate points + const points = data.map((d, i) => { + const x = padding.left + (i / Math.max(data.length - 1, 1)) * chartWidth + const y = padding.top + chartHeight - ((d[valueKey] - min) / range) * chartHeight + return { x, y, value: d[valueKey], date: d.created_at } + }).filter(p => p.value != null) + + const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ') + + // Y-axis labels + const yLabels = [min, (min + max) / 2, max].map(v => v.toFixed(1)) + + return ( +
+ + {/* Grid lines */} + {[0, 0.5, 1].map((ratio, i) => ( + + ))} + + {/* Y-axis labels */} + {yLabels.map((label, i) => ( + + {label} + + ))} + + {/* Line */} + + + {/* Points */} + {points.map((p, i) => ( + + ))} + +
+ {formatDate(data[0]?.created_at)} + {formatDate(data[data.length - 1]?.created_at)} +
+
+ ) +} + +// Progress Statistics Component +function ProgressStats({ data, valueKey, unit, label }) { + if (!data || data.length === 0) return null + + const values = data.map(d => d[valueKey]).filter(v => v != null) + if (values.length === 0) return null + + const current = values[values.length - 1] + const first = values[0] + const change = current - first + const changePercent = ((change / first) * 100).toFixed(1) + const trend = change > 0 ? '↑' : change < 0 ? '↓' : '→' + const trendClass = change > 0 ? 'up' : change < 0 ? 'down' : 'neutral' + + return ( +
+
+ Nuvarande + {current} {unit} +
+
+ Första + {first} {unit} +
+
+ Förändring + + {trend} {Math.abs(change).toFixed(1)} {unit} ({changePercent}%) + +
+
+ ) +} + +function EmptyState({ message }) { + return ( +
+ 📊 +

{message}

+

Logga mätningar för att se din progress

+
+ ) +} + +function formatDate(dateStr) { + if (!dateStr) return '-' + const date = new Date(dateStr) + return date.toLocaleDateString('sv-SE', { month: 'short', day: 'numeric' }) +} + +export default ProgressPage