1. Session end time saving. 2. Plan Id to the saved session. 3. History page redesigned (attributes moved, sets list hidden. 4. Session duration added. 5. Session start and end time logging fixed.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,7 +11,7 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
server/prisma/dev.db
|
||||
*.db
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
||||
@@ -41,6 +41,23 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
|
||||
return new Date(value).getTime();
|
||||
};
|
||||
|
||||
const formatDuration = (startTime: number, endTime?: number) => {
|
||||
if (!endTime || isNaN(endTime) || isNaN(startTime)) return '';
|
||||
const durationMs = endTime - startTime;
|
||||
if (durationMs < 0 || isNaN(durationMs)) return '';
|
||||
|
||||
const hours = Math.floor(durationMs / 3600000);
|
||||
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
if (minutes < 1) {
|
||||
return '<1m';
|
||||
}
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
if (editingSession && onUpdateSession) {
|
||||
onUpdateSession(editingSession);
|
||||
@@ -91,79 +108,64 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
|
||||
const totalWork = calculateSessionWork(session);
|
||||
|
||||
return (
|
||||
<div key={session.id} className="bg-surface-container rounded-xl p-5 shadow-elevation-1 border border-outline-variant/20">
|
||||
<div className="flex justify-between items-start mb-4 border-b border-outline-variant pb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-tertiary-container text-on-tertiary-container flex items-center justify-center">
|
||||
<Calendar size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-on-surface text-lg">
|
||||
{new Date(session.startTime).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { weekday: 'long', day: 'numeric', month: 'long' })}
|
||||
</div>
|
||||
<div className="text-xs text-on-surface-variant flex items-center gap-2">
|
||||
<span>{new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
{session.userBodyWeight && <span className="px-2 py-0.5 rounded-full bg-surface-container-high text-on-surface">{session.userBodyWeight}kg</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
<div
|
||||
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"
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Pencil size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeletingId(session.id)}
|
||||
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{Array.from(new Set(session.sets.map(s => s.exerciseName))).slice(0, 4).map(exName => {
|
||||
const sets = session.sets.filter(s => s.exerciseName === exName);
|
||||
const count = sets.length;
|
||||
const bestSet = sets[0];
|
||||
let detail = "";
|
||||
if (bestSet.type === ExerciseType.HIGH_JUMP) detail = `${t('max', lang)}: ${Math.max(...sets.map(s => s.height || 0))}cm`;
|
||||
else if (bestSet.type === ExerciseType.LONG_JUMP) detail = `${t('max', lang)}: ${Math.max(...sets.map(s => s.distanceMeters || 0))}m`;
|
||||
else if (bestSet.type === ExerciseType.STRENGTH) detail = `${t('upto', lang)} ${Math.max(...sets.map(s => s.weight || 0))}kg`;
|
||||
|
||||
return (
|
||||
<div key={`${session.id}-${exName}`} className="flex justify-between text-sm items-center">
|
||||
<span className="text-on-surface">{exName}</span>
|
||||
<span className="text-on-surface-variant flex gap-2 items-center">
|
||||
{detail && <span className="text-[10px] bg-surface-container-high px-2 py-0.5 rounded text-on-surface-variant">{detail}</span>}
|
||||
<span>{count}</span>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className="font-medium text-on-surface text-lg">
|
||||
{new Date(session.startTime).toISOString().split('T')[0]}
|
||||
</span>
|
||||
<span className="text-sm text-on-surface-variant">
|
||||
{new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
{session.endTime && (
|
||||
<span className="text-sm text-on-surface-variant">
|
||||
{formatDuration(session.startTime, session.endTime)}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-on-surface-variant">
|
||||
{session.planName || t('no_plan', lang)}
|
||||
</span>
|
||||
{session.userBodyWeight && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-surface-container-high text-on-surface text-xs">
|
||||
{session.userBodyWeight}kg
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{new Set(session.sets.map(s => s.exerciseName)).size > 4 && (
|
||||
<div className="text-xs text-center text-on-surface-variant mt-2">
|
||||
+ ...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-outline-variant flex justify-between items-center">
|
||||
<div className="flex gap-4">
|
||||
<span className="text-xs text-on-surface-variant">{t('sets_count', lang)}: <span className="text-on-surface font-medium">{session.sets.length}</span></span>
|
||||
<div className="mt-2 text-xs text-on-surface-variant">
|
||||
{t('sets_count', lang)}: <span className="text-on-surface font-medium">{session.sets.length}</span>
|
||||
{totalWork > 0 && (
|
||||
<span className="text-xs text-on-surface-variant flex items-center gap-1">
|
||||
<span className="ml-4 inline-flex items-center gap-1">
|
||||
<Scale size={12} />
|
||||
{(totalWork / 1000).toFixed(1)}t
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-primary text-xs font-bold tracking-wide">
|
||||
<TrendingUp size={14} />
|
||||
{t('finished', lang)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<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"
|
||||
>
|
||||
<Pencil size={20} />
|
||||
</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"
|
||||
>
|
||||
<Trash2 size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,8 +253,12 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
|
||||
<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}</span>
|
||||
</div>
|
||||
<button onClick={() => handleDeleteSet(set.id)} className="text-on-surface-variant hover:text-error">
|
||||
<X size={18} />
|
||||
<button
|
||||
onClick={() => handleDeleteSet(set.id)}
|
||||
className="text-on-surface-variant hover:text-error p-1 rounded hover:bg-error-container/10 transition-colors"
|
||||
title={t('delete', lang)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -290,7 +296,39 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Add other fields similarly styled if needed */}
|
||||
{(set.type === ExerciseType.CARDIO || 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"><Timer size={10} /> {t('time_sec', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
|
||||
value={set.durationSeconds ?? ''}
|
||||
onChange={(e) => handleUpdateSet(set.id, 'durationSeconds', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(set.type === ExerciseType.CARDIO || set.type === ExerciseType.LONG_JUMP) && (
|
||||
<div className="bg-surface-container-high rounded px-2 py-1">
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><ArrowRight size={10} /> {t('dist_m', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
|
||||
value={set.distanceMeters ?? ''}
|
||||
onChange={(e) => handleUpdateSet(set.id, 'distanceMeters', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(set.type === ExerciseType.HIGH_JUMP) && (
|
||||
<div className="bg-surface-container-high rounded px-2 py-1">
|
||||
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><ArrowUp size={10} /> {t('height_cm', lang)}</label>
|
||||
<input
|
||||
type="number"
|
||||
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
|
||||
value={set.height ?? ''}
|
||||
onChange={(e) => handleUpdateSet(set.id, 'height', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Binary file not shown.
@@ -57,6 +57,8 @@ model WorkoutSession {
|
||||
endTime DateTime?
|
||||
userBodyWeight Float?
|
||||
note String?
|
||||
planId String?
|
||||
planName String?
|
||||
|
||||
sets WorkoutSet[]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ router.get('/', async (req: any, res) => {
|
||||
router.post('/', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id, startTime, endTime, userBodyWeight, note, sets } = req.body;
|
||||
const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body;
|
||||
|
||||
// Convert timestamps to Date objects if they are numbers
|
||||
const start = new Date(startTime);
|
||||
@@ -62,6 +62,8 @@ router.post('/', async (req: any, res) => {
|
||||
endTime: end,
|
||||
userBodyWeight: weight,
|
||||
note,
|
||||
planId,
|
||||
planName,
|
||||
sets: {
|
||||
create: sets.map((s: any, idx: number) => ({
|
||||
exerciseId: s.exerciseId,
|
||||
@@ -87,6 +89,8 @@ router.post('/', async (req: any, res) => {
|
||||
endTime: end,
|
||||
userBodyWeight: weight,
|
||||
note,
|
||||
planId,
|
||||
planName,
|
||||
sets: {
|
||||
create: sets.map((s: any, idx: number) => ({
|
||||
exerciseId: s.exerciseId,
|
||||
|
||||
@@ -92,6 +92,7 @@ const translations = {
|
||||
end_time: 'End',
|
||||
max: 'Max',
|
||||
upto: 'Up to',
|
||||
no_plan: 'No plan',
|
||||
|
||||
// Plans
|
||||
plans_empty: 'No plans created',
|
||||
@@ -232,6 +233,7 @@ const translations = {
|
||||
end_time: 'Конец',
|
||||
max: 'Макс',
|
||||
upto: 'До',
|
||||
no_plan: 'Без плана',
|
||||
|
||||
// Plans
|
||||
plans_empty: 'Нет созданных планов',
|
||||
|
||||
@@ -3,7 +3,13 @@ import { api } from './api';
|
||||
|
||||
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
|
||||
try {
|
||||
return await api.get('/sessions');
|
||||
const sessions = await api.get('/sessions');
|
||||
// Convert ISO date strings to timestamps
|
||||
return sessions.map((session: any) => ({
|
||||
...session,
|
||||
startTime: new Date(session.startTime).getTime(),
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user