feat(04-03-partial): ExercisePicker and WorkoutEditPage components - swap/add/remove exercises with sets/reps editing
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Icon } from './Icons'
|
||||
|
||||
const API_URL = '/api'
|
||||
|
||||
function ExercisePicker({ open, onSelect, onClose, excludeIds = [] }) {
|
||||
const [exercises, setExercises] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const [activeGroup, setActiveGroup] = useState('Alla')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
fetchExercises()
|
||||
setSearch('')
|
||||
setActiveGroup('Alla')
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const fetchExercises = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/exercises`)
|
||||
if (!res.ok) throw new Error('Failed to fetch')
|
||||
const data = await res.json()
|
||||
setExercises(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch exercises:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const muscleGroups = useMemo(() => {
|
||||
const groups = new Set(exercises.map(e => e.muscle_group).filter(Boolean))
|
||||
return ['Alla', ...Array.from(groups).sort()]
|
||||
}, [exercises])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return exercises.filter(ex => {
|
||||
if (excludeIds.includes(ex.id)) return false
|
||||
if (activeGroup !== 'Alla' && ex.muscle_group !== activeGroup) return false
|
||||
if (search) {
|
||||
const q = search.toLowerCase()
|
||||
return ex.name.toLowerCase().includes(q) ||
|
||||
(ex.muscle_group || '').toLowerCase().includes(q)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}, [exercises, search, activeGroup, excludeIds])
|
||||
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="exercise-picker-overlay" onClick={onClose}>
|
||||
<div className="exercise-picker" onClick={e => e.stopPropagation()}>
|
||||
<div className="exercise-picker-header">
|
||||
<h2>Välj övning</h2>
|
||||
<button className="exercise-picker-close" onClick={onClose} aria-label="Stäng">
|
||||
<Icon name="chevronDown" size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="exercise-picker-search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Sök övning..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="exercise-picker-filters">
|
||||
{muscleGroups.map(group => (
|
||||
<button
|
||||
key={group}
|
||||
className={`filter-chip ${activeGroup === group ? 'active' : ''}`}
|
||||
onClick={() => setActiveGroup(group)}
|
||||
>
|
||||
{group}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="exercise-picker-list">
|
||||
{loading && <div className="exercise-picker-state">Laddar övningar...</div>}
|
||||
|
||||
{!loading && filtered.length === 0 && (
|
||||
<div className="exercise-picker-state">Inga övningar hittades.</div>
|
||||
)}
|
||||
|
||||
{!loading && filtered.map(ex => (
|
||||
<button
|
||||
key={ex.id}
|
||||
className="exercise-picker-item"
|
||||
onClick={() => onSelect(ex)}
|
||||
>
|
||||
<div className="exercise-picker-item-info">
|
||||
<strong>{ex.name}</strong>
|
||||
<span className="exercise-picker-item-group">{ex.muscle_group}</span>
|
||||
</div>
|
||||
<Icon name="arrowLeft" size={16} style={{ transform: 'rotate(180deg)', opacity: 0.4 }} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ExercisePicker
|
||||
@@ -234,6 +234,24 @@ export const Icons = {
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||||
</svg>
|
||||
),
|
||||
edit: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
),
|
||||
search: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
),
|
||||
x: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
),
|
||||
|
||||
// Brand
|
||||
gravl: (
|
||||
|
||||
Reference in New Issue
Block a user