Files
gravl/frontend/src/pages/ProgressPage.jsx
T
clawd 66812f9db2 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
2026-02-01 11:50:52 +01:00

328 lines
9.9 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className="progress-page loading">
<div className="spinner"></div>
<p>Laddar progress...</p>
</div>
)
}
return (
<div className="progress-page">
<header className="page-header">
<button className="back-btn" onClick={onBack}> Tillbaka</button>
<h1>Min progress</h1>
<div style={{ width: 40 }}></div>
</header>
<main className="page-main">
{/* Tab Navigation */}
<div className="progress-tabs">
<button
className={`tab-btn ${activeTab === 'weight' ? 'active' : ''}`}
onClick={() => setActiveTab('weight')}
>
Vikt
</button>
<button
className={`tab-btn ${activeTab === 'bodyfat' ? 'active' : ''}`}
onClick={() => setActiveTab('bodyfat')}
>
📊 Kroppsfett
</button>
<button
className={`tab-btn ${activeTab === 'strength' ? 'active' : ''}`}
onClick={() => setActiveTab('strength')}
>
💪 Styrka
</button>
</div>
{/* Weight Chart */}
{activeTab === 'weight' && (
<section className="chart-section">
<h2>Viktutveckling</h2>
{measurements.length > 0 ? (
<>
<SimpleLineChart
data={measurements}
valueKey="weight"
unit="kg"
color="var(--accent)"
/>
<ProgressStats
data={measurements}
valueKey="weight"
unit="kg"
label="Vikt"
/>
</>
) : (
<EmptyState message="Inga viktmätningar registrerade" />
)}
</section>
)}
{/* Body Fat Chart */}
{activeTab === 'bodyfat' && (
<section className="chart-section">
<h2>Kroppsfett</h2>
{measurements.filter(m => m.body_fat_pct).length > 0 ? (
<>
<SimpleLineChart
data={measurements.filter(m => m.body_fat_pct)}
valueKey="body_fat_pct"
unit="%"
color="#10b981"
/>
<ProgressStats
data={measurements.filter(m => m.body_fat_pct)}
valueKey="body_fat_pct"
unit="%"
label="Kroppsfett"
/>
</>
) : (
<EmptyState message="Inga kroppsfettmätningar registrerade" />
)}
</section>
)}
{/* Strength Charts */}
{activeTab === 'strength' && (
<section className="chart-section">
<h2>Styrkerekord (1RM)</h2>
{strength.length > 0 ? (
<div className="strength-charts">
<div className="strength-chart-item">
<h3>🏋 Bänkpress</h3>
<SimpleLineChart
data={strength.filter(s => s.bench_1rm)}
valueKey="bench_1rm"
unit="kg"
color="#f59e0b"
/>
<ProgressStats
data={strength.filter(s => s.bench_1rm)}
valueKey="bench_1rm"
unit="kg"
label="Bänkpress"
/>
</div>
<div className="strength-chart-item">
<h3>🦵 Knäböj</h3>
<SimpleLineChart
data={strength.filter(s => s.squat_1rm)}
valueKey="squat_1rm"
unit="kg"
color="#8b5cf6"
/>
<ProgressStats
data={strength.filter(s => s.squat_1rm)}
valueKey="squat_1rm"
unit="kg"
label="Knäböj"
/>
</div>
<div className="strength-chart-item">
<h3>💀 Marklyft</h3>
<SimpleLineChart
data={strength.filter(s => s.deadlift_1rm)}
valueKey="deadlift_1rm"
unit="kg"
color="#ef4444"
/>
<ProgressStats
data={strength.filter(s => s.deadlift_1rm)}
valueKey="deadlift_1rm"
unit="kg"
label="Marklyft"
/>
</div>
</div>
) : (
<EmptyState message="Inga styrkerekord registrerade" />
)}
</section>
)}
</main>
</div>
)
}
// 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 (
<div className="chart-container">
<svg viewBox={`0 0 ${width} ${height}`} className="line-chart">
{/* Grid lines */}
{[0, 0.5, 1].map((ratio, i) => (
<line
key={i}
x1={padding.left}
y1={padding.top + chartHeight * (1 - ratio)}
x2={width - padding.right}
y2={padding.top + chartHeight * (1 - ratio)}
stroke="var(--border)"
strokeDasharray="4"
/>
))}
{/* Y-axis labels */}
{yLabels.map((label, i) => (
<text
key={i}
x={padding.left - 8}
y={padding.top + chartHeight * (1 - i * 0.5) + 4}
textAnchor="end"
fontSize="10"
fill="var(--text-muted)"
>
{label}
</text>
))}
{/* Line */}
<path
d={pathD}
fill="none"
stroke={color}
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Points */}
{points.map((p, i) => (
<circle
key={i}
cx={p.x}
cy={p.y}
r="4"
fill={color}
/>
))}
</svg>
<div className="chart-labels">
<span>{formatDate(data[0]?.created_at)}</span>
<span>{formatDate(data[data.length - 1]?.created_at)}</span>
</div>
</div>
)
}
// 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 (
<div className="progress-stats">
<div className="stat-item">
<span className="stat-label">Nuvarande</span>
<span className="stat-value">{current} {unit}</span>
</div>
<div className="stat-item">
<span className="stat-label">Första</span>
<span className="stat-value">{first} {unit}</span>
</div>
<div className="stat-item">
<span className="stat-label">Förändring</span>
<span className={`stat-value trend-${trendClass}`}>
{trend} {Math.abs(change).toFixed(1)} {unit} ({changePercent}%)
</span>
</div>
</div>
)
}
function EmptyState({ message }) {
return (
<div className="empty-state">
<span className="empty-icon">📊</span>
<p>{message}</p>
<p className="empty-hint">Logga mätningar för att se din progress</p>
</div>
)
}
function formatDate(dateStr) {
if (!dateStr) return '-'
const date = new Date(dateStr)
return date.toLocaleDateString('sv-SE', { month: 'short', day: 'numeric' })
}
export default ProgressPage