design: WorkoutPage Hevy-style redesign + AlternativeModal + backend API

- Add GET /api/exercises/:id/alternatives endpoint
- Add GET /api/exercises/:id/last-workout endpoint
- New AlternativeModal component for swapping exercises
- WorkoutPage: single-tap logging, +/- buttons, rest timer
- Updated Icons with new workout icons
- Polish: card shadows, borders, micro-interactions
- Tasks directory for project management
This commit is contained in:
2026-02-28 21:25:23 +01:00
parent 0e5cec927a
commit 04bab32e26
9 changed files with 2244 additions and 842 deletions
@@ -0,0 +1,51 @@
import { Icon } from './Icons'
function AlternativeModal({ exercise, alternatives, loading, error, onSelect, onClose }) {
if (!exercise) return null
return (
<div className="alternative-modal-overlay" onClick={onClose}>
<div className="alternative-modal" onClick={(event) => event.stopPropagation()}>
<div className="alternative-modal-header">
<div>
<h3>Alternativa övningar</h3>
<p>För {exercise.name}</p>
</div>
<button className="alternative-modal-close" onClick={onClose} aria-label="Stäng">
<Icon name="chevronDown" size={18} />
</button>
</div>
{loading && (
<div className="alternative-modal-state">Laddar alternativ...</div>
)}
{!loading && error && (
<div className="alternative-modal-state error">{error}</div>
)}
{!loading && !error && alternatives.length === 0 && (
<div className="alternative-modal-state">Inga alternativ hittades.</div>
)}
{!loading && !error && alternatives.length > 0 && (
<div className="alternative-list">
{alternatives.map((alt) => (
<div key={alt.id} className="alternative-item">
<div className="alternative-info">
<strong>{alt.name}</strong>
<span>{alt.description || 'Ingen beskrivning tillgänglig.'}</span>
</div>
<button className="alternative-select-btn" onClick={() => onSelect(alt)}>
Välj
</button>
</div>
))}
</div>
)}
</div>
</div>
)
}
export default AlternativeModal