Side Sheets instead of Modals

This commit is contained in:
AG
2025-12-12 00:25:15 +02:00
parent 87f639e320
commit e1e956d6b2
7 changed files with 145 additions and 29 deletions

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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 && (

View File

@@ -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>
);

View File

@@ -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 */}

View 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
);
};

View File

@@ -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: 'Конец',