Spaces:
Running
Running
| import { useState } from 'react'; | |
| import './RunForm.css'; | |
| const INJURY_LOCATIONS = [ | |
| { key: 'left_knee', label: 'Left Knee' }, | |
| { key: 'right_knee', label: 'Right Knee' }, | |
| ]; | |
| const RPE_DESCRIPTIONS = { | |
| 1: { label: 'Very easy', detail: 'Gentle warm-up, recovery jog' }, | |
| 2: { label: 'Very easy', detail: 'Full conversation easy' }, | |
| 3: { label: 'Easy', detail: 'Easy run, full sentences' }, | |
| 4: { label: 'Comfortable', detail: 'Can talk in full sentences' }, | |
| 5: { label: 'Moderate', detail: 'Talking becomes slightly harder' }, | |
| 6: { label: 'Comfortably hard', detail: 'Tempo effort, short sentences only' }, | |
| 7: { label: 'Hard', detail: 'Threshold pace, conversation difficult' }, | |
| 8: { label: 'Very hard', detail: 'Interval pace, a few words at a time' }, | |
| 9: { label: 'Extremely hard', detail: 'Near max, sustain only briefly' }, | |
| 10: { label: 'All-out', detail: 'Max sprint, lasts only seconds' }, | |
| }; | |
| function RunForm({ onAddRun }) { | |
| const today = new Date().toISOString().split('T')[0]; | |
| const [date, setDate] = useState(today); | |
| const [distance, setDistance] = useState(''); | |
| const [time, setTime] = useState(''); | |
| const [rpe, setRpe] = useState(5); | |
| const [notes, setNotes] = useState(''); | |
| const [injuries, setInjuries] = useState( | |
| Object.fromEntries(INJURY_LOCATIONS.map((loc) => [loc.key, { enabled: false, during: '', after: '' }])) | |
| ); | |
| function handleSubmit(e) { | |
| e.preventDefault(); | |
| const dist = parseFloat(distance); | |
| const mins = parseFloat(time); | |
| if (!date || isNaN(dist) || dist <= 0 || isNaN(mins) || mins <= 0) return; | |
| const runData = { | |
| date, | |
| distance_km: dist, | |
| time_minutes: mins, | |
| rpe: Number(rpe), | |
| notes: notes.trim(), | |
| }; | |
| for (const loc of INJURY_LOCATIONS) { | |
| const inj = injuries[loc.key]; | |
| if (inj.enabled) { | |
| runData[`${loc.key}_during`] = inj.during ? Number(inj.during) : null; | |
| runData[`${loc.key}_after`] = inj.after ? Number(inj.after) : null; | |
| } | |
| } | |
| onAddRun(runData); | |
| setDistance(''); | |
| setTime(''); | |
| setRpe(5); | |
| setNotes(''); | |
| setInjuries( | |
| Object.fromEntries(INJURY_LOCATIONS.map((loc) => [loc.key, { enabled: false, during: '', after: '' }])) | |
| ); | |
| } | |
| return ( | |
| <form className="run-form card" onSubmit={handleSubmit}> | |
| <h2>Log a Run</h2> | |
| <div className="form-group"> | |
| <label htmlFor="run-date">Date</label> | |
| <input | |
| id="run-date" | |
| type="date" | |
| value={date} | |
| onChange={(e) => setDate(e.target.value)} | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label htmlFor="run-distance">Distance (km)</label> | |
| <input | |
| id="run-distance" | |
| type="number" | |
| step="0.01" | |
| min="0.01" | |
| placeholder="e.g. 5.25" | |
| value={distance} | |
| onChange={(e) => setDistance(e.target.value)} | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label htmlFor="run-time">Time (minutes)</label> | |
| <input | |
| id="run-time" | |
| type="number" | |
| step="0.01" | |
| min="0.01" | |
| placeholder="e.g. 30" | |
| value={time} | |
| onChange={(e) => setTime(e.target.value)} | |
| required | |
| /> | |
| </div> | |
| <div className="form-group"> | |
| <label htmlFor="run-rpe">Effort (RPE): <strong>{rpe}</strong></label> | |
| <input | |
| id="run-rpe" | |
| type="range" | |
| min="1" | |
| max="10" | |
| value={rpe} | |
| onChange={(e) => setRpe(e.target.value)} | |
| /> | |
| <div className="rpe-labels"> | |
| <span>Easy</span> | |
| <span>Max</span> | |
| </div> | |
| <div className="rpe-description"> | |
| <span className="rpe-effort">{RPE_DESCRIPTIONS[rpe].label}</span> | |
| <span className="rpe-detail">{RPE_DESCRIPTIONS[rpe].detail}</span> | |
| </div> | |
| </div> | |
| <div className="form-group"> | |
| <label>Injury Tracking</label> | |
| <div className="injury-checkboxes"> | |
| {INJURY_LOCATIONS.map((loc) => ( | |
| <label key={loc.key} className="injury-checkbox-label"> | |
| <input | |
| type="checkbox" | |
| checked={injuries[loc.key].enabled} | |
| onChange={(e) => | |
| setInjuries((prev) => ({ | |
| ...prev, | |
| [loc.key]: { ...prev[loc.key], enabled: e.target.checked, ...(!e.target.checked && { during: '', after: '' }) }, | |
| })) | |
| } | |
| /> | |
| {loc.label} | |
| </label> | |
| ))} | |
| </div> | |
| </div> | |
| {INJURY_LOCATIONS.filter((loc) => injuries[loc.key].enabled).map((loc) => ( | |
| <div key={loc.key} className="pain-inputs"> | |
| <span className="pain-location-label">{loc.label}</span> | |
| <div className="form-group pain-field"> | |
| <label htmlFor={`pain-during-${loc.key}`}>Pain During (1–10)</label> | |
| <input | |
| id={`pain-during-${loc.key}`} | |
| type="number" min="1" max="10" step="1" placeholder="1–10" | |
| value={injuries[loc.key].during} | |
| onChange={(e) => | |
| setInjuries((prev) => ({ | |
| ...prev, | |
| [loc.key]: { ...prev[loc.key], during: e.target.value }, | |
| })) | |
| } | |
| /> | |
| </div> | |
| <div className="form-group pain-field"> | |
| <label htmlFor={`pain-after-${loc.key}`}>Pain After (1–10)</label> | |
| <input | |
| id={`pain-after-${loc.key}`} | |
| type="number" min="1" max="10" step="1" placeholder="1–10" | |
| value={injuries[loc.key].after} | |
| onChange={(e) => | |
| setInjuries((prev) => ({ | |
| ...prev, | |
| [loc.key]: { ...prev[loc.key], after: e.target.value }, | |
| })) | |
| } | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| <div className="form-group"> | |
| <label htmlFor="run-notes">Notes</label> | |
| <textarea | |
| id="run-notes" | |
| placeholder="How did it feel? Any observations..." | |
| value={notes} | |
| onChange={(e) => setNotes(e.target.value)} | |
| rows="3" | |
| /> | |
| </div> | |
| <button type="submit" className="btn-primary">Add Run</button> | |
| </form> | |
| ); | |
| } | |
| export default RunForm; | |