UI refactoring: Profile, History, and Plans Components

This commit is contained in:
AG
2025-12-07 23:59:33 +02:00
parent 57f7ad077e
commit a3a9aa7194
8 changed files with 829 additions and 667 deletions

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View 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";

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

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