diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 881e75e..823e9b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -42,7 +42,7 @@ jobs: run: npm run test:coverage - name: Upload coverage reports - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 if: always() with: files: ./coverage/lcov.info @@ -56,7 +56,7 @@ jobs: run: npm run build - name: Upload build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: dist path: dist/ diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index edd0bd6..3395bfb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,10 +23,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -40,10 +40,10 @@ jobs: BASE_URL: /${{ github.event.repository.name }}/ - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v6 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: dist/ @@ -62,7 +62,7 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 e2e-test: name: E2E Tests @@ -71,10 +71,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -105,7 +105,7 @@ jobs: BASE_URL: ${{ needs.deploy.outputs.page_url }} - name: Upload test results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: playwright-report diff --git a/src/components/exercises/ExerciseForm.tsx b/src/components/exercises/ExerciseForm.tsx index 89d066f..59ce68a 100644 --- a/src/components/exercises/ExerciseForm.tsx +++ b/src/components/exercises/ExerciseForm.tsx @@ -123,6 +123,7 @@ export default function ExerciseForm({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + e.stopPropagation(); setError(null); if (!name.trim()) { diff --git a/src/components/mesocycles/SplitDayEditor.tsx b/src/components/mesocycles/SplitDayEditor.tsx index 8ea1fa7..fd82ea4 100644 --- a/src/components/mesocycles/SplitDayEditor.tsx +++ b/src/components/mesocycles/SplitDayEditor.tsx @@ -10,7 +10,7 @@ import type { } from '../../types/models'; import ExerciseSelector from '../workouts/ExerciseSelector'; import { isExerciseValidForSplitDay } from '../../lib/splitUtils'; -import { getExerciseWorkoutCount } from '../../db/service'; +import { getExerciseWorkoutCount, createExercise } from '../../db/service'; import '../common/shared-dialog.css'; import './SplitDayEditor.css'; @@ -38,13 +38,15 @@ export default function SplitDayEditor({ const handleAddExercise = (exerciseId: string) => { const exercise = exercises.find((e) => e.id === exerciseId); - if (!exercise) return; - // Validate muscle groups - if (!isExerciseValidForSplitDay(exercise.muscleGroups, splitDay.name)) { - alert( - `Warning: ${exercise.name} may not be appropriate for ${splitDay.name}. The muscle groups don't match the split focus.` - ); + // Validate muscle groups (only if exercise is in the current list; + // a newly created exercise may not have propagated yet via useLiveQuery) + if (exercise) { + if (!isExerciseValidForSplitDay(exercise.muscleGroups, splitDay.name)) { + alert( + `Warning: ${exercise.name} may not be appropriate for ${splitDay.name}. The muscle groups don't match the split focus.` + ); + } } const newExercise: MesocycleExercise = { @@ -63,6 +65,12 @@ export default function SplitDayEditor({ setShowExerciseSelector(false); }; + const handleCreateExercise = async ( + exerciseData: Omit + ): Promise => { + return await createExercise(exerciseData); + }; + const handleRemoveExercise = async (index: number) => { const exercise = splitDay.exercises[index]; @@ -293,6 +301,7 @@ export default function SplitDayEditor({ selectedExerciseIds={selectedExerciseIds} onSelect={handleAddExercise} onClose={() => setShowExerciseSelector(false)} + onCreateExercise={handleCreateExercise} /> )} diff --git a/src/components/workouts/ExerciseSelector.css b/src/components/workouts/ExerciseSelector.css index 132d9b9..d42a6bc 100644 --- a/src/components/workouts/ExerciseSelector.css +++ b/src/components/workouts/ExerciseSelector.css @@ -246,6 +246,34 @@ color: #9ca3af; } +/* Create New Exercise Button */ +.create-exercise-btn { + border-style: dashed !important; + border-color: #3b82f6 !important; + background: #eff6ff !important; +} + +.create-exercise-btn:hover { + background: #dbeafe !important; + border-color: #2563eb !important; +} + +.create-exercise-btn .exercise-item-name { + color: #3b82f6; +} + +.create-exercise-btn .create-hint { + background-color: transparent; + color: #6b7280; + font-style: italic; +} + +/* Exercise Form Portal - renders above ExerciseSelector (z-index: 1100) */ +.exercise-form-portal { + position: relative; + z-index: 1200; +} + /* Mobile Responsive */ @media (max-width: 640px) { .modal-overlay { diff --git a/src/components/workouts/ExerciseSelector.tsx b/src/components/workouts/ExerciseSelector.tsx index c0f7ab9..9894246 100644 --- a/src/components/workouts/ExerciseSelector.tsx +++ b/src/components/workouts/ExerciseSelector.tsx @@ -3,7 +3,9 @@ */ import { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import type { Exercise, MuscleGroup } from '../../types/models'; +import ExerciseForm from '../exercises/ExerciseForm'; import './ExerciseSelector.css'; interface ExerciseSelectorProps { @@ -11,6 +13,9 @@ interface ExerciseSelectorProps { selectedExerciseIds: string[]; onSelect: (exerciseId: string) => void; onClose: () => void; + onCreateExercise?: ( + exercise: Omit + ) => Promise; } // Define muscle group categories for filtering @@ -32,6 +37,7 @@ export default function ExerciseSelector({ selectedExerciseIds, onSelect, onClose, + onCreateExercise, }: ExerciseSelectorProps) { const [searchQuery, setSearchQuery] = useState(''); const [filterCategory, setFilterCategory] = useState< @@ -39,6 +45,7 @@ export default function ExerciseSelector({ >('all'); const [filterMuscleGroup, setFilterMuscleGroup] = useState('all'); + const [showCreateForm, setShowCreateForm] = useState(false); const filteredExercises = exercises.filter((exercise) => { const matchesSearch = exercise.name @@ -129,6 +136,15 @@ export default function ExerciseSelector({ onClose(); }; + const handleCreateExerciseSave = async ( + exerciseData: Omit + ) => { + if (!onCreateExercise) return; + const newId = await onCreateExercise(exerciseData); + setShowCreateForm(false); + onSelect(newId); + }; + // Lock body scroll when modal is open useEffect(() => { const originalStyle = window.getComputedStyle(document.body).overflow; @@ -217,6 +233,26 @@ export default function ExerciseSelector({
+ {onCreateExercise && ( + + )} + {filteredExercises.length === 0 && (

No exercises found. Try adjusting your search or filters. @@ -246,6 +282,19 @@ export default function ExerciseSelector({ ))}

+ + {showCreateForm && + onCreateExercise && + createPortal( +
+ setShowCreateForm(false)} + /> +
, + document.body + )} );