feat(frontend): Kinetic Precision design system — new lime theme, glassmorphism, redesigned pages

- New design system: Stitch (kinetic-precision.css) with lime (#cafd00) accent
- New Google Fonts: Lexend, Plus Jakarta Sans, Space Grotesk
- New page: BenchmarksPage with strength/endurance/body tracking
- Redesigned: Dashboard, ProgressPage, WorkoutPage, LoginPage + LoginPage.css
- Add shared glassmorphism nav, kinetic buttons, intensity indicators
- Build: 265KB JS / 88KB CSS / 2.54s (clean)
This commit is contained in:
2026-04-27 08:49:07 +02:00
parent b6c39574c2
commit 1f2a892391
20 changed files with 2380 additions and 516 deletions
+200 -79
View File
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { Icon } from '../components/Icons'
import SwapWorkoutModal from '../components/SwapWorkoutModal'
import '../styles/kinetic-precision.css'
const API_URL = '/api'
@@ -453,6 +454,8 @@ function WorkoutPage({ day, week, logs, onLogSet, onDeleteSet, onBack, fetchProg
onSwap={() => openAlternatives(exercise)}
onUndo={() => undoSwap(exercise.id)}
canUndo={Boolean(recentSwaps[exercise.id])}
exerciseIndex={idx + 1}
totalExercises={exercises.length}
/>
)
})}
@@ -488,7 +491,7 @@ function WorkoutPage({ day, week, logs, onLogSet, onDeleteSet, onBack, fetchProg
)
}
function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSet, onDeleteSet, onSwap, isSwapped, onStartRest, originalExercise, onUndo, canUndo }) {
function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSet, onDeleteSet, onSwap, isSwapped, onStartRest, originalExercise, onUndo, canUndo, exerciseIndex, totalExercises }) {
const [setList, setSetList] = useState([])
const [showAddModal, setShowAddModal] = useState(false)
const weightStep = 2.5
@@ -569,19 +572,62 @@ function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSe
const completedSets = setList.filter(s => s.completed).length
// Compute PR: current set weight exceeds progression last weight
const isPR = (input, idx) => {
const lastWeight = progression?.lastWeight
if (!lastWeight) return false
const w = parseFloat(input.weight)
return !isNaN(w) && w > lastWeight
}
return (
<div className={`exercise-card ${expanded ? 'expanded' : ''} ${completedSets === setList.length && setList.length > 0 ? 'all-done' : ''}`}>
<div className="exercise-header" onClick={onToggle}>
<div className="exercise-info">
<h3>{exercise.name}</h3>
<span className="muscle-group">{exercise.muscle_group}</span>
{isSwapped && originalExercise && (
<span className="swap-badge">Bytt från {originalExercise.name}</span>
{/* EXERCISE FOCUS HEADER */}
<div className="exercise-header" onClick={onToggle} style={{ paddingBottom: expanded ? '0.5rem' : undefined }}>
<div style={{ flex: 1 }}>
{/* Progress indicator */}
{exerciseIndex != null && (
<span style={{
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.65rem',
color: '#767575',
letterSpacing: '0.06em',
textTransform: 'uppercase',
display: 'block',
marginBottom: '0.25rem',
}}>Övning {exerciseIndex} av {totalExercises}</span>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<h3 style={{
fontFamily: 'Lexend, sans-serif',
fontWeight: 700,
fontSize: '1.1rem',
color: '#ffffff',
margin: 0,
}}>{exercise.name}</h3>
{isSwapped && originalExercise && (
<span className="swap-badge" style={{ fontSize: '0.6rem' }}>Bytt</span>
)}
</div>
{exercise.muscle_group && (
<span style={{
display: 'inline-block',
marginTop: '0.25rem',
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.7rem',
color: '#767575',
letterSpacing: '0.03em',
}}>{exercise.muscle_group}</span>
)}
</div>
<div className="exercise-actions">
<div className="exercise-meta">
<span className="sets-info">{exercise.sets}×{exercise.reps_min}-{exercise.reps_max}</span>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: '0.25rem' }}>
<span style={{
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.75rem',
color: '#adaaaa',
letterSpacing: '0.03em',
}}>{exercise.sets}×{exercise.reps_min}-{exercise.reps_max}</span>
<span className={`progress-badge ${completedSets === setList.length ? 'complete' : ''}`}>
{completedSets}/{setList.length}
</span>
@@ -617,8 +663,9 @@ function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSe
{expanded && (
<div className="exercise-body">
{/* Progression hint */}
{progression && (
<div className="progression-hint">
<div className="progression-hint" style={{ marginBottom: '0.75rem' }}>
{progression.reason}
{progression.suggestedWeight && (
<strong> {progression.suggestedWeight} kg</strong>
@@ -626,80 +673,154 @@ function ExerciseCard({ exercise, logs, progression, expanded, onToggle, onLogSe
</div>
)}
{/* Target line */}
{(exercise.reps_min || exercise.reps_max) && (
<div style={{
background: '#131313',
borderRadius: '6px',
padding: '0.5rem 0.75rem',
marginBottom: '0.75rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<span style={{
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.7rem',
color: '#767575',
letterSpacing: '0.04em',
textTransform: 'uppercase',
}}>Mål</span>
<span style={{
fontFamily: 'Lexend, sans-serif',
fontWeight: 700,
fontSize: '0.875rem',
color: '#adaaaa',
}}>
{exercise.sets} set · {exercise.reps_min}{exercise.reps_max && exercise.reps_max !== exercise.reps_min ? `${exercise.reps_max}` : ''} reps
</span>
</div>
)}
<div className="sets-list">
{setList.map((input, idx) => (
<div key={idx} className={`set-row ${input.completed ? 'completed' : ''}`}>
<div className="set-row-top">
<span className="set-number">Set {idx + 1}</span>
{setList.map((input, idx) => {
const setIsPR = isPR(input, idx)
return (
<div key={idx} className={`set-row ${input.completed ? 'completed' : ''}`}>
<div className="set-row-top">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span className="set-number" style={{
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.75rem',
color: '#767575',
letterSpacing: '0.04em',
textTransform: 'uppercase',
}}>Set {idx + 1}</span>
{setIsPR && (
<span style={{
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.6rem',
fontWeight: 700,
color: '#516700',
background: '#cafd00',
padding: '0.1rem 0.35rem',
borderRadius: '4px',
letterSpacing: '0.04em',
}}>PR</span>
)}
</div>
<button
className={`delete-set-btn ${setList.length <= 1 ? 'disabled' : ''}`}
onClick={() => handleDeleteSet(idx)}
disabled={setList.length <= 1}
aria-label={`Ta bort set ${idx + 1}`}
>
<Icon name="trash" size={16} />
</button>
</div>
<div className="set-controls">
<div className="set-metric">
<span className="metric-label">Vikt</span>
<div className="metric-controls">
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'weight', -weightStep)}
aria-label="Minska vikt"
>
</button>
<div className="metric-value">
<span className="metric-number" style={{
fontFamily: 'Lexend, sans-serif',
color: '#cafd00',
fontSize: '1.35rem',
fontWeight: 700,
}}>{input.weight === '' ? '0' : input.weight}</span>
<span className="metric-suffix">kg</span>
</div>
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'weight', weightStep)}
aria-label="Öka vikt"
>
+
</button>
</div>
</div>
<div className="set-metric">
<span className="metric-label">Reps</span>
<div className="metric-controls">
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'reps', -repsStep)}
aria-label="Minska reps"
>
</button>
<div className="metric-value">
<span className="metric-number" style={{
fontFamily: 'Lexend, sans-serif',
fontSize: '1.35rem',
fontWeight: 700,
}}>{input.reps === '' ? '0' : input.reps}</span>
</div>
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'reps', repsStep)}
aria-label="Öka reps"
>
+
</button>
</div>
</div>
</div>
{/* Previous session reference */}
{progression?.lastWeight && progression?.lastReps && (
<div style={{
fontFamily: 'Space Grotesk, monospace',
fontSize: '0.7rem',
color: '#767575',
letterSpacing: '0.03em',
marginTop: '0.25rem',
marginBottom: '0.25rem',
}}>
Förra träningen: {progression.lastWeight}kg×{progression.lastReps}
</div>
)}
<button
className={`delete-set-btn ${setList.length <= 1 ? 'disabled' : ''}`}
onClick={() => handleDeleteSet(idx)}
disabled={setList.length <= 1}
aria-label={`Ta bort set ${idx + 1}`}
className={`klart-btn ${input.completed ? 'done' : ''}`}
onClick={() => handleComplete(idx)}
>
<Icon name="trash" size={16} />
{input.completed ? <Icon name="check" size={18} /> : null}
KLART
</button>
</div>
<div className="set-controls">
<div className="set-metric">
<span className="metric-label">Vikt</span>
<div className="metric-controls">
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'weight', -weightStep)}
aria-label="Minska vikt"
>
</button>
<div className="metric-value">
<span className="metric-number">{input.weight === '' ? '0' : input.weight}</span>
<span className="metric-suffix">kg</span>
</div>
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'weight', weightStep)}
aria-label="Öka vikt"
>
+
</button>
</div>
</div>
<div className="set-metric">
<span className="metric-label">Reps</span>
<div className="metric-controls">
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'reps', -repsStep)}
aria-label="Minska reps"
>
</button>
<div className="metric-value">
<span className="metric-number">{input.reps === '' ? '0' : input.reps}</span>
</div>
<button
type="button"
className="metric-btn"
onClick={() => handleAdjust(idx, 'reps', repsStep)}
aria-label="Öka reps"
>
+
</button>
</div>
</div>
</div>
<button
className={`klart-btn ${input.completed ? 'done' : ''}`}
onClick={() => handleComplete(idx)}
>
{input.completed ? <Icon name="check" size={18} /> : null}
KLART
</button>
</div>
))}
)
})}
</div>
<button