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:
2026-02-01 11:50:52 +01:00
parent b034bb7b11
commit 66812f9db2
5 changed files with 1024 additions and 4 deletions
+327
View File
@@ -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 (
<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