From f012392de94a798cd43a0428ed01ba43ef7a4210 Mon Sep 17 00:00:00 2001 From: Clawd Agent Date: Mon, 2 Mar 2026 01:54:04 +0100 Subject: [PATCH] 04-06-02: Save error handling & retry logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added specific error type differentiation: * Network errors → 'Anslutning misslyckades' * Validation (400) → 'Ogiltiga ändringar' * Auth (401/403) → 'Saknar behörighet' * Server (500+) → 'Serverfel' * Generic fallback messages - Implemented retry tracking: * retryCount state for monitoring attempts * lastSavePayload storage for potential retry (future feature) * Console logging with context for debugging - Enhanced error handling: * getErrorMessage() function for error classification * Comprehensive error logging with workout/exercise context * Draft preserved on all error types (no data loss) - Improved UI/UX: * Error banner with specific, actionable messages * 'Försök igen' button with retry tracking * Sync status feedback (idle/saving/saved/error) * Success checkmark animation (2s duration) * Spinner animation during save - CSS Enhancements: * @keyframes spin for loading spinner * @keyframes slideInCheckmark for success feedback * Mobile-responsive error banner (flex column on <480px) * Smooth animations for state transitions Tests: npm run build ✓ (no syntax errors) Files modified: - frontend/src/pages/WorkoutEditPage.jsx - frontend/src/pages/WorkoutEditPage.css --- frontend/src/pages/WorkoutEditPage.css | 50 +++++++++++++++++++ frontend/src/pages/WorkoutEditPage.jsx | 67 +++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/WorkoutEditPage.css b/frontend/src/pages/WorkoutEditPage.css index 28c09ff..4d24f44 100644 --- a/frontend/src/pages/WorkoutEditPage.css +++ b/frontend/src/pages/WorkoutEditPage.css @@ -434,3 +434,53 @@ max-width: 90%; } } + +/* Spinner Animation for Save Loading */ +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Apply spinner animation to Icon component with spinner class */ +.save-header-btn svg[class*="spinner"], +.save-header-btn .icon-spinner { + animation: spin 1s linear infinite; +} + +/* Success Checkmark Animation */ +@keyframes slideInCheckmark { + 0% { + opacity: 0; + transform: scale(0.8); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.sync-status.saved { + animation: slideInCheckmark 0.3s ease-out; +} + +/* Ensure error actions align properly on mobile */ +@media (max-width: 480px) { + .error-banner { + flex-direction: column; + align-items: flex-start; + } + + .error-message { + width: 100%; + margin-bottom: 0.75rem; + } + + .error-actions { + width: 100%; + justify-content: space-between; + } +} diff --git a/frontend/src/pages/WorkoutEditPage.jsx b/frontend/src/pages/WorkoutEditPage.jsx index 8f461b5..46101b1 100644 --- a/frontend/src/pages/WorkoutEditPage.jsx +++ b/frontend/src/pages/WorkoutEditPage.jsx @@ -14,6 +14,8 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) { const [error, setError] = useState(null) const [syncStatus, setSyncStatus] = useState('idle') // idle | saving | saved | error const [draftPromptShown, setDraftPromptShown] = useState(false) + const [retryCount, setRetryCount] = useState(0) + const [lastSavePayload, setLastSavePayload] = useState(null) // Show draft recovery prompt on first render const handleRecoverDraft = () => { @@ -72,10 +74,41 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) { if (error) setError(null) } + /** + * Determine specific error message based on error type + */ + const getErrorMessage = (err) => { + // Network errors + if (!err || err instanceof TypeError && err.message.includes('fetch')) { + return 'Anslutning misslyckades. Försök igen?' + } + + // Check if error has a response (API error) + if (err.status) { + if (err.status === 400) { + return 'Ogiltiga ändringar. Kontrollera dina inmatningar.' + } + if (err.status === 401 || err.status === 403) { + return 'Du har inte behörighet att spara denna träning.' + } + if (err.status >= 500) { + return 'Serverfel. Försök igen senare.' + } + if (err.status >= 400) { + return 'Ett fel uppstod när träningen skulle sparas. Försök igen.' + } + } + + // Fallback + return err.message || 'Sparning misslyckades. Försök igen.' + } + const handleSave = async () => { setSaving(true) setSyncStatus('saving') setError(null) + setRetryCount(prev => prev + 1) + try { // Format for API const payload = { @@ -86,25 +119,55 @@ export default function WorkoutEditPage({ workout, onBack, onSave }) { reps_max: parseInt(ex.reps_max) || 12 })) } + + // Store payload for potential retry + setLastSavePayload(payload) + + // Call the save callback await onSave(workout.id, payload) // Success: clear draft and show confirmation clearDraft() setSyncStatus('saved') + setRetryCount(0) // Reset retry count on success + + // Log success + console.log('Workout saved successfully', { + workoutId: workout.id, + exerciseCount: exercises.length, + retryCount + }) // Reset status after 2 seconds setTimeout(() => setSyncStatus('idle'), 2000) } catch (err) { - console.error('Failed to save workout:', err) - setError(err.message || 'Sparning misslyckades. Försök igen.') + // Log error with context for debugging + console.error('Failed to save workout:', { + error: err, + workoutId: workout.id, + exerciseCount: exercises.length, + retryCount, + payload: lastSavePayload + }) + + // Determine error message based on error type + const errorMessage = getErrorMessage(err) + setError(errorMessage) setSyncStatus('error') + // Keep draft on error so user doesn't lose work + // (useDraftWorkout already auto-saves, so no action needed here) } finally { setSaving(false) } } const handleRetry = () => { + // Log retry attempt + console.log('User retrying save', { + workoutId: workout.id, + retryCount + }) handleSave() }