Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/exercises/ExerciseForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export default function ExerciseForm({

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.stopPropagation();
setError(null);

if (!name.trim()) {
Expand Down
23 changes: 16 additions & 7 deletions src/components/mesocycles/SplitDayEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 = {
Expand All @@ -63,6 +65,12 @@ export default function SplitDayEditor({
setShowExerciseSelector(false);
};

const handleCreateExercise = async (
exerciseData: Omit<Exercise, 'id' | 'createdAt'>
): Promise<string> => {
return await createExercise(exerciseData);
};

const handleRemoveExercise = async (index: number) => {
const exercise = splitDay.exercises[index];

Expand Down Expand Up @@ -293,6 +301,7 @@ export default function SplitDayEditor({
selectedExerciseIds={selectedExerciseIds}
onSelect={handleAddExercise}
onClose={() => setShowExerciseSelector(false)}
onCreateExercise={handleCreateExercise}
/>
)}
</div>
Expand Down
28 changes: 28 additions & 0 deletions src/components/workouts/ExerciseSelector.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 49 additions & 0 deletions src/components/workouts/ExerciseSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
*/

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 {
exercises: Exercise[];
selectedExerciseIds: string[];
onSelect: (exerciseId: string) => void;
onClose: () => void;
onCreateExercise?: (
exercise: Omit<Exercise, 'id' | 'createdAt'>
) => Promise<string>;
}

// Define muscle group categories for filtering
Expand All @@ -32,13 +37,15 @@ export default function ExerciseSelector({
selectedExerciseIds,
onSelect,
onClose,
onCreateExercise,
}: ExerciseSelectorProps) {
const [searchQuery, setSearchQuery] = useState('');
const [filterCategory, setFilterCategory] = useState<
Exercise['category'] | 'all'
>('all');
const [filterMuscleGroup, setFilterMuscleGroup] =
useState<MuscleGroupFilter>('all');
const [showCreateForm, setShowCreateForm] = useState(false);

const filteredExercises = exercises.filter((exercise) => {
const matchesSearch = exercise.name
Expand Down Expand Up @@ -129,6 +136,15 @@ export default function ExerciseSelector({
onClose();
};

const handleCreateExerciseSave = async (
exerciseData: Omit<Exercise, 'id' | 'createdAt'>
) => {
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;
Expand Down Expand Up @@ -217,6 +233,26 @@ export default function ExerciseSelector({
</div>

<div className="exercise-list">
{onCreateExercise && (
<button
type="button"
onClick={() => setShowCreateForm(true)}
className="exercise-item create-exercise-btn"
>
<div className="exercise-item-header">
<span className="exercise-item-icon">➕</span>
<span className="exercise-item-name">
Create New Exercise
</span>
</div>
<div className="exercise-item-muscles">
<span className="muscle-tag-small create-hint">
Add a custom exercise to your library
</span>
</div>
</button>
)}

{filteredExercises.length === 0 && (
<p className="no-results">
No exercises found. Try adjusting your search or filters.
Expand Down Expand Up @@ -246,6 +282,19 @@ export default function ExerciseSelector({
))}
</div>
</div>

{showCreateForm &&
onCreateExercise &&
createPortal(
<div className="exercise-form-portal">
<ExerciseForm
isOpen={showCreateForm}
onSave={handleCreateExerciseSave}
onCancel={() => setShowCreateForm(false)}
/>
</div>,
document.body
)}
</div>
</div>
);
Expand Down
Loading