diff --git a/apps/frontend/app/lib/generals.tsx b/apps/frontend/app/lib/generals.tsx index 7de3deaf74..30ce6f49c4 100644 --- a/apps/frontend/app/lib/generals.tsx +++ b/apps/frontend/app/lib/generals.tsx @@ -430,7 +430,6 @@ export const getMetadataDetailsQuery = (metadataId?: string | null) => .request(MetadataDetailsDocument, { metadataId }) .then((data) => data.metadataDetails) : skipToken, - staleTime: dayjs.duration(1, "day").asMilliseconds(), }); export const getUserMetadataDetailsQuery = (metadataId?: string | null) => diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts index 8dafe83db7..c03aa60db2 100644 --- a/apps/frontend/app/lib/hooks.ts +++ b/apps/frontend/app/lib/hooks.ts @@ -23,7 +23,12 @@ import { getUserMetadataDetailsQuery, selectRandomElement, } from "~/lib/generals"; -import { type InProgressWorkout, useCurrentWorkout } from "~/lib/state/fitness"; +import { + type InProgressWorkout, + useCurrentWorkout, + useCurrentWorkoutStopwatchAtom, + useCurrentWorkoutTimerAtom, +} from "~/lib/state/fitness"; import type { loader as dashboardLoader } from "~/routes/_dashboard"; export const useGetMantineColors = () => { @@ -96,9 +101,13 @@ export const useConfirmSubmit = () => { export const useGetWorkoutStarter = () => { const revalidator = useRevalidator(); - const [_, setCurrentWorkout] = useCurrentWorkout(); + const [_w, setCurrentWorkout] = useCurrentWorkout(); + const [_t, setTimer] = useCurrentWorkoutTimerAtom(); + const [_s, setStopwatch] = useCurrentWorkoutStopwatchAtom(); const fn = (wkt: InProgressWorkout, action: FitnessAction) => { + setTimer(null); + setStopwatch(null); setCurrentWorkout(wkt); window.location.href = $path("/fitness/:action", { action }); revalidator.revalidate(); diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts index f5615dbbf3..dceccb1e77 100644 --- a/apps/frontend/app/lib/state/fitness.ts +++ b/apps/frontend/app/lib/state/fitness.ts @@ -52,7 +52,6 @@ type AlreadyDoneExerciseSet = Pick; type Media = { imageSrc: string; key: string }; export type Exercise = { - name: string; lot: ExerciseLot; identifier: string; exerciseId: string; @@ -87,6 +86,7 @@ export type InProgressWorkout = { replacingExerciseIdx?: number; updateWorkoutTemplateId?: string; durations: Array; + timerDrawerLot: "timer" | "stopwatch"; currentActionOrCompleted: FitnessAction; }; @@ -118,6 +118,7 @@ export const getDefaultWorkout = ( videos: [], supersets: [], exercises: [], + timerDrawerLot: "timer", startTime: date.toISOString(), currentActionOrCompleted: fitnessEntity, durations: [{ from: date.toISOString() }], @@ -132,7 +133,6 @@ export const getExerciseDetailsQuery = (exerciseId: string) => clientGqlService .request(ExerciseDetailsDocument, { exerciseId }) .then((data) => data.exerciseDetails), - staleTime: dayjsLib.duration(1, "day").asMilliseconds(), }); export const getUserExerciseDetailsQuery = (exerciseId: string) => @@ -262,12 +262,23 @@ export type CurrentWorkoutTimer = { triggeredBy?: { exerciseIdentifier: string; setIdx: number }; }; -const timerAtom = atomWithStorage( +const currentWorkoutTimerAtom = atomWithStorage( "CurrentWorkoutTimer", null, ); -export const useTimerAtom = () => useAtom(timerAtom); +export const useCurrentWorkoutTimerAtom = () => + useAtom(currentWorkoutTimerAtom); + +export type CurrentWorkoutStopwatch = Array | null; + +const currentWorkoutStopwatchAtom = atomWithStorage( + "CurrentWorkoutStopwatch", + null, +); + +export const useCurrentWorkoutStopwatchAtom = () => + useAtom(currentWorkoutStopwatchAtom); const measurementsDrawerOpenAtom = atom(false); @@ -308,7 +319,6 @@ export const duplicateOldWorkout = async ( const exerciseDetails = await getExerciseDetails(ex.id); inProgress.exercises.push({ identifier: randomUUID(), - name: exerciseDetails.details.name, isShowDetailsOpen: userFitnessPreferences.logging.showDetailsWhileEditing ? exerciseIdx === 0 : false, @@ -397,7 +407,6 @@ export const addExerciseToWorkout = async ( } draft.exercises.push({ identifier: randomUUID(), - name: exerciseDetails.details.name, isShowDetailsOpen: userFitnessPreferences.logging.showDetailsWhileEditing, exerciseId: ex.name, lot: ex.lot, diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx index 81f5f513b6..e4891f5bf6 100644 --- a/apps/frontend/app/routes/_dashboard.collections.list.tsx +++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx @@ -74,7 +74,6 @@ import { PRO_REQUIRED_MESSAGE, clientGqlService, commaDelimitedString, - dayjsLib, getPartialMetadataDetailsQuery, openConfirmationModal, queryClient, @@ -309,7 +308,6 @@ const DisplayCollection = (props: { } return images; }, - staleTime: dayjsLib.duration(1, "hour").asMilliseconds(), }); const [hoveredStates, setHoveredStates] = useListState([]); diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx index 77765fa87a..4047f816c5 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx @@ -24,6 +24,7 @@ import { ScrollArea, Select, SimpleGrid, + Skeleton, Stack, Table, Text, @@ -36,6 +37,7 @@ import { useMantineTheme, } from "@mantine/core"; import { + type UseListStateHandlers, useDebouncedState, useDidUpdate, useDisclosure, @@ -70,6 +72,9 @@ import { IconChevronUp, IconClipboard, IconClock, + IconDeviceWatch, + IconDeviceWatchCancel, + IconDeviceWatchPause, IconDotsVertical, IconDroplet, IconDropletFilled, @@ -127,7 +132,9 @@ import { useUserUnitSystem, } from "~/lib/hooks"; import { + type CurrentWorkoutStopwatch, type CurrentWorkoutTimer, + type Exercise, type InProgressWorkout, type Superset, convertHistorySetToCurrentSet, @@ -137,10 +144,11 @@ import { getUserExerciseDetailsQuery, getWorkoutDetails, useCurrentWorkout, + useCurrentWorkoutStopwatchAtom, + useCurrentWorkoutTimerAtom, useGetExerciseAtIndex, useGetSetAtIndex, useMeasurementsDrawerOpen, - useTimerAtom, } from "~/lib/state/fitness"; export const loader = async ({ params }: LoaderFunctionArgs) => { @@ -276,7 +284,7 @@ export default function Page() { string | null >(); const [_, setMeasurementsDrawerOpen] = useMeasurementsDrawerOpen(); - const [currentTimer, setCurrentTimer] = useTimerAtom(); + const [currentTimer, setCurrentTimer] = useCurrentWorkoutTimerAtom(); const [assetsModalOpened, setAssetsModalOpened] = useState< string | null | undefined >(undefined); @@ -322,7 +330,11 @@ export default function Page() { const exerciseIdx = currentWorkout?.exercises.findIndex( (c) => c.identifier === triggeredBy.exerciseIdentifier, ); - if (exerciseIdx !== undefined && exerciseIdx !== -1) { + if ( + exerciseIdx !== -1 && + exerciseIdx !== undefined && + userPreferences.fitness.logging.promptForRestTimer + ) { performTasksAfterSetConfirmed(triggeredBy.setIdx, exerciseIdx); } } @@ -393,14 +405,14 @@ export default function Page() { return ( {currentWorkout ? ( - Loading workout...}> + {() => ( <> setAssetsModalOpened(undefined)} /> - ) : ( - - Loading {loaderData.isCreatingTemplate ? "template" : "workout"}... - + + + + + + + + + + )} ); @@ -763,14 +782,29 @@ const formatTimerDuration = (duration: number) => dayjsLib.duration(duration).format("mm:ss"); const RestTimer = () => { + const [currentWorkout] = useCurrentWorkout(); + const [currentTimer] = useCurrentWorkoutTimerAtom(); + const [currentStopwatch] = useCurrentWorkoutStopwatchAtom(); + invariant(currentWorkout); + forceUpdateEverySecond(); - const [currentTimer] = useTimerAtom(); - return currentTimer - ? formatTimerDuration( - dayjsLib(currentTimer.willEndAt).diff(currentTimer.wasPausedAt), - ) - : "Timer"; + const stopwatchMilliSeconds = getStopwatchMilliSeconds(currentStopwatch); + + return match(currentWorkout.timerDrawerLot) + .with("timer", () => + currentTimer + ? formatTimerDuration( + dayjsLib(currentTimer.willEndAt).diff(currentTimer.wasPausedAt), + ) + : "Timer", + ) + .with("stopwatch", () => + currentStopwatch + ? formatTimerDuration(stopwatchMilliSeconds) + : "Stopwatch", + ) + .exhaustive(); }; const WorkoutDurationTimer = (props: { isWorkoutPaused: boolean }) => { @@ -973,27 +1007,15 @@ const CreateSupersetModal = (props: { /> - {cw.exercises.map((ex) => { - const index = exercises.findIndex((e) => e === ex.identifier); - return ( - - ); - })} + {cw.exercises.map((ex) => ( + + ))} + ); +}; + const EditSupersetModal = (props: { onClose: () => void; supersetWith: string; @@ -1032,28 +1089,15 @@ const EditSupersetModal = (props: { Editing {props.superset[1].color} superset: - {cw.exercises.map((ex) => { - const index = exercises.findIndex((e) => e === ex.identifier); - return ( - - ); - })} + {cw.exercises.map((ex) => ( + + ))} + ); +}; + type FuncStartTimer = ( duration: number, triggeredBy: { exerciseIdentifier: string; setIdx: number }, @@ -1169,11 +1249,16 @@ const UploadAssetsModal = (props: { const exercise = exerciseIdx !== -1 ? currentWorkout.exercises[exerciseIdx] : null; + const { data: exerciseDetails } = useQuery({ + ...getExerciseDetailsQuery(exercise?.exerciseId || ""), + enabled: exercise !== null, + }); + return ( props.closeModal()} opened={props.modalOpenedBy !== undefined} - title={`Images for ${exercise ? exercise.name : "the workout"}`} + title={`Images for ${exerciseDetails ? exerciseDetails.name : "the workout"}`} > {fileUploadAllowed ? ( @@ -1290,7 +1375,7 @@ const ExerciseDisplay = (props: { const navigate = useNavigate(); const [parent] = useAutoAnimate(); const [currentWorkout, setCurrentWorkout] = useCurrentWorkout(); - const [currentTimer, _] = useTimerAtom(); + const [currentTimer, _] = useCurrentWorkoutTimerAtom(); const exercise = useGetExerciseAtIndex(props.exerciseIdx); invariant(exercise); const coreDetails = useCoreDetails(); @@ -1363,7 +1448,7 @@ const ExerciseDisplay = (props: { component={Link} to={getExerciseDetailsPath(exercise.exerciseId)} > - {exercise.name} + {exerciseDetails?.name} {didExerciseActivateTimer ? ( @@ -1473,7 +1558,7 @@ const ExerciseDisplay = (props: { leftSection={} onClick={() => { openConfirmationModal( - `This removes '${exercise.name}' and all its sets from your workout. You can not undo this action. Are you sure you want to continue?`, + `This removes '${exerciseDetails?.name}' and all its sets from your workout. You can not undo this action. Are you sure you want to continue?`, () => { const assets = [...exercise.images, ...exercise.videos]; for (const asset of assets) @@ -1733,7 +1818,7 @@ const ExerciseDisplay = (props: { const DisplayExerciseSetRestTimer = (props: { openTimerDrawer: () => void; }) => { - const [currentTimer] = useTimerAtom(); + const [currentTimer] = useCurrentWorkoutTimerAtom(); forceUpdateEverySecond(); if (!currentTimer) return null; @@ -1785,7 +1870,7 @@ const SetDisplay = (props: { const { isCreatingTemplate } = useLoaderData(); const coreDetails = useCoreDetails(); const userPreferences = useUserPreferences(); - const [currentTimer, _] = useTimerAtom(); + const [currentTimer, _] = useCurrentWorkoutTimerAtom(); const [parent] = useAutoAnimate(); const [currentWorkout, setCurrentWorkout] = useCurrentWorkout(); const exercise = useGetExerciseAtIndex(props.exerciseIdx); @@ -2337,27 +2422,139 @@ const styles = { }, }; -const TimerDrawer = (props: { +const restTimerOptions = [180, 300, 480, "Custom"]; + +const getStopwatchMilliSeconds = ( + currentStopwatch: CurrentWorkoutStopwatch, +) => { + if (!currentStopwatch) return 0; + let total = 0; + for (const duration of currentStopwatch) { + total += dayjsLib(duration.to).diff(duration.from); + } + return total; +}; + +const TimerAndStopwatchDrawer = (props: { opened: boolean; onClose: () => void; stopTimer: () => void; pauseOrResumeTimer: () => void; startTimer: (duration: number) => void; }) => { - const [currentTimer, setCurrentTimer] = useTimerAtom(); + const [currentWorkout, setCurrentWorkout] = useCurrentWorkout(); + const [currentTimer, setCurrentTimer] = useCurrentWorkoutTimerAtom(); + const [currentStopwatch, setCurrentStopwatch] = + useCurrentWorkoutStopwatchAtom(); + + invariant(currentWorkout); forceUpdateEverySecond(); + const stopwatchMilliSeconds = getStopwatchMilliSeconds(currentStopwatch); + const isStopwatchPaused = Boolean(currentStopwatch?.at(-1)?.to); + return ( + {!currentTimer && !currentStopwatch ? ( + <> + {restTimerOptions.map((option) => ( + + ))} + + + + ) : null} + {currentStopwatch ? ( + <> + + + {formatTimerDuration(stopwatchMilliSeconds)} + + } + /> + + + ) : null} {currentTimer ? ( <> @@ -2461,48 +2658,7 @@ const TimerDrawer = (props: { - ) : ( - <> - - - - - - )} + ) : null} ); @@ -2560,51 +2716,14 @@ const ReorderDrawer = (props: { gap="xs" > Hold and release to reorder exercises - {exerciseElements.map((de, index) => { - const isForThisExercise = - props.exerciseToReorder === de.identifier; - return ( - - {(provided) => ( - - - - {de.name} - - - {match(getProgressOfExercise(currentWorkout, index)) - .with("complete", () => ) - .with("in-progress", () => ( - - )) - .with("not-started", () => ) - .exhaustive()} - - - - )} - - ); - })} + {exerciseElements.map((exercise, index) => ( + + ))} {provided.placeholder} )} @@ -2614,6 +2733,55 @@ const ReorderDrawer = (props: { ) : null; }; +const ReorderDrawerExerciseElement = (props: { + index: number; + exercise: Exercise; + exerciseToReorder: string | null | undefined; +}) => { + const [currentWorkout] = useCurrentWorkout(); + const isForThisExercise = + props.exerciseToReorder === props.exercise.identifier; + + invariant(currentWorkout); + + const { data: exerciseDetails } = useQuery( + getExerciseDetailsQuery(props.exercise.exerciseId), + ); + + return ( + + {(provided) => ( + + + + {exerciseDetails?.name} + + + {match(getProgressOfExercise(currentWorkout, props.index)) + .with("complete", () => ) + .with("in-progress", () => ) + .with("not-started", () => ) + .exhaustive()} + + + + )} + + ); +}; + const NoteInput = (props: { note: string; noteIdx: number; diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx index 9c7534968c..1be430f64c 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx @@ -354,12 +354,9 @@ export default function Page() { !isNumber(currentWorkout.replacingExerciseIdx) ) return; - const selectedExercise = - draft.exercises[ - currentWorkout.replacingExerciseIdx - ]; - selectedExercise.name = exercise.name; - selectedExercise.exerciseId = exercise.id; + draft.exercises[ + currentWorkout.replacingExerciseIdx + ].exerciseId = exercise.id; draft.replacingExerciseIdx = undefined; }), ); diff --git a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx index 0dffd685b1..8d2293e2b3 100644 --- a/apps/frontend/app/routes/_dashboard.media.genre.list.tsx +++ b/apps/frontend/app/routes/_dashboard.media.genre.list.tsx @@ -31,7 +31,6 @@ import { } from "~/components/common"; import { clientGqlService, - dayjsLib, getPartialMetadataDetailsQuery, queryClient, queryFactory, @@ -149,7 +148,6 @@ const DisplayGenre = (props: { genre: Genre }) => { if (images.length < 4) images = images.splice(0, 1); return images; }, - staleTime: dayjsLib.duration(1, "hour").asMilliseconds(), }); return ( diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx index cd933c6268..57b80ce477 100644 --- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx +++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx @@ -490,7 +490,7 @@ export default function Page() { - Show me notifications related to the current workout + Send me notifications related to the current workout