diff --git a/src/components/WorkoutRoutines/widgets/SaveButton.tsx b/src/components/WorkoutRoutines/widgets/SaveButton.tsx new file mode 100644 index 00000000..24d6eb68 --- /dev/null +++ b/src/components/WorkoutRoutines/widgets/SaveButton.tsx @@ -0,0 +1,107 @@ +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 = 1500, + ...buttonProps + }) => { + const { t } = useTranslation(); + const [internalStatus, setInternalStatus] = useState('idle'); + const [isCoolingDown, setIsCoolingDown] = useState(false); + + const currentStatus = externalStatus || internalStatus; + + + const getButtonConfig = (status: SaveStatus) => { + switch (status) { + case 'loading': + return { + text: loadingText || t('saving', 'Saving...'), + disabled: true + }; + case 'success': + return { + text: successText || `${t('save', 'Save')} ✅`, + disabled: true + }; + case 'error': + return { + text: errorText || `${t('save', 'Save')} ❌`, + disabled: false + }; + default: + return { + text: defaultText || t('save', 'Save'), + disabled: false + }; + } + }; + + const buttonConfig = getButtonConfig(currentStatus); + + const handleClick = async () => { + if (currentStatus === 'loading') return; + + try { + // Start cooldown immediately on click + setIsCoolingDown(true); + const cooldownTimer = setTimeout(() => setIsCoolingDown(false), resetDelay); + + if (!externalStatus) { + setInternalStatus('loading'); + } + + await onSave(); + + if (!externalStatus) { + setInternalStatus('success'); + } + } catch (error) { + if (!externalStatus) { + setInternalStatus('error'); + } + } finally { + // Ensure cooldown timer exists for at least resetDelay; already set above + } + }; + + // 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 diff --git a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx index 59a61a66..bfccf566 100644 --- a/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx +++ b/src/components/WorkoutRoutines/widgets/forms/RoutineForm.tsx @@ -16,7 +16,9 @@ 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 { FormQueryErrors } from "components/Core/Widgets/FormError"; import { DateTime } from "luxon"; import React, { useState } from 'react'; import { useTranslation } from "react-i18next"; @@ -145,18 +147,13 @@ export const RoutineForm = ({ {formik => (
- - - - - + - + + + + - + + + + - + sx={{ mt: 2 }} + />