Side Sheets instead of Modals
This commit is contained in:
@@ -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<EditSetModalProps> = ({
|
||||
const hasHeight = ['HIGH_JUMP', 'High Jump'].includes(type) || set.height !== null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<SideSheet
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('edit_set', lang) || 'Edit Set'}
|
||||
maxWidth="sm"
|
||||
width="md"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -210,7 +210,7 @@ const EditSetModal: React.FC<EditSetModalProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</SideSheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<SideSheet
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
console.log('ExerciseModal onClose');
|
||||
onClose();
|
||||
}}
|
||||
title={t('create_exercise', lang)}
|
||||
maxWidth="sm"
|
||||
width="md"
|
||||
>
|
||||
{console.log('ExerciseModal Rendering. isOpen:', isOpen)}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<FilledInput
|
||||
<GymFilledInput
|
||||
label={t('ex_name', lang)}
|
||||
value={newName}
|
||||
onChange={(e: any) => {
|
||||
@@ -120,7 +120,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
||||
</div>
|
||||
|
||||
{newType === ExerciseType.BODYWEIGHT && (
|
||||
<FilledInput
|
||||
<GymFilledInput
|
||||
label={t('body_weight_percent', lang)}
|
||||
value={newBwPercentage}
|
||||
onChange={(e: any) => setNewBwPercentage(e.target.value)}
|
||||
@@ -150,7 +150,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</SideSheet>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<HistoryProps> = ({ lang }) => {
|
||||
|
||||
{/* EDIT SESSION MODAL */}
|
||||
{editingSession && (
|
||||
<Modal
|
||||
<SideSheet
|
||||
isOpen={!!editingSession}
|
||||
onClose={() => setEditingSession(null)}
|
||||
title={t('edit', lang)}
|
||||
maxWidth="lg"
|
||||
width="lg"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Meta Info */}
|
||||
@@ -449,7 +450,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</SideSheet>
|
||||
)
|
||||
}
|
||||
{editingSetInfo && (
|
||||
|
||||
@@ -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<PlansProps> = ({ lang }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
<SideSheet
|
||||
isOpen={showExerciseSelector}
|
||||
onClose={() => setShowExerciseSelector(false)}
|
||||
title={t('select_exercise', lang)}
|
||||
maxWidth="md"
|
||||
width="lg"
|
||||
>
|
||||
<div className="flex flex-col h-[60vh]">
|
||||
<div className="flex justify-end mb-2">
|
||||
@@ -325,13 +326,13 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</SideSheet>
|
||||
|
||||
<Modal
|
||||
<SideSheet
|
||||
isOpen={isCreatingExercise}
|
||||
onClose={() => setIsCreatingExercise(false)}
|
||||
title={t('create_exercise', lang)}
|
||||
maxWidth="md"
|
||||
width="md"
|
||||
>
|
||||
<div className="space-y-6 pt-2">
|
||||
<FilledInput
|
||||
@@ -390,7 +391,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</SideSheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -468,19 +469,23 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
|
||||
{/* Preparation Modal */}
|
||||
{showPlanPrep && (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-6 backdrop-blur-sm">
|
||||
<div className="w-full max-w-sm bg-surface-container rounded-[28px] p-6 shadow-elevation-3">
|
||||
<h3 className="text-2xl font-normal text-on-surface mb-4">{showPlanPrep.name}</h3>
|
||||
<div className="bg-surface-container-high p-4 rounded-xl text-on-surface-variant text-sm mb-8">
|
||||
<SideSheet
|
||||
isOpen={!!showPlanPrep}
|
||||
onClose={() => setShowPlanPrep(null)}
|
||||
title={showPlanPrep.name}
|
||||
width="md"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<div className="bg-surface-container-high p-4 rounded-xl text-on-surface-variant text-sm">
|
||||
<div className="text-xs font-bold text-primary mb-2">{t('prep_title', lang)}</div>
|
||||
{showPlanPrep.description || t('prep_no_instructions', lang)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={() => setShowPlanPrep(null)} className="px-6 py-2.5 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
|
||||
<button onClick={confirmPlanStart} className="px-6 py-2.5 rounded-full bg-primary text-on-primary font-medium">{t('start', lang)}</button>
|
||||
</div>
|
||||
<Button onClick={() => setShowPlanPrep(null)} variant="ghost">{t('cancel', lang)}</Button>
|
||||
<Button onClick={confirmPlanStart}>{t('start', lang)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SideSheet>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
|
||||
{/* Edit Exercise Modal */}
|
||||
{editingExercise && (
|
||||
<Modal
|
||||
<SideSheet
|
||||
isOpen={!!editingExercise}
|
||||
onClose={() => setEditingExercise(null)}
|
||||
title={t('edit', lang)}
|
||||
maxWidth="sm"
|
||||
width="md"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
||||
@@ -632,7 +633,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
<Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</SideSheet>
|
||||
)}
|
||||
|
||||
{/* Create Exercise Modal */}
|
||||
|
||||
107
src/components/ui/SideSheet.tsx
Normal file
107
src/components/ui/SideSheet.tsx
Normal file
@@ -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<SideSheetProps> = ({ 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(
|
||||
<div
|
||||
className={`fixed inset-0 z-50 flex flex-col sm:flex-row justify-end items-end sm:items-stretch transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Sheet */}
|
||||
<div
|
||||
className={`
|
||||
relative
|
||||
bg-surface-container
|
||||
w-full
|
||||
${widthClasses[width]}
|
||||
|
||||
/* Mobile Styles (Bottom Sheet) */
|
||||
rounded-t-[28px] sm:rounded-none sm:rounded-l-[28px]
|
||||
max-h-[85vh] sm:max-h-full h-full sm:h-full
|
||||
flex flex-col
|
||||
shadow-elevation-3
|
||||
|
||||
/* Animations */
|
||||
transition-transform duration-300 ease-out
|
||||
${isOpen
|
||||
? 'translate-y-0 sm:translate-y-0 sm:translate-x-0'
|
||||
: 'translate-y-full sm:translate-y-0 sm:translate-x-full'
|
||||
}
|
||||
`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Mobile Drag Handle */}
|
||||
<div className="sm:hidden flex justify-center pt-4 pb-2 shrink-0">
|
||||
<div className="w-8 h-1 bg-outline-variant rounded-full opacity-40" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 pb-6 pt-2 sm:pt-6 shrink-0 border-b border-transparent">
|
||||
<h3 className="text-[22px] leading-[28px] font-normal text-on-surface">{title}</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 -mr-2 text-on-surface-variant hover:text-on-surface hover:bg-surface-container-high rounded-full transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-6 pt-0">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
@@ -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: 'Конец',
|
||||
|
||||
Reference in New Issue
Block a user