Create Plan from Session. Top bar rounded

This commit is contained in:
AG
2025-12-17 00:44:12 +02:00
parent 46752e0f35
commit 54cd915818
9 changed files with 313 additions and 21 deletions

View File

@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save } from 'lucide-react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save, MoreVertical, ClipboardList } from 'lucide-react';
import { TopBar } from './ui/TopBar';
import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types';
import { t } from '../services/i18n';
@@ -9,6 +11,7 @@ import { useSession } from '../context/SessionContext';
import { useAuth } from '../context/AuthContext';
import { getExercises } from '../services/storage';
import { Button } from './ui/Button';
import { Ripple } from './ui/Ripple';
import { Card } from './ui/Card';
import { Modal } from './ui/Modal';
import { SideSheet } from './ui/SideSheet';
@@ -23,9 +26,11 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
const { sessions, updateSession, deleteSession } = useSession();
const { currentUser } = useAuth();
const userId = currentUser?.id || '';
const navigate = useNavigate();
const [exercises, setExercises] = useState<import('../types').ExerciseDef[]>([]);
const [editingSession, setEditingSession] = useState<WorkoutSession | null>(null);
const [menuState, setMenuState] = useState<{ id: string, x: number, y: number } | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null);
@@ -221,28 +226,23 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div>
</div>
<div className="flex gap-1">
<div className="relative">
<Button
onClick={(e) => {
e.stopPropagation();
setEditingSession(JSON.parse(JSON.stringify(session)));
const rect = e.currentTarget.getBoundingClientRect();
setMenuState({
id: session.id,
x: rect.right + window.scrollX,
y: rect.bottom + window.scrollY
});
}}
variant="ghost"
size="icon"
aria-label="Session Actions"
className="text-on-surface-variant hover:text-primary"
>
<Pencil size={24} />
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
setDeletingId(session.id);
}}
variant="ghost"
size="icon"
className="text-on-surface-variant hover:text-error"
>
<Trash2 size={24} />
<MoreVertical size={24} />
</Button>
</div>
</div>
@@ -328,6 +328,70 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div>
</div>
{/* MENU PORTAL */}
{menuState && typeof document !== 'undefined' && createPortal(
<>
<div
className="fixed inset-0 z-50"
onClick={(e) => {
e.stopPropagation();
setMenuState(null);
}}
/>
<div
className="absolute bg-surface-container-high rounded-xl shadow-elevation-2 z-50 min-w-[160px] py-1 flex flex-col overflow-hidden animate-menu-enter origin-top-right"
style={{
top: menuState.y,
left: menuState.x,
}}
>
<button
onClick={(e) => {
e.stopPropagation();
const session = sessions.find(s => s.id === menuState.id);
if (session) {
navigate(`/plans?createFromSessionId=${session.id}`);
}
setMenuState(null);
}}
className="w-full relative overflow-hidden text-left px-4 py-3 hover:bg-on-surface/10 text-on-surface flex items-center gap-3 transition-colors text-sm font-medium"
>
<Ripple />
<ClipboardList size={18} />
{t('create_plan', lang) || 'Create Plan'}
</button>
<button
onClick={(e) => {
e.stopPropagation();
const session = sessions.find(s => s.id === menuState.id);
if (session) {
setEditingSession(JSON.parse(JSON.stringify(session)));
}
setMenuState(null);
}}
className="w-full relative overflow-hidden text-left px-4 py-3 hover:bg-on-surface/10 text-on-surface flex items-center gap-3 transition-colors text-sm font-medium"
>
<Ripple />
<Pencil size={18} />
{t('edit', lang)}
</button>
<button
onClick={(e) => {
e.stopPropagation();
setDeletingId(menuState.id);
setMenuState(null);
}}
className="w-full relative overflow-hidden text-left px-4 py-3 hover:bg-error-container/10 text-error flex items-center gap-3 transition-colors text-sm font-medium"
>
<Ripple color="rgba(242, 184, 181, 0.2)" />
<Trash2 size={18} />
{t('delete', lang)}
</button>
</div>
</>,
document.body
)}
{/* DELETE CONFIRMATION MODAL */}
<Modal
isOpen={!!(deletingId || deletingSetInfo)}

View File

@@ -254,7 +254,44 @@ const Plans: React.FC<PlansProps> = ({ lang }) => {
setShowAISheet(true);
setSearchParams({});
}
}, [searchParams, setSearchParams]);
const sourceSessionId = searchParams.get('createFromSessionId');
if (sourceSessionId && sessions.length > 0) {
const sourceSession = sessions.find(s => s.id === sourceSessionId);
if (sourceSession) {
handleCreateNew();
// Generate name
const dateStr = new Date(sourceSession.startTime).toLocaleDateString();
setName(sourceSession.planName || (lang === 'ru' ? `План от ${dateStr}` : `Plan from ${dateStr}`));
if (sourceSession.note) setDescription(sourceSession.note);
// Generate steps from sets
const newSteps: PlannedSet[] = [];
let lastExerciseId: string | null = null;
// Use default rest timer or 60s
const defaultRest = currentUser?.profile?.restTimerDefault || 60;
sourceSession.sets.forEach(set => {
// Mirror every set from the session to the plan
newSteps.push({
id: generateId(),
exerciseId: set.exerciseId,
exerciseName: set.exerciseName,
exerciseType: set.type,
isWeighted: (set.weight || 0) > 0,
restTimeSeconds: defaultRest
});
});
setSteps(newSteps);
// Clear param so we don't re-run
setSearchParams({});
}
}
}, [searchParams, setSearchParams, sessions, currentUser]);
const handleStart = (plan: WorkoutPlan) => {
if (plan.description && plan.description.trim().length > 0) {

View File

@@ -9,7 +9,7 @@ interface TopBarProps {
export const TopBar: React.FC<TopBarProps> = ({ title, icon: Icon, actions }) => {
return (
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center gap-3 z-10 shrink-0">
<div className="p-4 bg-surface-container shadow-elevation-1 flex items-center gap-3 z-10 shrink-0 rounded-b-[24px]">
{Icon && (
<div className="w-10 h-10 rounded-full bg-secondary-container flex items-center justify-center">
<Icon size={20} className="text-on-secondary-container" />