From 72867668d4b71ce6230e59a838b2f6b1419f7472 Mon Sep 17 00:00:00 2001 From: AG Date: Mon, 24 Nov 2025 23:22:09 +0200 Subject: [PATCH] 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. --- .gitignore | 2 +- components/History.tsx | 160 +++++++++++++++++++++------------- server/prisma/dev.db | Bin 61440 -> 61440 bytes server/prisma/schema.prisma | 2 + server/src/routes/sessions.ts | 6 +- services/i18n.ts | 2 + services/storage.ts | 8 +- 7 files changed, 116 insertions(+), 64 deletions(-) diff --git a/.gitignore b/.gitignore index cb5b281..3c3bbbb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ node_modules dist dist-ssr *.local -server/prisma/dev.db +*.db # Editor directories and files .vscode/* diff --git a/components/History.tsx b/components/History.tsx index 112ffca..67a08aa 100644 --- a/components/History.tsx +++ b/components/History.tsx @@ -41,6 +41,23 @@ const History: React.FC = ({ 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,81 +108,66 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe const totalWork = calculateSessionWork(session); return ( -
-
-
-
- +
setEditingSession(JSON.parse(JSON.stringify(session)))} + > +
+
+
+ + {new Date(session.startTime).toISOString().split('T')[0]} + + + {new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + + {session.endTime && ( + + {formatDuration(session.startTime, session.endTime)} + + )} + + {session.planName || t('no_plan', lang)} + + {session.userBodyWeight && ( + + {session.userBodyWeight}kg + + )}
-
-
- {new Date(session.startTime).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { weekday: 'long', day: 'numeric', month: 'long' })} -
-
- {new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - {session.userBodyWeight && {session.userBodyWeight}kg} -
+
+ {t('sets_count', lang)}: {session.sets.length} + {totalWork > 0 && ( + + + {(totalWork / 1000).toFixed(1)}t + + )}
- -
- {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 ( -
- {exName} - - {detail && {detail}} - {count} - -
- ); - })} - {new Set(session.sets.map(s => s.exerciseName)).size > 4 && ( -
- + ... -
- )} -
- -
-
- {t('sets_count', lang)}: {session.sets.length} - {totalWork > 0 && ( - - - {(totalWork / 1000).toFixed(1)}t - - )} -
-
- - {t('finished', lang)} -
-
) })} @@ -251,8 +253,12 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe {idx + 1} {set.exerciseName}
-
@@ -290,7 +296,39 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe />
)} - {/* Add other fields similarly styled if needed */} + {(set.type === ExerciseType.CARDIO || set.type === ExerciseType.STATIC) && ( +
+ + handleUpdateSet(set.id, 'durationSeconds', parseFloat(e.target.value))} + /> +
+ )} + {(set.type === ExerciseType.CARDIO || set.type === ExerciseType.LONG_JUMP) && ( +
+ + handleUpdateSet(set.id, 'distanceMeters', parseFloat(e.target.value))} + /> +
+ )} + {(set.type === ExerciseType.HIGH_JUMP) && ( +
+ + handleUpdateSet(set.id, 'height', parseFloat(e.target.value))} + /> +
+ )}
))} diff --git a/server/prisma/dev.db b/server/prisma/dev.db index eb065402bddc6534bc3be9f61660f26fc012d5e3..2911abf32e2862131673ab52fc2cfd575370ad2b 100644 GIT binary patch delta 2411 zcmb7FO=w+37`^wsH0gVJ@7`viR_h{!stI($%-s1EL=l=If{T*p52l6r?MBlsB*rLB zMU2vgSmHvnkRU>LX(iIsLU2*{ZUjXTin!=nv}?b)Nt$ljUf*yZT;9z2&YbUj=Uuqf zzHqDk`H`_RS(cUX?0t$$$EqV|vgxIB{m#{_$Zb}nrB%)wS-$%{#3d!$bWde`oXi&T}H4 z+5G$Y5Sjy2{e`GK?a3IgH)z}yl zkz*zx<{3fO28(cHh6^u(lWO(5^H0CIwVr>HuWhZryf`!0+dXWxQ`~EbSmhKP))rZ1 zEIQrp?4V~ESJ7LKCDPV$#H82AQjP&%TkN>Dp7MnM?AG=90`RYY{nHG{^;JzS%c9mq z2%?Zuo^z*avia=q_!~DlF4(O+3&hiXt5%M+8@rQX)8x@$Nay!|L|=C!X5c z?wt=a#b8P%#vHI<2Ys-`ELEc3Ee%g3Hsj;Hz z=O!|@LD$KBLEZfoO^8ht80sfy9|DtqS{B8Z#o_*Re|q%CdL8ec?4AV2(eCWKz3yNz z=wx}8bv!gwvMM0Q3HXM4gKS_J1(v*Ib?fqC~c`UJa$N` z#3x!%n-YsC{NH$@ocA2U|Ka(ZpvSujG9}G#5T{qlQ?OXciXZkCB+bA5I#Qb?O!D$n zlfW2FQP^bsyJ_QclWFci?UPMLGudFO$)Js-+G%W-A2v6`39`u)97vE&Ts@RF?zwaR XLH2B?olWf4dZs<-G%?RNS6}!Kr>0}( delta 1140 zcma)*K}Z!r7{_OKpT6F6->!NvLSCpmGs%&e-PzgM4%It!$$}0p6sR#fy9-5W@jQqX zicyCGQ`VDoh@OW|CcInM=qM6hIuuAJp-vHxj?LN&3o>?xJ8m4lZxxKr-!7F5V5m)CTs3#prZLdL^-qPZ)%m?fU+0?AXsWNRi(soA>6GQ+eNS zYf}Ixt`pc%2pPixB#gw65l_ItMKR_Mcif04I}0aRedNkhWxYNUp7$qmEf_gA7ET<) z$c_ahgki`irO@^~Oe2>whFb8D=1X{}=kEA~)`DZXNgd%qOIAX{WrqMW3SGf*NFpbu zAxcjktq@l7h12Qk& z2AaziecwN#C;$|g@Z6BO1bQfnA@MkuQ$rYXH})uFVGxH-@^ZP7@R^FG>vLL)7WPqU zSq^m^3`5Q$88s0>&Mb)%Hx$@K0#lm2ljxai73*?tPCYo4*sg~HW3KfNsKwX|avt*U%ag-RA_FSgG81c*H{+W-In diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 1e3235c..3fefc09 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -57,6 +57,8 @@ model WorkoutSession { endTime DateTime? userBodyWeight Float? note String? + planId String? + planName String? sets WorkoutSet[] } diff --git a/server/src/routes/sessions.ts b/server/src/routes/sessions.ts index ae4928f..80954f6 100644 --- a/server/src/routes/sessions.ts +++ b/server/src/routes/sessions.ts @@ -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, diff --git a/services/i18n.ts b/services/i18n.ts index 12d1632..321b166 100644 --- a/services/i18n.ts +++ b/services/i18n.ts @@ -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: 'Нет созданных планов', diff --git a/services/storage.ts b/services/storage.ts index ce20261..3f79ca0 100644 --- a/services/storage.ts +++ b/services/storage.ts @@ -3,7 +3,13 @@ import { api } from './api'; export const getSessions = async (userId: string): Promise => { 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 []; }