From 901a12e28a2cccb13a336bbbdb24e752a22ccee4 Mon Sep 17 00:00:00 2001 From: ys677 Date: Mon, 15 Sep 2025 01:15:49 +1200 Subject: [PATCH 1/5] create new component- saveButton --- .../WorkoutRoutines/widgets/saveButton.tsx | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/components/WorkoutRoutines/widgets/saveButton.tsx diff --git a/src/components/WorkoutRoutines/widgets/saveButton.tsx b/src/components/WorkoutRoutines/widgets/saveButton.tsx new file mode 100644 index 00000000..1c862893 --- /dev/null +++ b/src/components/WorkoutRoutines/widgets/saveButton.tsx @@ -0,0 +1,105 @@ +import { Button, ButtonProps } from "@mui/material"; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from "react-i18next"; + +export type SaveStatus = 'idle' | 'loading' | 'success' | 'error'; + +interface SaveButtonProps extends Omit { + onSave: () => Promise | void; + saveStatus?: SaveStatus; + loadingText?: string; + successText?: string; + errorText?: string; + defaultText?: string; + resetDelay?: number; +} + +export const SaveButton: React.FC = ({ + onSave, + saveStatus: externalStatus, + loadingText, + successText, + errorText, + defaultText, + resetDelay = 2000, + ...buttonProps + }) => { + const { t } = useTranslation(); + const [internalStatus, setInternalStatus] = useState('idle'); + + const currentStatus = externalStatus || internalStatus; + + + const getButtonConfig = (status: SaveStatus) => { + switch (status) { + case 'loading': + return { + text: loadingText || t('saving', 'Saving...'), + color: 'primary' as const, + disabled: true + }; + case 'success': + return { + text: successText || `${t('save', 'Save')} ✅`, + color: 'success' as const, + disabled: true + }; + case 'error': + return { + text: errorText || `${t('save', 'Save')} ❌`, + color: 'error' as const, + disabled: false + }; + default: + return { + text: defaultText || t('save', 'Save'), + color: 'primary' as const, + disabled: false + }; + } + }; + + const buttonConfig = getButtonConfig(currentStatus); + + const handleClick = async () => { + if (currentStatus === 'loading') return; + + try { + if (!externalStatus) { + setInternalStatus('loading'); + } + + await onSave(); + + if (!externalStatus) { + setInternalStatus('success'); + } + } catch (error) { + if (!externalStatus) { + setInternalStatus('error'); + } + } + }; + + // reset status + useEffect(() => { + if ((currentStatus === 'success' || currentStatus === 'error') && !externalStatus) { + const timer = setTimeout(() => { + setInternalStatus('idle'); + }, resetDelay); + + return () => clearTimeout(timer); + } + }, [currentStatus, resetDelay, externalStatus]); + + return ( + + ); +}; \ No newline at end of file From 3eeee8d7f3a527e7996426587641bd6ad1396a9f Mon Sep 17 00:00:00 2001 From: ys677 Date: Mon, 22 Sep 2025 13:20:45 +1200 Subject: [PATCH 2/5] added handleSave function,replaced Standard Button with SaveButton, which import from /WorkoutRoutines/widgets/saveButton --- .../widgets/forms/RoutineForm.tsx | 117 ++++++++++-------- 1 file changed, 64 insertions(+), 53 deletions(-) diff --git a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx index 99b773dd..5645295e 100644 --- a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx +++ b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx @@ -17,6 +17,7 @@ import { } from "components/WorkoutRoutines/models/Routine"; import { useAddRoutineQuery, useEditRoutineQuery } from "components/WorkoutRoutines/queries/routines"; import { SlotEntryRoundingField } from "components/WorkoutRoutines/widgets/forms/SlotEntryForm"; +import { SaveButton } from "components/WorkoutRoutines/widgets/saveButton"; import { Form, Formik } from "formik"; import { DateTime } from "luxon"; import React, { useState } from 'react'; @@ -27,16 +28,13 @@ import * as yup from 'yup'; interface RoutineFormProps { routine?: Routine, - isTemplate?: boolean, - isPublicTemplate?: boolean, closeFn?: Function, } -export const RoutineForm = ({ routine, isTemplate = true, isPublicTemplate = true, closeFn }: RoutineFormProps) => { - +export const RoutineForm = ({ routine, closeFn }: RoutineFormProps) => { const [t, i18n] = useTranslation(); const addRoutineQuery = useAddRoutineQuery(); - const editRoutineQuery = useEditRoutineQuery(routine?.id ?? -1); + const editRoutineQuery = useEditRoutineQuery(routine?.id!); const navigate = useNavigate(); /* @@ -100,6 +98,33 @@ export const RoutineForm = ({ routine, isTemplate = true, isPublicTemplate = tru fitInWeek: yup.boolean() }); + // new handle save + const handleSave = async (values: any) => { + if (routine) { + // when editing routine + await editRoutineQuery.mutateAsync({ + ...values, + fit_in_week: values.fitInWeek, + start: values.start?.toISODate()!, + end: values.end?.toISODate()!, + id: routine.id + }); + } else { + // when create new routine + const result = await addRoutineQuery.mutateAsync({ + ...values, + fit_in_week: values.fitInWeek, + start: values.start?.toISODate()!, + end: values.end?.toISODate()!, + }); + + navigate(makeLink(WgerLink.ROUTINE_EDIT, i18n.language, { id: result.id })); + + if (closeFn) { + closeFn(); + } + } + }; return ( ( { - if (routine) { - const newRoutine = Routine.clone(routine); - newRoutine.fitInWeek = values.fitInWeek; - newRoutine.name = values.name; - newRoutine.description = values.description; - newRoutine.start = values.start!.toJSDate(); - newRoutine.end = values.end!.toJSDate(); - editRoutineQuery.mutate(newRoutine); - - } else { - const result = await addRoutineQuery.mutateAsync(new Routine({ - id: null, - name: values.name, - description: values.description, - created: new Date(), - start: values.start!.toJSDate(), - end: values.end?.toJSDate(), - fitInWeek: values.fitInWeek, - isTemplate: isTemplate, - isPublic: isPublicTemplate - })); - - navigate(makeLink(WgerLink.ROUTINE_EDIT, i18n.language, { id: result.id! })); - - if (closeFn) { - closeFn(); - } - } - }} + onSubmit={handleSave} // use the new handle save > {formik => (
- - + - - - - + - + + + + - + sx={{ mt: 2 }} + /> From 3a2a231d5e80b9a3549825ca8e05e389233ade76 Mon Sep 17 00:00:00 2001 From: ys677 Date: Mon, 22 Sep 2025 19:34:55 +1200 Subject: [PATCH 3/5] rename the file to fit in --- .../WorkoutRoutines/widgets/{saveButton.tsx => SaveButton.tsx} | 2 +- src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/components/WorkoutRoutines/widgets/{saveButton.tsx => SaveButton.tsx} (99%) diff --git a/src/components/WorkoutRoutines/widgets/saveButton.tsx b/src/components/WorkoutRoutines/widgets/SaveButton.tsx similarity index 99% rename from src/components/WorkoutRoutines/widgets/saveButton.tsx rename to src/components/WorkoutRoutines/widgets/SaveButton.tsx index 1c862893..bec50326 100644 --- a/src/components/WorkoutRoutines/widgets/saveButton.tsx +++ b/src/components/WorkoutRoutines/widgets/SaveButton.tsx @@ -21,7 +21,7 @@ export const SaveButton: React.FC = ({ successText, errorText, defaultText, - resetDelay = 2000, + resetDelay = 1500, ...buttonProps }) => { const { t } = useTranslation(); diff --git a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx index 5645295e..94bec348 100644 --- a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx +++ b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx @@ -17,7 +17,7 @@ import { } from "components/WorkoutRoutines/models/Routine"; import { useAddRoutineQuery, useEditRoutineQuery } from "components/WorkoutRoutines/queries/routines"; import { SlotEntryRoundingField } from "components/WorkoutRoutines/widgets/forms/SlotEntryForm"; -import { SaveButton } from "components/WorkoutRoutines/widgets/saveButton"; +import { SaveButton } from "components/WorkoutRoutines/widgets/SaveButton"; import { Form, Formik } from "formik"; import { DateTime } from "luxon"; import React, { useState } from 'react'; From 3d59dc96466bbd3f86f3e7629b32cc3cd3234526 Mon Sep 17 00:00:00 2001 From: ys677 Date: Wed, 24 Sep 2025 18:24:57 +1200 Subject: [PATCH 4/5] Build Routine objects in handleSave for both edit and create paths. --- .../widgets/forms/RoutineForm.tsx | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx index 94bec348..76885570 100644 --- a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx +++ b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx @@ -102,23 +102,40 @@ export const RoutineForm = ({ routine, closeFn }: RoutineFormProps) => { const handleSave = async (values: any) => { if (routine) { // when editing routine - await editRoutineQuery.mutateAsync({ - ...values, - fit_in_week: values.fitInWeek, - start: values.start?.toISODate()!, - end: values.end?.toISODate()!, - id: routine.id + const updatedRoutine: Routine = new Routine({ + id: routine.id, + name: values.name, + description: values.description ?? '', + created: routine.created, + start: values.start?.toJSDate() ?? routine.start, + end: values.end?.toJSDate() ?? routine.end, + fitInWeek: values.fitInWeek ?? routine.fitInWeek, + isTemplate: routine.isTemplate, + isPublic: routine.isPublic, + days: routine.days, + dayData: routine.dayData, }); + + await editRoutineQuery.mutateAsync(updatedRoutine); } else { - // when create new routine - const result = await addRoutineQuery.mutateAsync({ - ...values, - fit_in_week: values.fitInWeek, - start: values.start?.toISODate()!, - end: values.end?.toISODate()!, + // when creating a new routine + const newRoutine: Routine = new Routine({ + id: null, + name: values.name, + description: values.description ?? '', + created: new Date(), + start: values.start?.toJSDate() ?? new Date(), + end: values.end?.toJSDate() ?? new Date(), + fitInWeek: values.fitInWeek ?? true, + isTemplate: false, + isPublic: false, + days: [], + dayData: [], }); - navigate(makeLink(WgerLink.ROUTINE_EDIT, i18n.language, { id: result.id })); + const result = await addRoutineQuery.mutateAsync(newRoutine); + + navigate(makeLink(WgerLink.ROUTINE_EDIT, i18n.language, { id: result.id! })); if (closeFn) { closeFn(); From 7507eea4c4e604a57e18173cd24dc70cb274b1e0 Mon Sep 17 00:00:00 2001 From: ys677 Date: Wed, 24 Sep 2025 18:46:20 +1200 Subject: [PATCH 5/5] import the FormQueryErrors. keep the same color of save button. --- .../WorkoutRoutines/widgets/SaveButton.tsx | 14 ++++++++------ .../WorkoutRoutines/widgets/forms/RoutineForm.tsx | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/WorkoutRoutines/widgets/SaveButton.tsx b/src/components/WorkoutRoutines/widgets/SaveButton.tsx index bec50326..24d6eb68 100644 --- a/src/components/WorkoutRoutines/widgets/SaveButton.tsx +++ b/src/components/WorkoutRoutines/widgets/SaveButton.tsx @@ -26,6 +26,7 @@ export const SaveButton: React.FC = ({ }) => { const { t } = useTranslation(); const [internalStatus, setInternalStatus] = useState('idle'); + const [isCoolingDown, setIsCoolingDown] = useState(false); const currentStatus = externalStatus || internalStatus; @@ -35,25 +36,21 @@ export const SaveButton: React.FC = ({ case 'loading': return { text: loadingText || t('saving', 'Saving...'), - color: 'primary' as const, disabled: true }; case 'success': return { text: successText || `${t('save', 'Save')} ✅`, - color: 'success' as const, disabled: true }; case 'error': return { text: errorText || `${t('save', 'Save')} ❌`, - color: 'error' as const, disabled: false }; default: return { text: defaultText || t('save', 'Save'), - color: 'primary' as const, disabled: false }; } @@ -65,6 +62,10 @@ export const SaveButton: React.FC = ({ if (currentStatus === 'loading') return; try { + // Start cooldown immediately on click + setIsCoolingDown(true); + const cooldownTimer = setTimeout(() => setIsCoolingDown(false), resetDelay); + if (!externalStatus) { setInternalStatus('loading'); } @@ -78,6 +79,8 @@ export const SaveButton: React.FC = ({ if (!externalStatus) { setInternalStatus('error'); } + } finally { + // Ensure cooldown timer exists for at least resetDelay; already set above } }; @@ -95,8 +98,7 @@ export const SaveButton: React.FC = ({ return (