UI refactoring: Profile, History, and Plans Components
This commit is contained in:
Binary file not shown.
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { toTitleCase } from '../utils/text';
|
||||
import { X, Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react';
|
||||
import { Dumbbell, User, Flame, Timer as TimerIcon, ArrowUp, ArrowRight, Footprints, Ruler, Percent } from 'lucide-react';
|
||||
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 { Button } from './ui/Button';
|
||||
|
||||
interface ExerciseModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -58,19 +60,16 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
||||
setNewBwPercentage('100');
|
||||
setIsUnilateral(false);
|
||||
setError('');
|
||||
onClose();
|
||||
onClose(); // Modal controls its own open state usually, but here checking prop
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 z-[60] flex items-end sm:items-center justify-center p-4 backdrop-blur-sm">
|
||||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3 animate-in slide-in-from-bottom-10 duration-200">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="text-2xl font-normal text-on-surface">{t('create_exercise', lang)}</h3>
|
||||
<button onClick={onClose} className="p-2 bg-surface-container-high rounded-full hover:bg-outline-variant/20"><X size={20} /></button>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('create_exercise', lang)}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<FilledInput
|
||||
@@ -138,17 +137,16 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<button
|
||||
<div className="flex justify-end mt-4 pt-4">
|
||||
<Button
|
||||
onClick={handleCreateExercise}
|
||||
className="px-8 py-3 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1"
|
||||
fullWidth
|
||||
>
|
||||
{t('create_btn', lang)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react';
|
||||
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
|
||||
import { t } from '../services/i18n';
|
||||
import { useSession } from '../context/SessionContext';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Modal } from './ui/Modal';
|
||||
import FilledInput from './FilledInput';
|
||||
|
||||
interface HistoryProps {
|
||||
lang: Language;
|
||||
@@ -116,8 +119,6 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-on-surface-variant p-8 text-center">
|
||||
@@ -129,19 +130,20 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-surface">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 z-10">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 z-10 shrink-0">
|
||||
<h2 className="text-2xl font-normal text-on-surface">{t('tab_history', lang)}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-20">
|
||||
<div className="flex-1 overflow-y-auto p-4 pb-20">
|
||||
<div className="max-w-2xl mx-auto space-y-4">
|
||||
{/* Regular Workout Sessions */}
|
||||
{sessions.filter(s => s.type === 'STANDARD').map((session) => {
|
||||
const totalWork = calculateSessionWork(session);
|
||||
|
||||
return (
|
||||
<div
|
||||
<Card
|
||||
key={session.id}
|
||||
className="bg-surface-container rounded-xl p-5 shadow-elevation-1 border border-outline-variant/20 cursor-pointer hover:bg-surface-container-high transition-colors"
|
||||
className="cursor-pointer hover:bg-surface-container-high transition-colors"
|
||||
onClick={() => setEditingSession(JSON.parse(JSON.stringify(session)))}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
@@ -181,27 +183,31 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingSession(JSON.parse(JSON.stringify(session)));
|
||||
}}
|
||||
className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-on-surface-variant hover:text-primary"
|
||||
>
|
||||
<Pencil size={20} />
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeletingId(session.id);
|
||||
}}
|
||||
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-on-surface-variant hover:text-error"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -209,25 +215,27 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
{sessions.filter(s => s.type === 'QUICK_LOG').length > 0 && (
|
||||
<div className="mt-8">
|
||||
<h3 className="text-xl font-medium text-on-surface mb-4 px-2">{t('quick_log', lang)}</h3>
|
||||
{Object.entries(
|
||||
{(Object.entries(
|
||||
sessions
|
||||
.filter(s => s.type === 'QUICK_LOG')
|
||||
.reduce((groups: Record<string, WorkoutSession[]>, session) => {
|
||||
.reduce<Record<string, WorkoutSession[]>>((groups, session) => {
|
||||
const date = new Date(session.startTime).toISOString().split('T')[0];
|
||||
if (!groups[date]) groups[date] = [];
|
||||
groups[date].push(session);
|
||||
return groups;
|
||||
}, {})
|
||||
)
|
||||
) as [string, WorkoutSession[]][])
|
||||
.sort(([a], [b]) => b.localeCompare(a))
|
||||
.map(([date, daySessions]) => (
|
||||
<div key={date} className="mb-4">
|
||||
<div className="text-sm text-on-surface-variant px-2 mb-2 font-medium">{date}</div>
|
||||
<div className="space-y-2">
|
||||
{daySessions.flatMap(session => session.sets).map((set, idx) => (
|
||||
<div
|
||||
{daySessions
|
||||
.reduce<WorkoutSet[]>((acc, session) => acc.concat(session.sets), [])
|
||||
.map((set, idx) => (
|
||||
<Card
|
||||
key={set.id}
|
||||
className="bg-surface-container-low rounded-xl p-4 border border-outline-variant/10 flex justify-between items-center"
|
||||
className="bg-surface-container-low flex justify-between items-center"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-on-surface">
|
||||
@@ -248,7 +256,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Find the session this set belongs to and open edit mode
|
||||
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
|
||||
@@ -256,11 +264,13 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
setEditingSession(JSON.parse(JSON.stringify(parentSession)));
|
||||
}
|
||||
}}
|
||||
className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</button>
|
||||
<button
|
||||
<Pencil size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Find the session and set up for deletion
|
||||
const parentSession = daySessions.find(s => s.sets.some(st => st.id === set.id));
|
||||
@@ -268,69 +278,70 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
setDeletingSetInfo({ sessionId: parentSession.id, setId: set.id });
|
||||
}
|
||||
}}
|
||||
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-error hover:text-error"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DELETE CONFIRMATION DIALOG (MD3) */}
|
||||
{(deletingId || deletingSetInfo) && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-surface-container w-full max-w-xs rounded-[28px] p-6 shadow-elevation-3">
|
||||
<h3 className="text-xl font-normal text-on-surface mb-2">
|
||||
{deletingId ? t('delete_workout', lang) : t('delete_set', lang) || 'Delete Set'}
|
||||
</h3>
|
||||
<p className="text-sm text-on-surface-variant mb-8">
|
||||
{/* DELETE CONFIRMATION MODAL */}
|
||||
<Modal
|
||||
isOpen={!!(deletingId || deletingSetInfo)}
|
||||
onClose={() => {
|
||||
setDeletingId(null);
|
||||
setDeletingSetInfo(null);
|
||||
}}
|
||||
title={deletingId ? t('delete_workout', lang) : t('delete_set', lang) || 'Delete Set'}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-on-surface-variant">
|
||||
{deletingId ? t('delete_confirm', lang) : t('delete_set_confirm', lang) || 'Are you sure you want to delete this set?'}
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDeletingId(null);
|
||||
setDeletingSetInfo(null);
|
||||
}}
|
||||
className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
{t('cancel', lang)}
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirmDelete}
|
||||
className="px-4 py-2 rounded-full bg-error-container text-on-error-container font-medium"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
>
|
||||
{t('delete', lang)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* EDIT SESSION FULLSCREEN DIALOG */}
|
||||
{/* EDIT SESSION MODAL */}
|
||||
{editingSession && (
|
||||
<div className="fixed inset-0 z-[60] bg-surface flex flex-col animate-in slide-in-from-bottom-10 duration-200">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shadow-elevation-1">
|
||||
<button onClick={() => setEditingSession(null)} className="text-on-surface-variant hover:text-on-surface">
|
||||
<X />
|
||||
</button>
|
||||
<h2 className="text-lg font-medium text-on-surface">{t('edit', lang)}</h2>
|
||||
<button onClick={handleSaveEdit} className="text-primary font-medium flex items-center gap-2">
|
||||
{t('save', lang)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<Modal
|
||||
isOpen={!!editingSession}
|
||||
onClose={() => setEditingSession(null)}
|
||||
title={t('edit', lang)}
|
||||
maxWidth="lg"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Meta Info */}
|
||||
<div className="bg-surface-container p-4 rounded-xl border border-outline-variant/20 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-surface-container-high rounded-t-lg px-3 py-2 border-b border-outline-variant">
|
||||
<label className="text-[10px] text-on-surface-variant font-bold block">{t('start_time', lang)}</label>
|
||||
<input
|
||||
@@ -359,27 +370,28 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
className="w-full bg-transparent text-on-surface focus:outline-none text-lg mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-primary ml-1">{t('sets_count', lang)} ({editingSession.sets.length})</h3>
|
||||
{editingSession.sets.map((set, idx) => (
|
||||
<div key={set.id} className="bg-surface-container p-3 rounded-xl border border-outline-variant/20 flex flex-col gap-3 shadow-sm">
|
||||
<div className="flex justify-between items-center border-b border-outline-variant pb-2">
|
||||
<div key={set.id} className="bg-surface-container-low p-3 rounded-xl border border-outline-variant/20 flex flex-col gap-3">
|
||||
<div className="flex justify-between items-center border-b border-outline-variant/50 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container text-xs font-bold flex items-center justify-center">{idx + 1}</span>
|
||||
<span className="font-medium text-on-surface text-sm">{set.exerciseName}{set.side && <span className="ml-2 text-xs font-medium text-on-surface-variant">{t(set.side.toLowerCase(), lang)}</span>}</span>
|
||||
<span className="font-medium text-on-surface text-sm">{set.exerciseName}{set.side && <span className="ml-2 text-xs font-medium text-on-surface-variant">{t(set.side.toLowerCase() as any, lang)}</span>}</span>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleDeleteSet(set.id)}
|
||||
className="text-on-surface-variant hover:text-error p-1 rounded hover:bg-error-container/10 transition-colors"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-on-surface-variant hover:text-error hover:bg-error-container/10"
|
||||
title={t('delete', lang)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{(set.type === ExerciseType.STRENGTH || set.type === ExerciseType.BODYWEIGHT || set.type === ExerciseType.STATIC) && (
|
||||
<div className="bg-surface-container-high rounded px-2 py-1">
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Dumbbell size={10} /> {t('weight_kg', lang)}</label>
|
||||
@@ -450,11 +462,16 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4 border-t border-outline-variant">
|
||||
<Button onClick={handleSaveEdit}>
|
||||
<Save size={16} className="mr-2" />
|
||||
{t('save', lang)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Scale, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical } from 'lucide-react';
|
||||
import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical, Scale } from 'lucide-react';
|
||||
import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types';
|
||||
import { getExercises, saveExercise } from '../services/storage';
|
||||
import { t } from '../services/i18n';
|
||||
@@ -11,6 +10,8 @@ import { useActiveWorkout } from '../context/ActiveWorkoutContext';
|
||||
|
||||
import FilledInput from './FilledInput';
|
||||
import { toTitleCase } from '../utils/text';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
|
||||
interface PlansProps {
|
||||
lang: Language;
|
||||
@@ -162,26 +163,25 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-surface">
|
||||
<div className="px-4 py-3 bg-surface-container border-b border-outline-variant flex justify-between items-center">
|
||||
<button onClick={() => setIsEditing(false)} className="p-2 text-on-surface-variant hover:bg-white/5 rounded-full"><X /></button>
|
||||
<div className="px-4 py-3 bg-surface-container border-b border-outline-variant flex justify-between items-center shrink-0">
|
||||
<Button onClick={() => setIsEditing(false)} variant="ghost" size="icon">
|
||||
<X size={20} />
|
||||
</Button>
|
||||
<h2 className="text-title-medium font-medium text-on-surface">{t('plan_editor', lang)}</h2>
|
||||
<button onClick={handleSave} className="p-2 text-primary font-medium">
|
||||
<Button onClick={handleSave} variant="ghost" className="text-primary font-medium hover:bg-primary-container/10">
|
||||
{t('save', lang)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label>
|
||||
<input
|
||||
className="w-full bg-transparent text-xl text-on-surface focus:outline-none pt-1 pb-2"
|
||||
placeholder={t('plan_name_ph', lang)}
|
||||
<FilledInput
|
||||
label={t('ex_name', lang)}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
autoCapitalize="words"
|
||||
onChange={(e: any) => setName(e.target.value)}
|
||||
type="text"
|
||||
autocapitalize="words"
|
||||
onBlur={() => setName(toTitleCase(name))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-4 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('prep_title', lang)}</label>
|
||||
@@ -200,9 +200,9 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
|
||||
<div className="space-y-2">
|
||||
{steps.map((step, idx) => (
|
||||
<div
|
||||
<Card
|
||||
key={step.id}
|
||||
className={`bg-surface-container rounded-xl p-3 flex items-center gap-3 shadow-elevation-1 cursor-move transition-all hover:bg-surface-container-high ${draggingIndex === idx ? 'opacity-50 ring-2 ring-primary bg-surface-container-high' : ''}`}
|
||||
className={`flex items-center gap-3 transition-all hover:bg-surface-container-high ${draggingIndex === idx ? 'opacity-50 ring-2 ring-primary bg-surface-container-high' : ''}`}
|
||||
draggable
|
||||
onDragStart={() => onDragStart(idx)}
|
||||
onDragEnter={() => onDragEnter(idx)}
|
||||
@@ -221,7 +221,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
<div className="text-base font-medium text-on-surface">{step.exerciseName}</div>
|
||||
<label className="flex items-center gap-2 mt-1 cursor-pointer w-fit">
|
||||
<div className={`w-4 h-4 border rounded flex items-center justify-center ${step.isWeighted ? 'bg-primary border-primary' : 'border-outline'}`}>
|
||||
{step.isWeighted && <Scale size={10} className="text-on-primary" />}
|
||||
{step.isWeighted && <Dumbbell size={10} className="text-on-primary" />}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -232,32 +232,36 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
<span className="text-xs text-on-surface-variant">{t('weighted', lang)}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button onClick={() => removeStep(step.id)} className="text-on-surface-variant hover:text-error p-2">
|
||||
<Button onClick={() => removeStep(step.id)} variant="ghost" size="icon" className="text-on-surface-variant hover:text-error hover:bg-error/10">
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setShowExerciseSelector(true)}
|
||||
className="w-full py-4 rounded-full border border-outline text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary-container/10 transition-all"
|
||||
variant="outline"
|
||||
fullWidth
|
||||
className="py-6 rounded-full border border-outline text-primary font-medium flex items-center justify-center gap-2 hover:bg-primary-container/10 transition-all h-auto"
|
||||
>
|
||||
<Plus size={20} />
|
||||
{t('add_exercise', lang)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showExerciseSelector && (
|
||||
<div className="fixed inset-0 bg-surface z-50 flex flex-col animate-in slide-in-from-bottom-full duration-200">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shrink-0">
|
||||
<span className="font-medium text-on-surface">{t('select_exercise', lang)}</span>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setIsCreatingExercise(true)} className="p-2 text-primary hover:bg-primary-container/20 rounded-full">
|
||||
<Button onClick={() => setIsCreatingExercise(true)} variant="ghost" size="icon" className="text-primary hover:bg-primary-container/20">
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
<button onClick={() => setShowExerciseSelector(false)}><X /></button>
|
||||
</Button>
|
||||
<Button onClick={() => setShowExerciseSelector(false)} variant="ghost" size="icon">
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
@@ -278,9 +282,11 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
|
||||
{isCreatingExercise && (
|
||||
<div className="fixed inset-0 bg-surface z-[60] flex flex-col animate-in slide-in-from-bottom-full duration-200">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container">
|
||||
<div className="px-4 py-3 border-b border-outline-variant flex justify-between items-center bg-surface-container shrink-0">
|
||||
<h3 className="text-title-medium font-medium text-on-surface">{t('create_exercise', lang)}</h3>
|
||||
<button onClick={() => setIsCreatingExercise(false)} className="p-2 text-on-surface-variant hover:bg-white/5 rounded-full"><X /></button>
|
||||
<Button onClick={() => setIsCreatingExercise(false)} variant="ghost" size="icon" className="text-on-surface-variant hover:bg-white/5">
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-6 overflow-y-auto flex-1">
|
||||
@@ -330,13 +336,14 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
)}
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<button
|
||||
<Button
|
||||
onClick={handleCreateExercise}
|
||||
className="w-full h-14 bg-primary text-on-primary rounded-full font-medium shadow-elevation-1 flex items-center justify-center gap-2"
|
||||
fullWidth
|
||||
size="lg"
|
||||
>
|
||||
<CheckCircle size={20} />
|
||||
<CheckCircle size={20} className="mr-2" />
|
||||
{t('create_btn', lang)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,7 +356,7 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-surface relative">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 z-10">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 z-10 shrink-0">
|
||||
<h2 className="text-2xl font-normal text-on-surface">{t('my_plans', lang)}</h2>
|
||||
</div>
|
||||
|
||||
@@ -363,23 +370,27 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
</div>
|
||||
) : (
|
||||
plans.map(plan => (
|
||||
<div key={plan.id} className="bg-surface-container rounded-xl p-4 shadow-elevation-1 border border-outline-variant/20 relative overflow-hidden">
|
||||
<Card key={plan.id} className="relative overflow-hidden">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="text-xl font-normal text-on-surface">{plan.name}</h3>
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => handleDelete(plan.id, e)}
|
||||
className="text-on-surface-variant hover:text-error p-2 rounded-full hover:bg-white/5"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-on-surface-variant hover:text-error hover:bg-white/5"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="absolute top-4 right-14">
|
||||
<button
|
||||
<Button
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(plan); }}
|
||||
className="text-on-surface-variant hover:text-primary p-2 rounded-full hover:bg-white/5"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-on-surface-variant hover:text-primary hover:bg-white/5"
|
||||
>
|
||||
<Edit2 size={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-on-surface-variant text-sm line-clamp-2 mb-4 min-h-[1.25rem]">
|
||||
{plan.description || t('prep_no_instructions', lang)}
|
||||
@@ -388,15 +399,15 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
|
||||
<div className="text-xs font-medium text-primary bg-primary-container/20 px-3 py-1 rounded-full">
|
||||
{plan.steps.length} {t('exercises_count', lang)}
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => startSession(plan)}
|
||||
className="flex items-center gap-2 bg-primary text-on-primary px-5 py-2 rounded-full text-sm font-medium hover:shadow-elevation-2 transition-all"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<PlayCircle size={18} />
|
||||
{t('start', lang)}
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,9 @@ import ExerciseModal from './ExerciseModal';
|
||||
import FilledInput from './FilledInput';
|
||||
import { t } from '../services/i18n';
|
||||
import Snackbar from './Snackbar';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Modal } from './ui/Modal';
|
||||
|
||||
interface ProfileProps {
|
||||
user: User;
|
||||
@@ -238,20 +241,21 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-surface">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center justify-between z-10">
|
||||
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center justify-between z-10 shrink-0">
|
||||
<h2 className="text-xl font-normal text-on-surface flex items-center gap-2">
|
||||
<UserIcon size={20} />
|
||||
{t('profile_title', lang)}
|
||||
</h2>
|
||||
<button onClick={onLogout} className="text-error flex items-center gap-1 text-sm font-medium hover:bg-error-container/10 px-3 py-1 rounded-full">
|
||||
<LogOut size={16} /> {t('logout', lang)}
|
||||
</button>
|
||||
<Button onClick={onLogout} variant="ghost" size="sm" className="text-error hover:bg-error-container/10">
|
||||
<LogOut size={16} className="mr-1" /> {t('logout', lang)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-6 pb-24">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
|
||||
{/* User Info Card */}
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||
<Card>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-14 h-14 rounded-full bg-primary-container text-on-primary-container flex items-center justify-center text-xl font-bold">
|
||||
{user.email[0].toUpperCase()}
|
||||
@@ -303,13 +307,13 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button onClick={handleSaveProfile} className="w-full py-2 rounded-full border border-outline text-primary text-sm font-medium hover:bg-primary-container/10 flex justify-center gap-2 items-center">
|
||||
<Save size={16} /> {t('save_profile', lang)}
|
||||
</button>
|
||||
</div>
|
||||
<Button onClick={handleSaveProfile} variant="outline" fullWidth>
|
||||
<Save size={16} className="mr-2" /> {t('save_profile', lang)}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* WEIGHT TRACKER */}
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||
<Card>
|
||||
<button
|
||||
onClick={() => setShowWeightTracker(!showWeightTracker)}
|
||||
className="w-full flex justify-between items-center text-sm font-bold text-primary"
|
||||
@@ -332,12 +336,12 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
placeholder="Enter weight..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={handleLogWeight}
|
||||
className="bg-primary text-on-primary px-4 py-3 rounded-lg font-medium text-sm mb-[1px]"
|
||||
className="mb-[1px]"
|
||||
>
|
||||
Log
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
@@ -355,10 +359,10 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* EXERCISE MANAGER */}
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||
<Card>
|
||||
<button
|
||||
onClick={() => setShowExercises(!showExercises)}
|
||||
className="w-full flex justify-between items-center text-sm font-bold text-primary"
|
||||
@@ -380,7 +384,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
label={t('filter_by_name', lang) || 'Filter by name'}
|
||||
value={exerciseNameFilter}
|
||||
onChange={(e: any) => setExerciseNameFilter(e.target.value)}
|
||||
icon={<i className="hidden" />} // No icon needed or maybe use Search icon? Profile doesn't import Search. I'll omit icon if optional.
|
||||
icon={<i className="hidden" />}
|
||||
type="text"
|
||||
autoFocus={false}
|
||||
/>
|
||||
@@ -426,10 +430,10 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Change Password */}
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-outline-variant/20">
|
||||
<Card>
|
||||
<h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><Lock size={14} /> {t('change_pass_btn', lang)}</h3>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
@@ -439,14 +443,14 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="flex-1 bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
|
||||
/>
|
||||
<button onClick={handleChangePassword} className="bg-secondary-container text-on-secondary-container px-4 rounded-lg font-medium text-sm">OK</button>
|
||||
<Button onClick={handleChangePassword} size="sm" variant="secondary">OK</Button>
|
||||
</div>
|
||||
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* User Self Deletion (Not for Admin) */}
|
||||
{user.role !== 'ADMIN' && (
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-error/30">
|
||||
<Card className="border-error/30">
|
||||
<h3 className="text-sm font-bold text-error mb-2 flex items-center gap-2"><Trash2 size={14} /> {t('delete_account', lang)}</h3>
|
||||
{!showDeleteConfirm ? (
|
||||
<button onClick={() => setShowDeleteConfirm(true)} className="text-error text-sm hover:underline">
|
||||
@@ -456,17 +460,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-error">{t('delete_account_confirm', lang)}</p>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setShowDeleteConfirm(false)} className="text-xs px-3 py-1 bg-surface-container-high rounded-full">{t('cancel', lang)}</button>
|
||||
<button onClick={handleDeleteMyAccount} className="text-xs px-3 py-1 bg-error text-on-error rounded-full">{t('delete', lang)}</button>
|
||||
<Button onClick={() => setShowDeleteConfirm(false)} size="sm" variant="ghost">{t('cancel', lang)}</Button>
|
||||
<Button onClick={handleDeleteMyAccount} size="sm" variant="destructive">{t('delete', lang)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* ADMIN AREA */}
|
||||
{user.role === 'ADMIN' && (
|
||||
<div className="bg-surface-container rounded-xl p-4 border border-primary/30 relative overflow-hidden">
|
||||
<Card className="border-primary/30 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-2 bg-primary/10 rounded-bl-xl">
|
||||
<Shield size={16} className="text-primary" />
|
||||
</div>
|
||||
@@ -487,9 +491,9 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
onChange={(e) => setNewUserPass(e.target.value)}
|
||||
type="text"
|
||||
/>
|
||||
<button onClick={handleCreateUser} className="w-full py-2 bg-primary text-on-primary rounded-full text-sm font-medium">
|
||||
<Button onClick={handleCreateUser} fullWidth>
|
||||
{t('create_btn', lang)}
|
||||
</button>
|
||||
</Button>
|
||||
{createMsg && <p className="text-xs text-error text-center font-medium">{createMsg}</p>}
|
||||
</div>
|
||||
|
||||
@@ -549,12 +553,13 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
onChange={(e) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => handleAdminResetPass(u.id)}
|
||||
className="text-xs bg-secondary-container text-on-secondary-container px-3 py-2 rounded font-medium"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('reset_pass', lang)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -562,14 +567,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Edit Exercise Modal */}
|
||||
{editingExercise && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
|
||||
<div className="bg-surface-container w-full max-w-sm rounded-[28px] p-6 shadow-elevation-3">
|
||||
<h3 className="text-xl font-normal text-on-surface mb-4">{t('edit', lang)}</h3>
|
||||
<Modal
|
||||
isOpen={!!editingExercise}
|
||||
onClose={() => setEditingExercise(null)}
|
||||
title={t('edit', lang)}
|
||||
maxWidth="sm"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-surface-container-high rounded-t-lg border-b border-outline-variant px-3 py-2">
|
||||
<label className="text-[10px] text-on-surface-variant font-medium">{t('ex_name', lang)}</label>
|
||||
@@ -591,12 +599,11 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setEditingExercise(null)} className="px-4 py-2 rounded-full text-primary font-medium hover:bg-white/5">{t('cancel', lang)}</button>
|
||||
<button onClick={handleSaveExerciseEdit} className="px-4 py-2 rounded-full bg-primary text-on-primary font-medium">{t('save', lang)}</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => setEditingExercise(null)} variant="ghost">{t('cancel', lang)}</Button>
|
||||
<Button onClick={handleSaveExerciseEdit}>{t('save', lang)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Create Exercise Modal */}
|
||||
@@ -618,6 +625,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
onClose={() => setSnackbar(prev => ({ ...prev, isOpen: false }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
49
src/components/ui/Button.tsx
Normal file
49
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive';
|
||||
size?: 'sm' | 'md' | 'lg' | 'icon';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className = '', variant = 'primary', size = 'md', fullWidth = false, loading = false, children, disabled, ...props }, ref) => {
|
||||
|
||||
const baseStyles = "inline-flex items-center justify-center rounded-full font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
|
||||
|
||||
const variants = {
|
||||
primary: "bg-primary text-on-primary hover:bg-primary/90",
|
||||
secondary: "bg-secondary-container text-on-secondary-container hover:bg-secondary-container/80",
|
||||
outline: "border border-outline text-primary hover:bg-primary-container/10",
|
||||
ghost: "text-on-surface hover:bg-surface-container-high",
|
||||
destructive: "bg-error text-on-error hover:bg-error/90",
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: "h-8 px-3 text-xs",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-8 text-base",
|
||||
icon: "h-10 w-10",
|
||||
};
|
||||
|
||||
const width = fullWidth ? "w-full" : "";
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${width} ${className}`}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
18
src/components/ui/Card.tsx
Normal file
18
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
export const Card: React.FC<CardProps> = ({ className = '', children, noPadding = false, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className={`bg-surface-container rounded-xl border border-outline-variant/20 shadow-elevation-1 overflow-hidden ${!noPadding ? 'p-4' : ''} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
61
src/components/ui/Modal.tsx
Normal file
61
src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, maxWidth = 'sm' }) => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
if (!isMounted || !isOpen) return null;
|
||||
|
||||
const maxWidthClasses = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 sm:p-6">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className={`relative bg-surface-container w-full ${maxWidthClasses[maxWidth]} rounded-[28px] shadow-elevation-3 animate-in slide-in-from-bottom-10 zoom-in-95 duration-200 flex flex-col max-h-[90vh]`}>
|
||||
<div className="flex items-center justify-between p-6 pb-2 shrink-0">
|
||||
<h3 className="text-xl 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"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 pt-2 overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user