From cf85e9e3144f62576b4d1fc6ea5513205b499d24 Mon Sep 17 00:00:00 2001 From: Clawd Agent Date: Sun, 1 Mar 2026 20:44:45 +0100 Subject: [PATCH] 04-05: Reset to Original feature - custom workouts can be reverted to program versions - Added reset button (refresh icon) to custom workout cards - Implemented confirmation dialog to prevent accidental resets - Integrated with DELETE /api/custom-workouts/:id endpoint - Added CSS styling: reset button, success message, modal dialog - Added refresh icon to SVG library - Frontend build successful Changes: - frontend/src/pages/WorkoutSelectPage.jsx (reset flow logic) - frontend/src/App.css (170 new lines for reset/modal styling) - frontend/src/components/Icons.jsx (refresh icon) - Checkpoint updated with task completion metadata --- .pm-checkpoint.json | 25 ++-- frontend/src/App.css | 170 +++++++++++++++++++++++ frontend/src/components/Icons.jsx | 6 + frontend/src/pages/WorkoutSelectPage.jsx | 96 +++++++++++++ 4 files changed, 288 insertions(+), 9 deletions(-) diff --git a/.pm-checkpoint.json b/.pm-checkpoint.json index c42c6aa..0935566 100644 --- a/.pm-checkpoint.json +++ b/.pm-checkpoint.json @@ -1,13 +1,20 @@ { - "lastRun": "2026-03-01T17:38:00+01:00", + "lastRun": "2026-03-01T20:42:00+01:00", "status": "completed", "phase": "04-workout-modification", - "activeTask": "04-03-frontend-workout-edit", - "tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api", "04-03-frontend-workout-edit"], - "nextTask": "04-04-visual-distinction", - "agentSession": "claude-code-frontend", - "agentType": "claude-code-local-exec", - "spawnTime": "2026-03-01T17:38:00+01:00", - "result": "Phase 04-03 complete. Edit workflow implemented: ExercisePicker modal, swap/add/remove exercise flows, fork confirmation dialog, API integration (POST/PUT custom-workouts). All success criteria met. Ready for 04-04.", - "notes": "Previous attempt hit Gemini quota limit. Recovered at 17:38. Advancing to 04-04: Add visual distinction badges (custom vs program) on WorkoutSelectPage." + "activeTask": "04-05-reset-to-original", + "tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api", "04-03-frontend-workout-edit", "04-04-visual-distinction", "04-05-reset-to-original"], + "nextTask": "04-06-persistence-improvements", + "agentSession": "local-exec", + "agentType": "gravl-pm-cron", + "spawnTime": "2026-03-01T20:42:00+01:00", + "result": "Phase 04-05 complete. Reset to Original feature fully implemented. Changes: 1) Added refresh button to custom workout cards (visible only on 'Anpassad' workouts). 2) Implemented handleResetClick + handleConfirmReset flow with confirmation dialog ('Är du säker? Dina ändringar kommer att försvinna...'). 3) DELETE /api/custom-workouts/:id endpoint verified (exists in backend). 4) Added CSS styling: .reset-btn (orange icon button with hover effects), .success-message (green slide-down animation), .modal-overlay/.modal-dialog/.modal-btn (reusable confirmation dialog). 5) Added refresh icon to Icons.jsx SVG library. Frontend build successful with no errors.", + "notes": "Task 04-05 complete. Custom workouts can now be reset to original program versions. User gets confirmation dialog before deletion. UI updates show badge change from 'Anpassad' to 'Program' after reset. Next: 04-06 (persistence improvements or advanced features like workout export/backup).", + "filesModified": [ + "frontend/src/pages/WorkoutSelectPage.jsx", + "frontend/src/App.css", + "frontend/src/components/Icons.jsx" + ], + "buildStatus": "success", + "buildTime": "3.59s" } diff --git a/frontend/src/App.css b/frontend/src/App.css index 1893ffc..33ef817 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2998,3 +2998,173 @@ .workout-select-card:hover .workout-badge { box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); } + +/* Reset button for custom workouts */ +.reset-btn { + position: absolute; + top: -8px; + right: -8px; + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: var(--accent); + border: 2px solid var(--bg-primary); + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(255, 107, 74, 0.3); + padding: 0; + min-width: 32px; + min-height: 32px; +} + +.reset-btn:hover { + background: #e85a3c; + transform: scale(1.1); + box-shadow: 0 4px 12px rgba(255, 107, 74, 0.4); +} + +.reset-btn:active { + transform: scale(0.95); +} + +/* Success message */ +.success-message { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + background: linear-gradient(135deg, var(--success), #16a34a); + color: white; + border-radius: var(--radius-lg); + margin-bottom: var(--space-4); + animation: slideDown 0.3s ease; + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2); +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Modal dialog styles */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-dialog { + background: var(--bg-primary); + border-radius: var(--radius-xl); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + max-width: 400px; + width: 90%; + animation: slideUp 0.3s ease; + overflow: hidden; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-header { + padding: var(--space-4); + border-bottom: 1px solid var(--border); +} + +.modal-header h2 { + font-size: var(--font-lg); + font-weight: 700; + margin: 0; + color: var(--text-primary); +} + +.modal-body { + padding: var(--space-4); +} + +.modal-body p { + font-size: var(--font-md); + color: var(--text-secondary); + line-height: 1.5; + margin: 0; +} + +.modal-footer { + padding: var(--space-4); + border-top: 1px solid var(--border); + display: flex; + gap: var(--space-2); + justify-content: flex-end; +} + +.modal-btn { + padding: var(--space-2) var(--space-4); + border-radius: var(--radius-md); + border: none; + cursor: pointer; + font-size: var(--font-md); + font-weight: 600; + transition: all 0.2s ease; + min-height: 40px; +} + +.modal-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.modal-btn.cancel { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border); +} + +.modal-btn.cancel:hover:not(:disabled) { + background: var(--bg-tertiary); + border-color: var(--border); +} + +.modal-btn.confirm { + background: var(--accent); + color: white; +} + +.modal-btn.confirm:hover:not(:disabled) { + background: #e85a3c; + box-shadow: 0 4px 12px rgba(255, 107, 74, 0.3); +} + +.modal-btn.confirm:active:not(:disabled) { + transform: scale(0.98); +} diff --git a/frontend/src/components/Icons.jsx b/frontend/src/components/Icons.jsx index 4a59f61..ef37a01 100644 --- a/frontend/src/components/Icons.jsx +++ b/frontend/src/components/Icons.jsx @@ -261,6 +261,12 @@ export const Icons = { ), + refresh: ( + + + + + ), } // Icon component wrapper diff --git a/frontend/src/pages/WorkoutSelectPage.jsx b/frontend/src/pages/WorkoutSelectPage.jsx index 564c804..8967f9b 100644 --- a/frontend/src/pages/WorkoutSelectPage.jsx +++ b/frontend/src/pages/WorkoutSelectPage.jsx @@ -20,12 +20,23 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { const [customWorkouts, setCustomWorkouts] = useState([]) const [loading, setLoading] = useState(true) const [selectedWorkout, setSelectedWorkout] = useState(null) + const [resetConfirm, setResetConfirm] = useState(null) + const [resetting, setResetting] = useState(false) + const [successMessage, setSuccessMessage] = useState(null) useEffect(() => { fetchProgram() fetchCustomWorkouts() }, []) + // Auto-clear success message after 3 seconds + useEffect(() => { + if (successMessage) { + const timer = setTimeout(() => setSuccessMessage(null), 3000) + return () => clearTimeout(timer) + } + }, [successMessage]) + const fetchProgram = async () => { try { const res = await fetch(`${API_URL}/programs/1`) @@ -52,6 +63,11 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { } } + const getCustomWorkoutId = (programDayId) => { + const customWorkout = customWorkouts.find(cw => cw.source_program_day_id === programDayId) + return customWorkout?.id + } + const isWorkoutCustom = (programDayId) => { return customWorkouts.some(cw => cw.source_program_day_id === programDayId) } @@ -66,6 +82,38 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { } } + const handleResetClick = (e, workoutId) => { + e.stopPropagation() + setResetConfirm(workoutId) + } + + const handleConfirmReset = async () => { + if (!resetConfirm) return + + setResetting(true) + try { + const token = localStorage.getItem('token') + const res = await fetch(`${API_URL}/custom-workouts/${resetConfirm}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }) + + if (res.ok) { + // Refresh custom workouts list + await fetchCustomWorkouts() + setSuccessMessage('Passet återställdes till original') + setSelectedWorkout(null) + setResetConfirm(null) + } else { + console.error('Failed to reset workout:', res.status) + } + } catch (err) { + console.error('Error resetting workout:', err) + } finally { + setResetting(false) + } + } + if (loading) { return (
@@ -90,6 +138,13 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { Vilken träning vill du köra idag?

+ {successMessage && ( +
+ + {successMessage} +
+ )} +
{program?.days?.map((workout) => { const iconName = getWorkoutIconName(workout.name) @@ -97,6 +152,7 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) { const isSelected = selectedWorkout?.id === workout.id const exerciseCount = workout.exercises?.filter(e => e.name).length || 0 const isCustom = isWorkoutCustom(workout.id) + const customWorkoutId = getCustomWorkoutId(workout.id) return (
{isCustom ? 'Anpassad' : 'Program'} + {isCustom && ( + + )}

{workout.name}

@@ -146,6 +212,36 @@ function WorkoutSelectPage({ onBack, onSelectWorkout }) {
)} + + {/* Reset confirmation dialog */} + {resetConfirm && ( +
setResetConfirm(null)}> +
e.stopPropagation()}> +
+

Återställ till original?

+
+
+

Är du säker? Dina ändringar kommer att försvinna och passet återställs till programversionen.

+
+
+ + +
+
+
+ )}
) }