From e1e956d6b26df3964279ffeb6dce682be9e78ebf Mon Sep 17 00:00:00 2001 From: AG Date: Fri, 12 Dec 2025 00:25:15 +0200 Subject: [PATCH] Side Sheets instead of Modals --- src/components/EditSetModal.tsx | 8 +-- src/components/ExerciseModal.tsx | 12 ++-- src/components/History.tsx | 7 +- src/components/Plans.tsx | 31 +++++---- src/components/Profile.tsx | 7 +- src/components/ui/SideSheet.tsx | 107 +++++++++++++++++++++++++++++++ src/services/i18n.ts | 2 + 7 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 src/components/ui/SideSheet.tsx diff --git a/src/components/EditSetModal.tsx b/src/components/EditSetModal.tsx index c814841..41e0162 100644 --- a/src/components/EditSetModal.tsx +++ b/src/components/EditSetModal.tsx @@ -3,7 +3,7 @@ import { Dumbbell, Activity, Percent, Timer, ArrowRight, ArrowUp, Save, Calendar import { WorkoutSet, ExerciseType, ExerciseDef, Language } from '../types'; import { t } from '../services/i18n'; import { formatSetMetrics } from '../utils/setFormatting'; -import { Modal } from './ui/Modal'; +import { SideSheet } from './ui/SideSheet'; import { Button } from './ui/Button'; interface EditSetModalProps { @@ -84,11 +84,11 @@ const EditSetModal: React.FC = ({ const hasHeight = ['HIGH_JUMP', 'High Jump'].includes(type) || set.height !== null; return ( -
@@ -210,7 +210,7 @@ const EditSetModal: React.FC = ({
-
+ ); }; diff --git a/src/components/ExerciseModal.tsx b/src/components/ExerciseModal.tsx index 89c4c12..d6c59a3 100644 --- a/src/components/ExerciseModal.tsx +++ b/src/components/ExerciseModal.tsx @@ -5,7 +5,7 @@ import { ExerciseDef, ExerciseType, Language } from '../types'; import { t } from '../services/i18n'; import { generateId } from '../utils/uuid'; import FilledInput from './FilledInput'; -import { Modal } from './ui/Modal'; +import { SideSheet } from './ui/SideSheet'; import { Button } from './ui/Button'; interface ExerciseModalProps { @@ -64,19 +64,19 @@ const ExerciseModal: React.FC = ({ isOpen, onClose, onSave, }; return ( - { console.log('ExerciseModal onClose'); onClose(); }} title={t('create_exercise', lang)} - maxWidth="sm" + width="md" > {console.log('ExerciseModal Rendering. isOpen:', isOpen)}
- { @@ -120,7 +120,7 @@ const ExerciseModal: React.FC = ({ isOpen, onClose, onSave,
{newType === ExerciseType.BODYWEIGHT && ( - setNewBwPercentage(e.target.value)} @@ -150,7 +150,7 @@ const ExerciseModal: React.FC = ({ isOpen, onClose, onSave,
-
+ ); }; diff --git a/src/components/History.tsx b/src/components/History.tsx index c06f65c..bfcd6a5 100644 --- a/src/components/History.tsx +++ b/src/components/History.tsx @@ -9,6 +9,7 @@ import { getExercises } from '../services/storage'; import { Button } from './ui/Button'; import { Card } from './ui/Card'; import { Modal } from './ui/Modal'; +import { SideSheet } from './ui/SideSheet'; import EditSetModal from './EditSetModal'; import FilledInput from './FilledInput'; @@ -366,11 +367,11 @@ const History: React.FC = ({ lang }) => { {/* EDIT SESSION MODAL */} {editingSession && ( - setEditingSession(null)} title={t('edit', lang)} - maxWidth="lg" + width="lg" >
{/* Meta Info */} @@ -449,7 +450,7 @@ const History: React.FC = ({ lang }) => {
-
+ ) } {editingSetInfo && ( diff --git a/src/components/Plans.tsx b/src/components/Plans.tsx index 1059f6f..43d50b4 100644 --- a/src/components/Plans.tsx +++ b/src/components/Plans.tsx @@ -13,6 +13,7 @@ import { toTitleCase } from '../utils/text'; import { Button } from './ui/Button'; import { Card } from './ui/Card'; import { Modal } from './ui/Modal'; +import { SideSheet } from './ui/SideSheet'; interface PlansProps { lang: Language; @@ -297,11 +298,11 @@ const Plans: React.FC = ({ lang }) => { - setShowExerciseSelector(false)} title={t('select_exercise', lang)} - maxWidth="md" + width="lg" >
@@ -325,13 +326,13 @@ const Plans: React.FC = ({ lang }) => { ))}
-
+ - setIsCreatingExercise(false)} title={t('create_exercise', lang)} - maxWidth="md" + width="md" >
= ({ lang }) => {
-
+ ); } @@ -468,19 +469,23 @@ const Plans: React.FC = ({ lang }) => { {/* Preparation Modal */} {showPlanPrep && ( -
-
-

{showPlanPrep.name}

-
+ setShowPlanPrep(null)} + title={showPlanPrep.name} + width="md" + > +
+
{t('prep_title', lang)}
{showPlanPrep.description || t('prep_no_instructions', lang)}
- - + +
-
+ )}
); diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index 9650c7f..2678834 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -12,6 +12,7 @@ import Snackbar from './Snackbar'; import { Button } from './ui/Button'; import { Card } from './ui/Card'; import { Modal } from './ui/Modal'; +import { SideSheet } from './ui/SideSheet'; interface ProfileProps { user: User; @@ -601,11 +602,11 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang {/* Edit Exercise Modal */} {editingExercise && ( - setEditingExercise(null)} title={t('edit', lang)} - maxWidth="sm" + width="md" >
@@ -632,7 +633,7 @@ const Profile: React.FC = ({ user, onLogout, lang, onLanguageChang
-
+ )} {/* Create Exercise Modal */} diff --git a/src/components/ui/SideSheet.tsx b/src/components/ui/SideSheet.tsx new file mode 100644 index 0000000..1981031 --- /dev/null +++ b/src/components/ui/SideSheet.tsx @@ -0,0 +1,107 @@ + +import React, { useEffect, useState } from 'react'; +import { X } from 'lucide-react'; +import { createPortal } from 'react-dom'; + +interface SideSheetProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: React.ReactNode; + width?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; +} + +export const SideSheet: React.FC = ({ isOpen, onClose, title, children, width = 'md' }) => { + const [isMounted, setIsMounted] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + useEffect(() => { + if (isOpen) { + setIsVisible(true); + document.body.style.overflow = 'hidden'; + } else { + const timer = setTimeout(() => setIsVisible(false), 300); // Allow exit animation + document.body.style.overflow = 'unset'; + return () => clearTimeout(timer); + } + return () => { + document.body.style.overflow = 'unset'; + }; + }, [isOpen]); + + if (!isMounted || (!isOpen && !isVisible)) return null; + + // Width classes for Desktop side sheet + const widthClasses = { + sm: 'sm:max-w-sm', + md: 'sm:max-w-md', + lg: 'sm:max-w-lg', + xl: 'sm:max-w-xl', + full: 'sm:max-w-full' + }; + + return createPortal( +
+ {/* Backdrop */} +
+ + {/* Sheet */} +
e.stopPropagation()} + > + {/* Mobile Drag Handle */} +
+
+
+ + {/* Header */} +
+

{title}

+ +
+ + {/* Content */} +
+ {children} +
+
+
, + document.body + ); +}; diff --git a/src/services/i18n.ts b/src/services/i18n.ts index 3efdb4c..749b45a 100644 --- a/src/services/i18n.ts +++ b/src/services/i18n.ts @@ -97,6 +97,7 @@ const translations = { delete_set_confirm: 'Are you sure you want to delete this set?', delete: 'Delete', edit: 'Edit', + edit_set: 'Edit Set', save: 'Save', start_time: 'Start', end_time: 'End', @@ -266,6 +267,7 @@ const translations = { delete_set_confirm: 'Вы уверены, что хотите удалить этот подход?', delete: 'Удалить', edit: 'Редактировать', + edit_set: 'Редактировать подход', save: 'Сохранить', start_time: 'Начало', end_time: 'Конец',