Repository: IgnisDa/ryot (language: rust)

Issue:
Feature request - Books can have quotes
I'm seeking a way to collect all the quotes to the specific book and i would love to use ryot for this do you think its simple to implement ?


Discussion / hints:
Should be simple to implement via #578.
But the current implementation does not allow for adding a media to collection multiple times.

Note to self: maybe an array type of information field.

Fix patch:
diff --git a/apps/backend/src/fitness/resolver.rs b/apps/backend/src/fitness/resolver.rs
index 60930af1ea..c1df3426e3 100644
--- a/apps/backend/src/fitness/resolver.rs
+++ b/apps/backend/src/fitness/resolver.rs
@@ -111,13 +111,6 @@ struct UserExerciseDetails {
     collections: Vec<collection::Model>,
 }
 
-#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
-struct UserExerciseDetailsInput {
-    exercise_id: String,
-    /// The number of elements to return in the history.
-    take_history: Option<u64>,
-}
-
 #[derive(Clone, Debug, Deserialize, Serialize, InputObject)]
 struct EditUserWorkoutInput {
     id: String,
@@ -191,11 +184,11 @@ impl ExerciseQuery {
     async fn user_exercise_details(
         &self,
         gql_ctx: &Context<'_>,
-        input: UserExerciseDetailsInput,
+        exercise_id: String,
     ) -> Result<UserExerciseDetails> {
         let service = gql_ctx.data_unchecked::<Arc<ExerciseService>>();
         let user_id = service.user_id_from_ctx(gql_ctx).await?;
-        service.user_exercise_details(user_id, input).await
+        service.user_exercise_details(user_id, exercise_id).await
     }
 
     /// Get all the measurements for a user.
@@ -382,7 +375,7 @@ impl ExerciseService {
     async fn user_exercise_details(
         &self,
         user_id: String,
-        input: UserExerciseDetailsInput,
+        exercise_id: String,
     ) -> Result<UserExerciseDetails> {
         let collections = entity_in_collections(
             &self.db,
@@ -390,7 +383,7 @@ impl ExerciseService {
             None,
             None,
             None,
-            Some(input.exercise_id.clone()),
+            Some(exercise_id.clone()),
         )
         .await?;
         let mut resp = UserExerciseDetails {
@@ -400,7 +393,7 @@ impl ExerciseService {
         };
         if let Some(association) = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(user_id))
-            .filter(user_to_entity::Column::ExerciseId.eq(input.exercise_id))
+            .filter(user_to_entity::Column::ExerciseId.eq(exercise_id))
             .one(&self.db)
             .await?
         {
@@ -417,7 +410,6 @@ impl ExerciseService {
                             .map(|h| h.workout_id.clone()),
                     ),
                 )
-                .limit(input.take_history)
                 .order_by_desc(workout::Column::EndTime)
                 .all(&self.db)
                 .await?;
diff --git a/apps/backend/src/miscellaneous/mod.rs b/apps/backend/src/miscellaneous/mod.rs
index 546e8deb69..9f61b80b65 100644
--- a/apps/backend/src/miscellaneous/mod.rs
+++ b/apps/backend/src/miscellaneous/mod.rs
@@ -16,6 +16,7 @@ pub enum CollectionExtraInformationLot {
     Number,
     Date,
     DateTime,
+    StringArray,
 }
 
 #[derive(
diff --git a/apps/backend/src/models.rs b/apps/backend/src/models.rs
index da02b527b7..95b7d54534 100644
--- a/apps/backend/src/models.rs
+++ b/apps/backend/src/models.rs
@@ -149,7 +149,7 @@ pub struct ChangeCollectionToEntityInput {
     pub person_id: Option<String>,
     pub metadata_group_id: Option<String>,
     pub exercise_id: Option<String>,
-    pub information: Option<HashMap<String, String>>,
+    pub information: Option<serde_json::Value>,
 }
 
 #[derive(
diff --git a/apps/backend/src/utils.rs b/apps/backend/src/utils.rs
index 8ab1ea3a5f..115feb1a46 100644
--- a/apps/backend/src/utils.rs
+++ b/apps/backend/src/utils.rs
@@ -24,7 +24,6 @@ use sea_orm::{
     ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
     PartialModelTrait, QueryFilter,
 };
-use serde_json::Value;
 
 use crate::{
     background::{ApplicationJob, CoreApplicationJob},
@@ -340,16 +339,13 @@ pub async fn add_entity_to_collection(
         to_update.last_updated_on = ActiveValue::Set(Utc::now());
         to_update.update(db).await.is_ok()
     } else {
-        let information = input
-            .information
-            .map(|d| serde_json::from_str::<Value>(&serde_json::to_string(&d).unwrap()).unwrap());
         let created_collection = collection_to_entity::ActiveModel {
             collection_id: ActiveValue::Set(collection.id),
             metadata_id: ActiveValue::Set(input.metadata_id.clone()),
             person_id: ActiveValue::Set(input.person_id.clone()),
             metadata_group_id: ActiveValue::Set(input.metadata_group_id.clone()),
             exercise_id: ActiveValue::Set(input.exercise_id.clone()),
-            information: ActiveValue::Set(information),
+            information: ActiveValue::Set(input.information),
             ..Default::default()
         };
         if let Ok(created) = created_collection.insert(db).await {
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index 95050a9d33..d74cc785ff 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -2,6 +2,7 @@ import { Carousel } from "@mantine/carousel";
 import "@mantine/carousel/styles.css";
 import {
 	ActionIcon,
+	Alert,
 	Anchor,
 	Badge,
 	Box,
@@ -11,24 +12,24 @@ import {
 	Stack,
 	Text,
 	TextInput,
-	useComputedColorScheme,
+	Tooltip,
 } from "@mantine/core";
-import { useDebouncedState, useDidUpdate } from "@mantine/hooks";
+import { useDebouncedValue, useDidUpdate } from "@mantine/hooks";
 import type {
 	MediaLot,
 	MediaSource,
 } from "@ryot/generated/graphql/backend/graphql";
 import { snakeCase } from "@ryot/ts-utils";
 import { IconExternalLink, IconSearch, IconX } from "@tabler/icons-react";
-import { type ReactNode, useRef } from "react";
+import type { ReactNode } from "react";
 import { useState } from "react";
 import { withFragment, withoutHost } from "ufo";
+import { getSurroundingElements, redirectToQueryParam } from "~/lib/generals";
 import {
-	getFallbackImageUrl,
-	getSurroundingElements,
-	redirectToQueryParam,
-} from "~/lib/generals";
-import { useSearchParam } from "~/lib/hooks";
+	useCoreDetails,
+	useFallbackImageUrl,
+	useSearchParam,
+} from "~/lib/hooks";
 import classes from "~/styles/common.module.css";
 
 export const ApplicationGrid = (props: {
@@ -51,7 +52,7 @@ export const MediaDetailsLayout = (props: {
 	};
 }) => {
 	const [activeImageId, setActiveImageId] = useState(0);
-	const colorScheme = useComputedColorScheme("dark");
+	const fallbackImageUrl = useFallbackImageUrl();
 
 	return (
 		<Flex direction={{ base: "column", md: "row" }} gap="lg">
@@ -61,11 +62,7 @@ export const MediaDetailsLayout = (props: {
 				className={classes.imagesContainer}
 			>
 				{props.images.length > 1 ? (
-					<Carousel
-						withIndicators={props.images.length > 1}
-						w={300}
-						onSlideChange={setActiveImageId}
-					>
+					<Carousel w={300} onSlideChange={setActiveImageId}>
 						{props.images.map((url, idx) => (
 							<Carousel.Slide key={url} data-image-idx={idx}>
 								{getSurroundingElements(props.images, activeImageId).includes(
@@ -82,7 +79,7 @@ export const MediaDetailsLayout = (props: {
 							src={props.images[0]}
 							height={400}
 							radius="lg"
-							fallbackSrc={getFallbackImageUrl(colorScheme)}
+							fallbackSrc={fallbackImageUrl}
 						/>
 					</Box>
 				)}
@@ -141,40 +138,27 @@ export const DebouncedSearchInput = (props: {
 	queryParam?: string;
 	placeholder?: string;
 }) => {
-	const [debouncedQuery, setDebouncedQuery] = useDebouncedState(
-		props.initialValue || "",
-		1000,
-	);
+	const [query, setQuery] = useState(props.initialValue || "");
+	const [debounced] = useDebouncedValue(query, 1000);
 	const [_, { setP }] = useSearchParam();
 
-	useDidUpdate(
-		() => setP(props.queryParam || "query", debouncedQuery),
-		[debouncedQuery],
-	);
-
-	const ref = useRef<HTMLInputElement>(null);
+	useDidUpdate(() => {
+		setP(props.queryParam || "query", debounced);
+	}, [debounced]);
 
 	return (
 		<TextInput
-			ref={ref}
 			name="query"
 			placeholder={props.placeholder || "Search..."}
 			leftSection={<IconSearch />}
-			onChange={(e) => setDebouncedQuery(e.currentTarget.value)}
-			defaultValue={debouncedQuery}
+			onChange={(e) => setQuery(e.currentTarget.value)}
+			value={query}
 			style={{ flexGrow: 1 }}
 			autoCapitalize="none"
 			autoComplete="off"
 			rightSection={
-				debouncedQuery ? (
-					<ActionIcon
-						onClick={() => {
-							if (ref.current) {
-								ref.current.value = "";
-								setDebouncedQuery("");
-							}
-						}}
-					>
+				query ? (
+					<ActionIcon onClick={() => setQuery("")}>
 						<IconX size={16} />
 					</ActionIcon>
 				) : null
@@ -182,3 +166,20 @@ export const DebouncedSearchInput = (props: {
 		/>
 	);
 };
+
+export const ProRequiredAlert = (props: { tooltipLabel?: string }) => {
+	const coreDetails = useCoreDetails();
+
+	return !coreDetails.isPro ? (
+		<Alert>
+			<Tooltip label={props.tooltipLabel} disabled={!props.tooltipLabel}>
+				<Text size="xs">
+					<Anchor href={coreDetails.websiteUrl} target="_blank">
+						Ryot Pro
+					</Anchor>{" "}
+					required to use this feature
+				</Text>
+			</Tooltip>
+		</Alert>
+	) : null;
+};
diff --git a/apps/frontend/app/components/confirmation.tsx b/apps/frontend/app/components/confirmation.tsx
index 01014d4638..dd9a55fd16 100644
--- a/apps/frontend/app/components/confirmation.tsx
+++ b/apps/frontend/app/components/confirmation.tsx
@@ -51,7 +51,7 @@ const mounter = createReactTreeMounter();
 
 export const createConfirmation = createConfirmationCreater(mounter);
 
-export const MountPoint = createMountPoint(mounter);
+export const ConfirmationMountPoint = createMountPoint(mounter);
 
 export const confirmWrapper = createConfirmation<
 	ConfirmationProps,
diff --git a/apps/frontend/app/components/media.tsx b/apps/frontend/app/components/media.tsx
index 2898ff8b25..ee1ab88d09 100644
--- a/apps/frontend/app/components/media.tsx
+++ b/apps/frontend/app/components/media.tsx
@@ -23,7 +23,6 @@ import {
 	TextInput,
 	ThemeIcon,
 	Tooltip,
-	useComputedColorScheme,
 } from "@mantine/core";
 import "@mantine/dates/styles.css";
 import { useDisclosure } from "@mantine/hooks";
@@ -63,12 +62,9 @@ import { match } from "ts-pattern";
 import { withQuery, withoutHost } from "ufo";
 import { HiddenLocationInput, MEDIA_DETAILS_HEIGHT } from "~/components/common";
 import { confirmWrapper } from "~/components/confirmation";
+import { dayjsLib, redirectToQueryParam } from "~/lib/generals";
 import {
-	dayjsLib,
-	getFallbackImageUrl,
-	redirectToQueryParam,
-} from "~/lib/generals";
-import {
+	useFallbackImageUrl,
 	useGetMantineColor,
 	useUserDetails,
 	useUserPreferences,
@@ -439,7 +435,7 @@ export const BaseDisplayItem = (props: {
 	nameRight?: ReactNode;
 	mediaReason?: Array<UserToMediaReason> | null;
 }) => {
-	const colorScheme = useComputedColorScheme("dark");
+	const fallbackImageUrl = useFallbackImageUrl(getInitials(props.name));
 
 	const SurroundingElement = (iProps: {
 		children: ReactNode;
@@ -498,10 +494,7 @@ export const BaseDisplayItem = (props: {
 					}}
 					h={260}
 					w={170}
-					fallbackSrc={getFallbackImageUrl(
-						colorScheme,
-						getInitials(props.name),
-					)}
+					fallbackSrc={fallbackImageUrl}
 				/>
 				<Box pos="absolute" style={{ zIndex: 999 }} top={10} left={10}>
 					{props.topLeft}
diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts
index 0576f8589f..8ff89e7574 100644
--- a/apps/frontend/app/lib/generals.ts
+++ b/apps/frontend/app/lib/generals.ts
@@ -1,4 +1,7 @@
-import type { MantineColorScheme } from "@mantine/core";
+import {
+	createQueryKeys,
+	mergeQueryKeys,
+} from "@lukemorales/query-key-factory";
 import {
 	MediaLot,
 	MediaSource,
@@ -42,14 +45,6 @@ export const getSetColor = (l: SetLot) =>
 		.with(SetLot.Normal, () => "indigo.6")
 		.exhaustive();
 
-export const getFallbackImageUrl = (
-	colorScheme: Exclude<MantineColorScheme, "auto">,
-	text = "No Image",
-) =>
-	`https://placehold.co/100x200/${
-		colorScheme === "dark" ? "343632" : "c1c4bb"
-	}/${colorScheme === "dark" ? "FFF" : "121211"}?text=${text}`;
-
 /**
  * Get the correct name of the lot from a string
  */
@@ -232,3 +227,36 @@ export const getSurroundingElements = <T>(
 	if (elementIndex === lastIndex) return [elementIndex - 1, elementIndex, 0];
 	return [elementIndex - 1, elementIndex, elementIndex + 1];
 };
+
+const mediaQueryKeys = createQueryKeys("media", {
+	metadataDetails: (metadataId: string) => ({
+		queryKey: ["metadataDetails", metadataId],
+	}),
+	userMetadataDetails: (metadataId: string) => ({
+		queryKey: ["userMetadataDetails", metadataId],
+	}),
+});
+
+const collectionQueryKeys = createQueryKeys("collections", {
+	userList: (userId: string) => ({
+		queryKey: ["userCollectionsList", userId],
+	}),
+	details: (collectionId: string) => ({
+		queryKey: ["collectionDetails", collectionId],
+	}),
+});
+
+const fitnessQueryKeys = createQueryKeys("fitness", {
+	exerciseDetails: (exerciseId: string) => ({
+		queryKey: ["exerciseDetails", exerciseId],
+	}),
+	userExerciseDetails: (exerciseId: string) => ({
+		queryKey: ["userExerciseDetails", exerciseId],
+	}),
+});
+
+export const queryFactory = mergeQueryKeys(
+	mediaQueryKeys,
+	collectionQueryKeys,
+	fitnessQueryKeys,
+);
diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts
index 888d8752fb..4d342ed933 100644
--- a/apps/frontend/app/lib/hooks.ts
+++ b/apps/frontend/app/lib/hooks.ts
@@ -1,5 +1,5 @@
 import { $path } from "@ignisda/remix-routes";
-import { useMantineTheme } from "@mantine/core";
+import { useComputedColorScheme, useMantineTheme } from "@mantine/core";
 import {
 	useNavigate,
 	useRouteLoaderData,
@@ -9,18 +9,19 @@ import {
 	MetadataDetailsDocument,
 	UserMetadataDetailsDocument,
 } from "@ryot/generated/graphql/backend/graphql";
-import { skipToken, useQuery } from "@tanstack/react-query";
+import { queryOptions, skipToken, useQuery } from "@tanstack/react-query";
 import Cookies from "js-cookie";
 import {
 	CurrentWorkoutKey,
 	clientGqlService,
 	dayjsLib,
 	getStringAsciiValue,
+	queryFactory,
 } from "~/lib/generals";
 import { type InProgressWorkout, useCurrentWorkout } from "~/lib/state/workout";
 import type { loader } from "~/routes/_dashboard";
 
-export function useGetMantineColor() {
+export const useGetMantineColor = () => {
 	const theme = useMantineTheme();
 	const colors = Object.keys(theme.colors);
 
@@ -29,9 +30,16 @@ export function useGetMantineColor() {
 		colors[(getStringAsciiValue(input) + colors.length) % colors.length];
 
 	return getColor;
-}
+};
+
+export const useFallbackImageUrl = (text = "No Image") => {
+	const colorScheme = useComputedColorScheme("dark");
+	return `https://placehold.co/100x200/${
+		colorScheme === "dark" ? "343632" : "c1c4bb"
+	}/${colorScheme === "dark" ? "FFF" : "121211"}?text=${text}`;
+};
 
-export function useSearchParam() {
+export const useSearchParam = () => {
 	const [searchParams, setSearchParams] = useSearchParams();
 
 	const delP = (key: string) => {
@@ -50,9 +58,9 @@ export function useSearchParam() {
 	};
 
 	return [searchParams, { setP, delP }] as const;
-}
+};
 
-export function getWorkoutStarter() {
+export const getWorkoutStarter = () => {
 	const navigate = useNavigate();
 	const [_, setCurrentWorkout] = useCurrentWorkout();
 
@@ -65,11 +73,11 @@ export function getWorkoutStarter() {
 		navigate($path("/fitness/workouts/current"));
 	};
 	return fn;
-}
+};
 
 export const getMetadataDetailsQuery = (metadataId?: string | null) =>
-	({
-		queryKey: ["metadataDetails", metadataId],
+	queryOptions({
+		queryKey: queryFactory.media.metadataDetails(metadataId || "").queryKey,
 		queryFn: metadataId
 			? () =>
 					clientGqlService
@@ -77,15 +85,15 @@ export const getMetadataDetailsQuery = (metadataId?: string | null) =>
 						.then((data) => data.metadataDetails)
 			: skipToken,
 		staleTime: dayjsLib.duration(1, "day").asMilliseconds(),
-	}) as const;
+	});
 
 export const useMetadataDetails = (metadataId?: string | null) => {
 	return useQuery(getMetadataDetailsQuery(metadataId));
 };
 
 export const getUserMetadataDetailsQuery = (metadataId?: string | null) =>
-	({
-		queryKey: ["userMetadataDetails", metadataId],
+	queryOptions({
+		queryKey: queryFactory.media.userMetadataDetails(metadataId || "").queryKey,
 		queryFn: metadataId
 			? () =>
 					clientGqlService
@@ -93,7 +101,7 @@ export const getUserMetadataDetailsQuery = (metadataId?: string | null) =>
 						.then((data) => data.userMetadataDetails)
 			: skipToken,
 		staleTime: Number.POSITIVE_INFINITY,
-	}) as const;
+	});
 
 export const useUserMetadataDetails = (metadataId?: string | null) => {
 	return useQuery(getUserMetadataDetailsQuery(metadataId));
diff --git a/apps/frontend/app/lib/state/workout.ts b/apps/frontend/app/lib/state/workout.ts
index 69b26870f1..ab27a5e51e 100644
--- a/apps/frontend/app/lib/state/workout.ts
+++ b/apps/frontend/app/lib/state/workout.ts
@@ -9,6 +9,7 @@ import {
 	type WorkoutDetailsQuery,
 	type WorkoutSetStatistic,
 } from "@ryot/generated/graphql/backend/graphql";
+import { queryOptions } from "@tanstack/react-query";
 import type { Dayjs } from "dayjs";
 import { createDraft, finishDraft } from "immer";
 import { useAtom } from "jotai";
@@ -17,13 +18,15 @@ import { v4 as randomUUID } from "uuid";
 import {
 	CurrentWorkoutKey,
 	clientGqlService,
+	dayjsLib,
 	queryClient,
+	queryFactory,
 } from "~/lib/generals";
 
 export type ExerciseSet = {
 	statistic: WorkoutSetStatistic;
 	lot: SetLot;
-	confirmedAt?: string | null;
+	confirmedAt: string | null;
 };
 
 type AlreadyDoneExerciseSet = Pick<ExerciseSet, "statistic">;
@@ -75,13 +78,13 @@ export const useGetSetAtIndex = (exerciseIdx: number, setIdx: number) => {
 	return exercise?.sets[setIdx];
 };
 
-function getTimeOfDay(date: Date) {
+const getTimeOfDay = (date: Date) => {
 	const hours = date.getHours();
 	if (hours >= 5 && hours < 12) return "Morning";
 	if (hours >= 12 && hours < 17) return "Afternoon";
 	if (hours >= 17 && hours < 21) return "Evening";
 	return "Night";
-}
+};
 
 export const getDefaultWorkout = (): InProgressWorkout => {
 	const date = new Date();
@@ -95,22 +98,24 @@ export const getDefaultWorkout = (): InProgressWorkout => {
 };
 
 export const getExerciseDetailsQuery = (exerciseId: string) =>
-	({
-		queryKey: ["exerciseDetails", exerciseId],
+	queryOptions({
+		queryKey: queryFactory.fitness.exerciseDetails(exerciseId).queryKey,
 		queryFn: () =>
 			clientGqlService
 				.request(ExerciseDetailsDocument, { exerciseId })
 				.then((data) => data.exerciseDetails),
-	}) as const;
+		staleTime: dayjsLib.duration(1, "day").asMilliseconds(),
+	});
 
 export const getUserExerciseDetailsQuery = (exerciseId: string) =>
-	({
-		queryKey: ["userExerciseDetails", exerciseId],
+	queryOptions({
+		queryKey: queryFactory.fitness.userExerciseDetails(exerciseId).queryKey,
 		queryFn: () =>
 			clientGqlService
-				.request(UserExerciseDetailsDocument, { input: { exerciseId } })
+				.request(UserExerciseDetailsDocument, { exerciseId })
 				.then((data) => data.userExerciseDetails),
-	}) as const;
+		staleTime: Number.POSITIVE_INFINITY,
+	});
 
 const getExerciseDetails = async (exerciseId: string) => {
 	const [details, userDetails] = await Promise.all([
@@ -182,7 +187,7 @@ export const addExerciseToWorkout = async (
 				images: exerciseDetails.details.attributes.images,
 			},
 			lot: ex.lot,
-			sets: [{ statistic: {}, lot: SetLot.Normal }],
+			sets: [{ statistic: {}, lot: SetLot.Normal, confirmedAt: null }],
 			supersetWith: [],
 			alreadyDoneSets:
 				exerciseDetails.userDetails.history?.at(0)?.sets.map((s) => ({
diff --git a/apps/frontend/app/lib/utilities.server.ts b/apps/frontend/app/lib/utilities.server.ts
index a1cdf920ce..f22f9647b1 100644
--- a/apps/frontend/app/lib/utilities.server.ts
+++ b/apps/frontend/app/lib/utilities.server.ts
@@ -30,6 +30,7 @@ import {
 	USER_PREFERENCES_COOKIE_NAME,
 	dayjsLib,
 	queryClient,
+	queryFactory,
 	redirectToQueryParam,
 } from "~/lib/generals";
 
@@ -93,9 +94,9 @@ const expectedServerVariables = z.object({
 /**
  * Combine multiple header objects into one (uses append so headers are not overridden)
  */
-export function combineHeaders(
+export const combineHeaders = (
 	...headers: Array<ResponseInit["headers"] | null | undefined>
-) {
+) => {
 	const combined = new Headers();
 	for (const header of headers) {
 		if (!header) continue;
@@ -103,7 +104,7 @@ export function combineHeaders(
 			combined.append(key, value);
 	}
 	return combined;
-}
+};
 
 const emptyNumberString = z
 	.any()
@@ -138,7 +139,7 @@ export const processSubmission = <Schema extends ZodTypeAny>(
 export const getCachedUserCollectionsList = async (request: Request) => {
 	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
 	return queryClient.ensureQueryData({
-		queryKey: ["userCollectionsList", userDetails.id],
+		queryKey: queryFactory.collections.userList(userDetails.id).queryKey,
 		queryFn: () =>
 			serverGqlService
 				.request(
@@ -154,7 +155,7 @@ export const getCachedUserCollectionsList = async (request: Request) => {
 export const removeCachedUserCollectionsList = async (request: Request) => {
 	const userDetails = await redirectIfNotAuthenticatedOrUpdated(request);
 	queryClient.removeQueries({
-		queryKey: ["userCollectionsList", userDetails.id],
+		queryKey: queryFactory.collections.userList(userDetails.id).queryKey,
 	});
 };
 
@@ -271,26 +272,26 @@ export type OptionalToast = Omit<Toast, "id" | "type"> & {
 	type?: z.infer<typeof TypeSchema>;
 };
 
-export async function redirectWithToast(
+export const redirectWithToast = async (
 	url: string,
 	toast: OptionalToast,
 	init?: ResponseInit,
-) {
+) => {
 	return redirect(url, {
 		...init,
 		headers: combineHeaders(init?.headers, await createToastHeaders(toast)),
 	});
-}
+};
 
-export async function createToastHeaders(optionalToast: OptionalToast) {
+export const createToastHeaders = async (optionalToast: OptionalToast) => {
 	const session = await toastSessionStorage.getSession();
 	const toast = ToastSchema.parse(optionalToast);
 	session.flash(toastKey, toast);
 	const cookie = await toastSessionStorage.commitSession(session);
 	return new Headers({ "set-cookie": cookie });
-}
+};
 
-export async function getToast(request: Request) {
+export const getToast = async (request: Request) => {
 	const session = await toastSessionStorage.getSession(
 		request.headers.get("cookie"),
 	);
@@ -304,7 +305,7 @@ export async function getToast(request: Request) {
 				})
 			: null,
 	};
-}
+};
 
 export const getCookiesForApplication = async (token: string) => {
 	const [{ coreDetails }, { userPreferences }, { userDetails }] =
diff --git a/apps/frontend/app/root.tsx b/apps/frontend/app/root.tsx
index d203493e86..2c4bb81fe8 100644
--- a/apps/frontend/app/root.tsx
+++ b/apps/frontend/app/root.tsx
@@ -28,7 +28,7 @@ import {
 import { QueryClientProvider } from "@tanstack/react-query";
 import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
 import "mantine-datatable/styles.layer.css";
-import { MountPoint } from "~/components/confirmation";
+import { ConfirmationMountPoint } from "~/components/confirmation";
 import { Toaster } from "~/components/toaster";
 import { LOGO_IMAGE_URL, queryClient } from "~/lib/generals";
 import {
@@ -137,7 +137,7 @@ export default function App() {
 						theme={theme}
 						forceColorScheme={loaderData.defaultColorScheme}
 					>
-						<MountPoint />
+						<ConfirmationMountPoint />
 						{navigation.state === "loading" ||
 						navigation.state === "submitting" ? (
 							<Loader
@@ -156,7 +156,7 @@ export default function App() {
 						<ScrollRestoration />
 						<Scripts />
 					</MantineProvider>
-					<ReactQueryDevtools />
+					<ReactQueryDevtools buttonPosition="top-right" />
 				</QueryClientProvider>
 			</body>
 		</html>
diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx
index 99fc96bdf4..1b8291c5a0 100644
--- a/apps/frontend/app/routes/_dashboard._index.tsx
+++ b/apps/frontend/app/routes/_dashboard._index.tsx
@@ -1,8 +1,6 @@
 import { $path } from "@ignisda/remix-routes";
 import {
 	ActionIcon,
-	Alert,
-	Anchor,
 	Box,
 	Center,
 	Container,
@@ -36,14 +34,12 @@ import {
 	isNumber,
 } from "@ryot/ts-utils";
 import {
-	IconAlertCircle,
 	IconBarbell,
 	IconFriends,
 	IconPlayerPlay,
 	IconScaleOutline,
 	IconServer,
 } from "@tabler/icons-react";
-import { parse } from "cookie";
 import { Fragment, type ReactNode } from "react";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
@@ -52,12 +48,7 @@ import {
 	MediaItemWithoutUpdateModal,
 	NewUserGuideAlert,
 } from "~/components/media";
-import {
-	CurrentWorkoutKey,
-	dayjsLib,
-	getLot,
-	getMetadataIcon,
-} from "~/lib/generals";
+import { dayjsLib, getLot, getMetadataIcon } from "~/lib/generals";
 import { useGetMantineColor, useUserPreferences } from "~/lib/hooks";
 import { useMetadataProgressUpdate } from "~/lib/state/media";
 import {
@@ -67,8 +58,6 @@ import {
 	serverGqlService,
 } from "~/lib/utilities.server";
 
-const cookieName = CurrentWorkoutKey;
-
 const getTake = (preferences: UserPreferences, el: DashboardElementLot) => {
 	const t = preferences.general.dashboard.find(
 		(de) => de.section === el,
@@ -113,10 +102,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 			getAuthorizationHeader(request),
 		),
 	]);
-	const cookies = request.headers.get("cookie");
-	const workoutInProgress = parse(cookies || "")[cookieName] === "true";
 	return {
-		workoutInProgress,
 		userPreferences: {
 			reviewScale: preferences.general.reviewScale,
 			dashboard: preferences.general.dashboard,
@@ -142,17 +128,6 @@ export default function Page() {
 	return (
 		<Container>
 			<Stack gap={32}>
-				{loaderData.workoutInProgress ? (
-					<Alert icon={<IconAlertCircle />} variant="outline" color="yellow">
-						<Text>
-							You have a workout in progress. Click{" "}
-							<Anchor component={Link} to={$path("/fitness/workouts/current")}>
-								here
-							</Anchor>{" "}
-							to continue.
-						</Text>
-					</Alert>
-				) : null}
 				{loaderData.latestUserSummary.media.metadataOverall.interactedWith ===
 				0 ? (
 					<NewUserGuideAlert />
diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx
index 7f4093a387..315c4836b9 100644
--- a/apps/frontend/app/routes/_dashboard.collections.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx
@@ -6,10 +6,12 @@ import {
 	Button,
 	Container,
 	Flex,
+	Group,
+	Image,
+	Input,
 	Modal,
-	SimpleGrid,
+	Paper,
 	Stack,
-	Tabs,
 	Text,
 	TextInput,
 	Textarea,
@@ -23,6 +25,7 @@ import {
 	type MetaArgs_SingleFetch,
 	useFetcher,
 	useNavigation,
+	useSearchParams,
 } from "@remix-run/react";
 import {
 	CreateOrUpdateCollectionDocument,
@@ -30,20 +33,20 @@ import {
 	type UserCollectionsListQuery,
 } from "@ryot/generated/graphql/backend/graphql";
 import { truncate } from "@ryot/ts-utils";
-import {
-	IconAssembly,
-	IconEdit,
-	IconPlus,
-	IconTrashFilled,
-	IconUserCog,
-} from "@tabler/icons-react";
+import { IconEdit, IconPlus, IconTrashFilled } from "@tabler/icons-react";
 import { ClientError } from "graphql-request";
 import { useEffect, useRef, useState } from "react";
+import { Virtuoso } from "react-virtuoso";
 import { namedAction } from "remix-utils/named-action";
 import { withQuery } from "ufo";
 import { z } from "zod";
+import { DebouncedSearchInput, ProRequiredAlert } from "~/components/common";
 import { confirmWrapper } from "~/components/confirmation";
-import { useUserCollections, useUserDetails } from "~/lib/hooks";
+import {
+	useFallbackImageUrl,
+	useUserCollections,
+	useUserDetails,
+} from "~/lib/hooks";
 import {
 	createToastHeaders,
 	getAuthorizationHeader,
@@ -144,7 +147,12 @@ type UpdateCollectionInput = {
 export default function Page() {
 	const transition = useNavigation();
 	const collections = useUserCollections();
-	const userCreatedCollections = collections.filter((c) => !c.isDefault);
+	const [params] = useSearchParams();
+	const query = params.get("query") || undefined;
+
+	const filteredCollections = collections.filter((c) =>
+		query ? c.name.toLowerCase().includes(query.toLowerCase()) : true,
+	);
 
 	const [toUpdateCollection, setToUpdateCollection] =
 		useState<UpdateCollectionInput>();
@@ -160,7 +168,7 @@ export default function Page() {
 	}, [transition.state]);
 
 	return (
-		<Container>
+		<Container size="sm">
 			<Stack>
 				<Flex align="center" gap="md">
 					<Title>Your collections</Title>
@@ -184,52 +192,23 @@ export default function Page() {
 						<CreateOrUpdateModal toUpdateCollection={toUpdateCollection} />
 					</Modal>
 				</Flex>
-				<Tabs defaultValue="userCreated" variant="outline">
-					<Tabs.List mb="xs">
-						<Tabs.Tab
-							value="userCreated"
-							leftSection={<IconUserCog size={16} />}
-						>
-							User created
-						</Tabs.Tab>
-						<Tabs.Tab
-							value="systemCreated"
-							leftSection={<IconAssembly size={16} />}
-						>
-							System created
-						</Tabs.Tab>
-					</Tabs.List>
-					<Tabs.Panel value="userCreated">
-						{userCreatedCollections.length > 0 ? (
-							<SimpleGrid cols={{ base: 1, md: 2 }}>
-								{userCreatedCollections.map((c) => (
-									<DisplayCollection
-										key={c.id}
-										collection={c}
-										setToUpdateCollection={setToUpdateCollection}
-										openModal={createOrUpdateModalOpen}
-									/>
-								))}
-							</SimpleGrid>
-						) : (
-							<Text>You have not created any collections yet</Text>
-						)}
-					</Tabs.Panel>
-					<Tabs.Panel value="systemCreated">
-						<SimpleGrid cols={{ base: 1, md: 2 }}>
-							{collections
-								.filter((c) => c.isDefault)
-								.map((c) => (
-									<DisplayCollection
-										key={c.id}
-										collection={c}
-										setToUpdateCollection={setToUpdateCollection}
-										openModal={createOrUpdateModalOpen}
-									/>
-								))}
-						</SimpleGrid>
-					</Tabs.Panel>
-				</Tabs>
+				<DebouncedSearchInput initialValue={query} />
+				<Virtuoso
+					style={{ height: "80vh" }}
+					data={filteredCollections}
+					itemContent={(index) => {
+						const c = filteredCollections[index];
+						return (
+							<DisplayCollection
+								key={c.id}
+								index={index}
+								collection={c}
+								setToUpdateCollection={setToUpdateCollection}
+								openModal={createOrUpdateModalOpen}
+							/>
+						);
+					}}
+				/>
 			</Stack>
 		</Container>
 	);
@@ -237,14 +216,18 @@ export default function Page() {
 
 type Collection = UserCollectionsListQuery["userCollectionsList"][number];
 
+const IMAGES_CONTAINER_WIDTH = 250;
+
 const DisplayCollection = (props: {
 	collection: Collection;
+	index: number;
 	setToUpdateCollection: (c: UpdateCollectionInput) => void;
 	openModal: () => void;
 }) => {
 	const userDetails = useUserDetails();
 	const fetcher = useFetcher<typeof action>();
 	const deleteFormRef = useRef<HTMLFormElement>(null);
+	const fallbackImageUrl = useFallbackImageUrl(props.collection.name);
 	const additionalDisplay = [];
 
 	if (props.collection.creator.id !== userDetails.id)
@@ -257,73 +240,102 @@ const DisplayCollection = (props: {
 		);
 
 	return (
-		<Flex align="center" justify="space-between" gap="md" mr="lg">
-			<Box>
-				<Flex align="center" gap="xs">
-					<Anchor
-						component={Link}
-						to={$path("/collections/:id", { id: props.collection.id })}
-					>
-						<Title order={4}>
-							{truncate(props.collection.name, { length: 20 })}
-						</Title>
-					</Anchor>
-					{additionalDisplay.length > 0 ? (
-						<Text c="dimmed" size="xs">
-							({additionalDisplay.join(", ")})
-						</Text>
-					) : null}
+		<Paper
+			pr="md"
+			radius="lg"
+			withBorder
+			mt={props.index !== 0 ? "lg" : undefined}
+			pl={{ base: "md", md: 0 }}
+			py={{ base: "sm", md: 0 }}
+			style={{ overflow: "hidden" }}
+		>
+			<Flex gap="xs" direction={{ base: "column", md: "row" }}>
+				<Flex h={180} w={{ md: IMAGES_CONTAINER_WIDTH }} pos="relative">
+					<Image
+						src={fallbackImageUrl}
+						h="100%"
+						flex="none"
+						mx="auto"
+						radius="md"
+					/>
+					<Box pos="absolute" left={0} right={0} bottom={0}>
+						<ProRequiredAlert tooltipLabel="Collage image using collection contents" />
+					</Box>
 				</Flex>
-				{props.collection.description ? (
-					<Text lineClamp={1}>{props.collection.description}</Text>
-				) : null}
-			</Box>
-			<Flex gap="sm" style={{ flex: 0 }}>
-				{userDetails.id === props.collection.creator.id ? (
-					<ActionIcon
-						color="blue"
-						variant="outline"
-						onClick={() => {
-							props.setToUpdateCollection({
-								name: props.collection.name,
-								id: props.collection.id,
-								description: props.collection.description,
-								isDefault: props.collection.isDefault,
-							});
-							props.openModal();
-						}}
-					>
-						<IconEdit size={18} />
-					</ActionIcon>
-				) : null}
-				{!props.collection.isDefault ? (
-					<fetcher.Form
-						method="POST"
-						ref={deleteFormRef}
-						action={withQuery("", { intent: "delete" })}
-					>
-						<input
-							hidden
-							name="collectionName"
-							defaultValue={props.collection.name}
-						/>
-						<ActionIcon
-							color="red"
-							variant="outline"
-							onClick={async () => {
-								const conf = await confirmWrapper({
-									confirmation:
-										"Are you sure you want to delete this collection?",
-								});
-								if (conf) fetcher.submit(deleteFormRef.current);
-							}}
+				<Stack flex={1} py={{ md: "sm" }}>
+					<Group justify="space-between">
+						<Anchor
+							component={Link}
+							to={$path("/collections/:id", { id: props.collection.id })}
 						>
-							<IconTrashFilled size={18} />
-						</ActionIcon>
-					</fetcher.Form>
-				) : null}
+							<Title order={4}>
+								{truncate(props.collection.name, { length: 20 })}
+							</Title>
+						</Anchor>
+						<Group gap="md">
+							{additionalDisplay.length > 0 ? (
+								<Text c="dimmed" size="xs">
+									({additionalDisplay.join(", ")})
+								</Text>
+							) : null}
+							{userDetails.id === props.collection.creator.id ? (
+								<ActionIcon
+									color="blue"
+									variant="outline"
+									onClick={() => {
+										props.setToUpdateCollection({
+											name: props.collection.name,
+											id: props.collection.id,
+											description: props.collection.description,
+											isDefault: props.collection.isDefault,
+										});
+										props.openModal();
+									}}
+								>
+									<IconEdit size={18} />
+								</ActionIcon>
+							) : null}
+							{!props.collection.isDefault ? (
+								<fetcher.Form
+									method="POST"
+									ref={deleteFormRef}
+									action={withQuery("", { intent: "delete" })}
+								>
+									<input
+										hidden
+										name="collectionName"
+										defaultValue={props.collection.name}
+									/>
+									<ActionIcon
+										color="red"
+										variant="outline"
+										onClick={async () => {
+											const conf = await confirmWrapper({
+												confirmation:
+													"Are you sure you want to delete this collection?",
+											});
+											if (conf) fetcher.submit(deleteFormRef.current);
+										}}
+									>
+										<IconTrashFilled size={18} />
+									</ActionIcon>
+								</fetcher.Form>
+							) : null}
+						</Group>
+					</Group>
+					{props.collection.description ? (
+						<Text size="xs" lineClamp={5}>
+							{props.collection.description}
+						</Text>
+					) : null}
+					{props.collection.isDefault ? (
+						<Text lineClamp={1} mt="auto" ta="right" c="dimmed" size="xs">
+							System created
+						</Text>
+					) : null}
+				</Stack>
 			</Flex>
-		</Flex>
+		</Paper>
 	);
 };
 
@@ -348,6 +360,11 @@ const CreateOrUpdateModal = (props: {
 						props.toUpdateCollection ? props.toUpdateCollection.name : undefined
 					}
 					readOnly={props.toUpdateCollection?.isDefault}
+					description={
+						props.toUpdateCollection?.isDefault
+							? "Can not edit a default collection"
+							: undefined
+					}
 				/>
 				<Textarea
 					label="Description"
@@ -359,6 +376,14 @@ const CreateOrUpdateModal = (props: {
 					}
 					autosize
 				/>
+				<Input.Wrapper
+					label="Collaborators"
+					description={<ProRequiredAlert />}
+				/>
+				<Input.Wrapper
+					label="Information template"
+					description={<ProRequiredAlert />}
+				/>
 				<Button
 					variant="outline"
 					type="submit"
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
index 64e14b2ba7..48568dde53 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
@@ -75,7 +75,7 @@ export const loader = unstable_defineLoader(async ({ params, request }) => {
 		serverGqlService.request(ExerciseDetailsDocument, { exerciseId }),
 		serverGqlService.request(
 			UserExerciseDetailsDocument,
-			{ input: { exerciseId } },
+			{ exerciseId },
 			getAuthorizationHeader(request),
 		),
 	]);
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
index b5a99fdc55..954775679c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
@@ -56,7 +56,7 @@ import {
 	IconZzz,
 } from "@tabler/icons-react";
 import { useQuery } from "@tanstack/react-query";
-import type { ReactNode } from "react";
+import { type ReactNode, useState } from "react";
 import { namedAction } from "remix-utils/named-action";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
@@ -160,6 +160,7 @@ export default function Page() {
 		adjustTimeModalOpened,
 		{ open: adjustTimeModalOpen, close: adjustTimeModalClose },
 	] = useDisclosure(false);
+	const [isWorkoutLoading, setIsWorkoutLoading] = useState(false);
 	const startWorkout = getWorkoutStarter();
 
 	return (
@@ -207,17 +208,19 @@ export default function Page() {
 						<Title>{loaderData.workoutDetails.name}</Title>
 						<Menu shadow="md" position="bottom-end">
 							<Menu.Target>
-								<ActionIcon>
+								<ActionIcon variant="transparent" loading={isWorkoutLoading}>
 									<IconDotsVertical />
 								</ActionIcon>
 							</Menu.Target>
 							<Menu.Dropdown>
 								<Menu.Item
 									onClick={async () => {
+										setIsWorkoutLoading(true);
 										const workout = await duplicateOldWorkout(
 											loaderData.workoutDetails,
 										);
 										startWorkout(workout);
+										setIsWorkoutLoading(false);
 									}}
 									leftSection={<IconRepeat size={14} />}
 								>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
index 281a540a05..7694a70bd0 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
@@ -97,7 +97,13 @@ import { withQuery } from "ufo";
 import { confirmWrapper } from "~/components/confirmation";
 import { DisplayExerciseStats } from "~/components/fitness";
 import events from "~/lib/events";
-import { CurrentWorkoutKey, dayjsLib, getSetColor } from "~/lib/generals";
+import {
+	CurrentWorkoutKey,
+	dayjsLib,
+	getSetColor,
+	queryClient,
+	queryFactory,
+} from "~/lib/generals";
 import { useUserPreferences } from "~/lib/hooks";
 import {
 	type InProgressWorkout,
@@ -285,7 +291,7 @@ export default function Page() {
 										value={`${
 											currentWorkout.exercises
 												.map((e) => e.sets.every((s) => s.confirmedAt))
-												.filter((e) => e !== undefined).length
+												.filter((e) => e !== null).length
 										}/${currentWorkout.exercises.length}`}
 									/>
 									<StatDisplay
@@ -373,6 +379,14 @@ export default function Page() {
 															currentWorkoutToCreateWorkoutInput(
 																currentWorkout,
 															);
+														for (const exercise of currentWorkout.exercises) {
+															queryClient.removeQueries({
+																queryKey:
+																	queryFactory.fitness.userExerciseDetails(
+																		exercise.exerciseId,
+																	).queryKey,
+															});
+														}
 														stopTimer();
 														interval.stop();
 														Cookies.remove(workoutCookieName);
@@ -409,7 +423,7 @@ export default function Page() {
 													for (const asset of assets)
 														deleteUploadedAsset(asset.key);
 												}
-												navigate($path("/"));
+												navigate(-1);
 												Cookies.remove(workoutCookieName);
 												setCurrentWorkout(RESET);
 											}
@@ -1063,6 +1077,7 @@ const ExerciseDisplay = (props: {
 									draft.exercises[props.exerciseIdx].sets.push({
 										statistic: currentSet?.statistic ?? {},
 										lot: SetLot.Normal,
+										confirmedAt: null,
 									});
 								}),
 							);
diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
index 9fabc931ab..7360e946f7 100644
--- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
@@ -92,7 +92,13 @@ import {
 	ToggleMediaMonitorMenuItem,
 } from "~/components/media";
 import events from "~/lib/events";
-import { Verb, dayjsLib, getVerb, queryClient } from "~/lib/generals";
+import {
+	Verb,
+	dayjsLib,
+	getVerb,
+	queryClient,
+	queryFactory,
+} from "~/lib/generals";
 import { useGetMantineColor, useUserPreferences } from "~/lib/hooks";
 import {
 	useAddEntityToCollection,
@@ -880,10 +886,9 @@ export default function Page() {
 												})}
 												onSubmit={async () => {
 													await queryClient.invalidateQueries({
-														queryKey: [
-															"metadataDetails",
+														queryKey: queryFactory.media.metadataDetails(
 															loaderData.metadataId,
-														],
+														).queryKey,
 													});
 												}}
 											>
@@ -1361,7 +1366,11 @@ const SeenItem = (props: { history: History; index: number }) => {
 							</Text>
 						) : null}
 					</Flex>
-					<SimpleGrid cols={{ base: 1, md: 2 }} spacing={{ base: 2 }}>
+					<SimpleGrid
+						spacing="md"
+						verticalSpacing={2}
+						cols={{ base: 1, md: 2 }}
+					>
 						<Flex gap="xs">
 							<Text size="sm">Started:</Text>
 							<Text size="sm" fw="bold">
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index a5b9374683..59527ca0a5 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -617,13 +617,13 @@ const EditDashboardElement = (props: {
 	);
 };
 
-function reorder<T>(
+const reorder = <T,>(
 	array: Array<T>,
 	{ from, to }: { from: number; to: number },
-) {
+) => {
 	const cloned = [...array];
 	const item = array[from];
 	cloned.splice(from, 1);
 	cloned.splice(to, 0, item);
 	return cloned;
-}
+};
diff --git a/apps/frontend/app/routes/_dashboard.tsx b/apps/frontend/app/routes/_dashboard.tsx
index c4276007f5..6b38a6a5c2 100644
--- a/apps/frontend/app/routes/_dashboard.tsx
+++ b/apps/frontend/app/routes/_dashboard.tsx
@@ -1,6 +1,8 @@
 import { useAutoAnimate } from "@formkit/auto-animate/react";
 import { $path } from "@ignisda/remix-routes";
 import {
+	ActionIcon,
+	Affix,
 	Alert,
 	Anchor,
 	AppShell,
@@ -29,13 +31,27 @@ import {
 	ThemeIcon,
 	Title,
 	UnstyledButton,
+	rem,
 	useDirection,
 	useMantineTheme,
 } from "@mantine/core";
 import { DateInput, DatePickerInput, DateTimePicker } from "@mantine/dates";
-import { upperFirst, useDisclosure, useLocalStorage } from "@mantine/hooks";
+import {
+	upperFirst,
+	useCounter,
+	useDisclosure,
+	useLocalStorage,
+} from "@mantine/hooks";
 import { unstable_defineLoader } from "@remix-run/node";
-import { Form, Link, NavLink, Outlet, useLoaderData } from "@remix-run/react";
+import {
+	Form,
+	Link,
+	NavLink,
+	Outlet,
+	useLoaderData,
+	useLocation,
+	useNavigate,
+} from "@remix-run/react";
 import {
 	CollectionExtraInformationLot,
 	type CoreDetails,
@@ -73,6 +89,7 @@ import {
 	IconStretching,
 	IconSun,
 } from "@tabler/icons-react";
+import { parse } from "cookie";
 import { produce } from "immer";
 import { useState } from "react";
 import { Fragment } from "react/jsx-runtime";
@@ -82,11 +99,13 @@ import { HiddenLocationInput } from "~/components/common";
 import events from "~/lib/events";
 import {
 	CORE_DETAILS_COOKIE_NAME,
+	CurrentWorkoutKey,
 	LOGO_IMAGE_URL,
 	Verb,
 	getLot,
 	getVerb,
 	queryClient,
+	queryFactory,
 } from "~/lib/generals";
 import {
 	useMetadataDetails,
@@ -209,6 +228,9 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		!envData.DISABLE_TELEMETRY &&
 		!userDetails.isDemo;
 
+	const cookies = request.headers.get("cookie");
+	const workoutInProgress = parse(cookies || "")[CurrentWorkoutKey] === "true";
+
 	return {
 		envData,
 		mediaLinks,
@@ -217,6 +239,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 		fitnessLinks,
 		settingsLinks,
 		userPreferences,
+		workoutInProgress,
 		shouldHaveUmami,
 		userCollections,
 		currentColorScheme,
@@ -245,6 +268,8 @@ export default function Layout() {
 		getInitialValueInEffect: true,
 	});
 	const theme = useMantineTheme();
+	const navigate = useNavigate();
+	const location = useLocation();
 	const [opened, { toggle }] = useDisclosure(false);
 	const Icon = loaderData.currentColorScheme === "dark" ? IconSun : IconMoon;
 	const [metadataToUpdate, setMetadataToUpdate] = useMetadataProgressUpdate();
@@ -258,6 +283,20 @@ export default function Layout() {
 
 	return (
 		<>
+			{loaderData.workoutInProgress &&
+			location.pathname !== $path("/fitness/workouts/current") ? (
+				<Affix position={{ bottom: rem(30), right: rem(30) }}>
+					<ActionIcon
+						variant="filled"
+						color="orange"
+						radius="xl"
+						size="xl"
+						onClick={() => navigate($path("/fitness/workouts/current"))}
+					>
+						<IconStretching size={32} />
+					</ActionIcon>
+				</Affix>
+			) : null}
 			<Modal
 				onClose={closeMetadataProgressUpdateModal}
 				opened={metadataToUpdate !== null}
@@ -498,7 +537,7 @@ interface LinksGroupProps {
 	links?: Array<{ label: string; link: string }>;
 }
 
-function LinksGroup({
+const LinksGroup = ({
 	icon: Icon,
 	label,
 	href,
@@ -506,7 +545,7 @@ function LinksGroup({
 	toggle,
 	opened,
 	links,
-}: LinksGroupProps) {
+}: LinksGroupProps) => {
 	const { dir } = useDirection();
 	const hasLinks = Array.isArray(links);
 	const ChevronIcon = dir === "ltr" ? IconChevronRight : IconChevronLeft;
@@ -562,7 +601,7 @@ function LinksGroup({
 			{hasLinks ? <Collapse in={opened}>{items}</Collapse> : null}
 		</>
 	);
-}
+};
 
 const Footer = () => {
 	const loaderData = useLoaderData<typeof loader>();
@@ -624,7 +663,9 @@ const MetadataProgressUpdateForm = ({
 
 	const onSubmit = () => {
 		queryClient.removeQueries({
-			queryKey: ["userMetadataDetails", metadataToUpdate.metadataId],
+			queryKey: queryFactory.media.userMetadataDetails(
+				metadataToUpdate.metadataId,
+			).queryKey,
 		});
 		events.updateProgress(metadataDetails.title);
 		closeMetadataProgressUpdateModal();
@@ -1240,6 +1281,7 @@ const AddEntityToCollectionForm = ({
 		useState<Collection | null>(null);
 	const [ownedOn, setOwnedOn] = useState<Date | null>();
 	const [addEntityToCollectionData, _] = useAddEntityToCollection();
+	const [numArrayElements, setNumArrayElements] = useCounter(1);
 
 	if (!addEntityToCollectionData) return null;
 
@@ -1362,6 +1404,47 @@ const AddEntityToCollectionForm = ({
 											required={!!template.required}
 										/>
 									))
+									.with(CollectionExtraInformationLot.StringArray, () => (
+										<Input.Wrapper
+											label={template.name}
+											description={
+												<>
+													{template.description}
+													<Anchor
+														ml={4}
+														size="xs"
+														onClick={() => setNumArrayElements.increment()}
+													>
+														Add more
+													</Anchor>
+												</>
+											}
+											required={!!template.required}
+										>
+											<Stack gap="xs" mt={4}>
+												{Array.from({ length: numArrayElements }).map(
+													(_, i) => (
+														<Group key={i.toString()}>
+															<TextInput
+																name={`information.${template.name}[${i}]`}
+																flex={1}
+																defaultValue={
+																	template.defaultValue || undefined
+																}
+															/>
+															<Anchor
+																ml="auto"
+																size="xs"
+																onClick={() => setNumArrayElements.decrement()}
+															>
+																Remove
+															</Anchor>
+														</Group>
+													),
+												)}
+											</Stack>
+										</Input.Wrapper>
+									))
 									.exhaustive()}
 							</Fragment>
 						))}
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index bd9b6f1204..6ec61c28a5 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -12,13 +12,14 @@
 		"@formkit/auto-animate": "0.8.2",
 		"@hello-pangea/dnd": "16.6.0",
 		"@ignisda/remix-routes": "3.3.0",
-		"@mantine/carousel": "7.11.0",
-		"@mantine/charts": "7.11.0",
-		"@mantine/core": "7.11.0",
-		"@mantine/dates": "7.11.0",
-		"@mantine/form": "7.11.0",
-		"@mantine/hooks": "7.11.0",
-		"@mantine/notifications": "7.11.0",
+		"@lukemorales/query-key-factory": "1.3.4",
+		"@mantine/carousel": "7.11.1",
+		"@mantine/charts": "7.11.1",
+		"@mantine/core": "7.11.1",
+		"@mantine/dates": "7.11.1",
+		"@mantine/form": "7.11.1",
+		"@mantine/hooks": "7.11.1",
+		"@mantine/notifications": "7.11.1",
 		"@remix-run/node": "2.10.0",
 		"@remix-run/react": "2.10.0",
 		"@remix-run/serve": "2.10.0",
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index 82c8c1e813..33bc43df24 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -35,7 +35,7 @@ const documents = {
     "query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    sourceUrl\n    details {\n      id\n      name\n      source\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n    }\n    contents {\n      name\n      items {\n        character\n        media {\n          id\n          title\n          image\n        }\n      }\n    }\n  }\n}": types.PersonDetailsDocument,
     "query PeopleList($input: PeopleListInput!) {\n  peopleList(input: $input) {\n    details {\n      total\n      nextPage\n    }\n    items {\n      id\n      name\n      image\n      mediaCount\n    }\n  }\n}": types.PeopleListDocument,
     "query UserDetails {\n  userDetails {\n    __typename\n    ... on User {\n      id\n      lot\n      name\n      oidcIssuerId\n      isDemo\n    }\n  }\n}": types.UserDetailsDocument,
-    "query UserExerciseDetails($input: UserExerciseDetailsInput!) {\n  userExerciseDetails(input: $input) {\n    collections {\n      ...CollectionPart\n    }\n    history {\n      workoutId\n      workoutName\n      workoutTime\n      index\n      sets {\n        lot\n        statistic {\n          ...WorkoutSetStatisticPart\n        }\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            workoutDoneOn\n            exerciseIdx\n            setIdx\n            data {\n              statistic {\n                ...WorkoutSetStatisticPart\n              }\n              lot\n            }\n          }\n        }\n      }\n    }\n  }\n}": types.UserExerciseDetailsDocument,
+    "query UserExerciseDetails($exerciseId: String!) {\n  userExerciseDetails(exerciseId: $exerciseId) {\n    collections {\n      ...CollectionPart\n    }\n    history {\n      workoutId\n      workoutName\n      workoutTime\n      index\n      sets {\n        lot\n        statistic {\n          ...WorkoutSetStatisticPart\n        }\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            workoutDoneOn\n            exerciseIdx\n            setIdx\n            data {\n              statistic {\n                ...WorkoutSetStatisticPart\n              }\n              lot\n            }\n          }\n        }\n      }\n    }\n  }\n}": types.UserExerciseDetailsDocument,
     "query UserMeasurementsList($input: UserMeasurementsListInput!) {\n  userMeasurementsList(input: $input) {\n    timestamp\n    name\n    comment\n    stats {\n      weight\n      bodyMassIndex\n      totalBodyWater\n      muscle\n      leanBodyMass\n      bodyFat\n      boneMass\n      visceralFat\n      waistCircumference\n      waistToHeightRatio\n      hipCircumference\n      waistToHipRatio\n      chestCircumference\n      thighCircumference\n      bicepsCircumference\n      neckCircumference\n      bodyFatCaliper\n      chestSkinfold\n      abdominalSkinfold\n      thighSkinfold\n      basalMetabolicRate\n      totalDailyEnergyExpenditure\n      calories\n      custom\n    }\n  }\n}": types.UserMeasurementsListDocument,
     "query UserMetadataDetails($metadataId: String!) {\n  userMetadataDetails(metadataId: $metadataId) {\n    collections {\n      ...CollectionPart\n    }\n    inProgress {\n      ...SeenPart\n    }\n    history {\n      ...SeenPart\n    }\n    averageRating\n    unitsConsumed\n    reviews {\n      ...ReviewItemPart\n    }\n    seenByAllCount\n    seenByUserCount\n    nextEntry {\n      season\n      episode\n      chapter\n    }\n    showProgress {\n      timesSeen\n      seasonNumber\n      episodes {\n        episodeNumber\n        timesSeen\n      }\n    }\n    podcastProgress {\n      episodeNumber\n      timesSeen\n    }\n  }\n}": types.UserMetadataDetailsDocument,
     "query UserMetadataGroupDetails($metadataGroupId: String!) {\n  userMetadataGroupDetails(metadataGroupId: $metadataGroupId) {\n    reviews {\n      ...ReviewItemPart\n    }\n    collections {\n      ...CollectionPart\n    }\n  }\n}": types.UserMetadataGroupDetailsDocument,
@@ -152,7 +152,7 @@ export function graphql(source: "query UserDetails {\n  userDetails {\n    __typ
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query UserExerciseDetails($input: UserExerciseDetailsInput!) {\n  userExerciseDetails(input: $input) {\n    collections {\n      ...CollectionPart\n    }\n    history {\n      workoutId\n      workoutName\n      workoutTime\n      index\n      sets {\n        lot\n        statistic {\n          ...WorkoutSetStatisticPart\n        }\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            workoutDoneOn\n            exerciseIdx\n            setIdx\n            data {\n              statistic {\n                ...WorkoutSetStatisticPart\n              }\n              lot\n            }\n          }\n        }\n      }\n    }\n  }\n}"): (typeof documents)["query UserExerciseDetails($input: UserExerciseDetailsInput!) {\n  userExerciseDetails(input: $input) {\n    collections {\n      ...CollectionPart\n    }\n    history {\n      workoutId\n      workoutName\n      workoutTime\n      index\n      sets {\n        lot\n        statistic {\n          ...WorkoutSetStatisticPart\n        }\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            workoutDoneOn\n            exerciseIdx\n            setIdx\n            data {\n              statistic {\n                ...WorkoutSetStatisticPart\n              }\n              lot\n            }\n          }\n        }\n      }\n    }\n  }\n}"];
+export function graphql(source: "query UserExerciseDetails($exerciseId: String!) {\n  userExerciseDetails(exerciseId: $exerciseId) {\n    collections {\n      ...CollectionPart\n    }\n    history {\n      workoutId\n      workoutName\n      workoutTime\n      index\n      sets {\n        lot\n        statistic {\n          ...WorkoutSetStatisticPart\n        }\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            workoutDoneOn\n            exerciseIdx\n            setIdx\n            data {\n              statistic {\n                ...WorkoutSetStatisticPart\n              }\n              lot\n            }\n          }\n        }\n      }\n    }\n  }\n}"): (typeof documents)["query UserExerciseDetails($exerciseId: String!) {\n  userExerciseDetails(exerciseId: $exerciseId) {\n    collections {\n      ...CollectionPart\n    }\n    history {\n      workoutId\n      workoutName\n      workoutTime\n      index\n      sets {\n        lot\n        statistic {\n          ...WorkoutSetStatisticPart\n        }\n      }\n    }\n    details {\n      exerciseId\n      createdOn\n      lastUpdatedOn\n      exerciseNumTimesInteracted\n      exerciseExtraInformation {\n        lifetimeStats {\n          weight\n          reps\n          distance\n          duration\n          personalBestsAchieved\n        }\n        personalBests {\n          lot\n          sets {\n            workoutId\n            workoutDoneOn\n            exerciseIdx\n            setIdx\n            data {\n              statistic {\n                ...WorkoutSetStatisticPart\n              }\n              lot\n            }\n          }\n        }\n      }\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index 6994afbdb1..e39a72a084 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -21,6 +21,8 @@ export type Scalars = {
    */
   DateTime: { input: string; output: string; }
   Decimal: { input: string; output: string; }
+  /** A scalar that can represent any JSON value. */
+  JSON: { input: any; output: any; }
   /** A scalar that can represent any JSON Object value. */
   JSONObject: { input: any; output: any; }
   /**
@@ -93,7 +95,7 @@ export type ChangeCollectionToEntityInput = {
   collectionName: Scalars['String']['input'];
   creatorUserId: Scalars['String']['input'];
   exerciseId?: InputMaybe<Scalars['String']['input']>;
-  information?: InputMaybe<Scalars['JSONObject']['input']>;
+  information?: InputMaybe<Scalars['JSON']['input']>;
   metadataGroupId?: InputMaybe<Scalars['String']['input']>;
   metadataId?: InputMaybe<Scalars['String']['input']>;
   personId?: InputMaybe<Scalars['String']['input']>;
@@ -152,7 +154,8 @@ export enum CollectionExtraInformationLot {
   Date = 'DATE',
   DateTime = 'DATE_TIME',
   Number = 'NUMBER',
-  String = 'STRING'
+  String = 'STRING',
+  StringArray = 'STRING_ARRAY'
 }
 
 export type CollectionItem = {
@@ -1650,7 +1653,7 @@ export type QueryRootUserCollectionsListArgs = {
 
 
 export type QueryRootUserExerciseDetailsArgs = {
-  input: UserExerciseDetailsInput;
+  exerciseId: Scalars['String']['input'];
 };
 
 
@@ -1917,12 +1920,6 @@ export type UserExerciseDetails = {
   history?: Maybe<Array<UserExerciseHistoryInformation>>;
 };
 
-export type UserExerciseDetailsInput = {
-  exerciseId: Scalars['String']['input'];
-  /** The number of elements to return in the history. */
-  takeHistory?: InputMaybe<Scalars['Int']['input']>;
-};
-
 export type UserExerciseHistoryInformation = {
   index: Scalars['Int']['output'];
   sets: Array<WorkoutSetRecord>;
@@ -2832,7 +2829,7 @@ export type UserDetailsQueryVariables = Exact<{ [key: string]: never; }>;
 export type UserDetailsQuery = { userDetails: { __typename: 'User', id: string, lot: UserLot, name: string, oidcIssuerId?: string | null, isDemo?: boolean | null } | { __typename: 'UserDetailsError' } };
 
 export type UserExerciseDetailsQueryVariables = Exact<{
-  input: UserExerciseDetailsInput;
+  exerciseId: Scalars['String']['input'];
 }>;
 
 
@@ -3058,7 +3055,7 @@ export const PeopleSearchDocument = {"kind":"Document","definitions":[{"kind":"O
 export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"birthDate"}},{"kind":"Field","name":{"kind":"Name","value":"deathDate"}},{"kind":"Field","name":{"kind":"Name","value":"place"}},{"kind":"Field","name":{"kind":"Name","value":"website"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"character"}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<PersonDetailsQuery, PersonDetailsQueryVariables>;
 export const PeopleListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PeopleList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PeopleListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"peopleList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"mediaCount"}}]}}]}}]}}]} as unknown as DocumentNode<PeopleListQuery, PeopleListQueryVariables>;
 export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}},{"kind":"Field","name":{"kind":"Name","value":"isDemo"}}]}}]}}]}}]} as unknown as DocumentNode<UserDetailsQuery, UserDetailsQueryVariables>;
-export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserExerciseDetailsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutName"}},{"kind":"Field","name":{"kind":"Name","value":"workoutTime"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDoneOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}},{"kind":"Field","name":{"kind":"Name","value":"setIdx"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}}]}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]} as unknown as DocumentNode<UserExerciseDetailsQuery, UserExerciseDetailsQueryVariables>;
+export const UserExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userExerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutName"}},{"kind":"Field","name":{"kind":"Name","value":"workoutTime"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseId"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseNumTimesInteracted"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lifetimeStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"personalBestsAchieved"}}]}},{"kind":"Field","name":{"kind":"Name","value":"personalBests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"sets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutId"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDoneOn"}},{"kind":"Field","name":{"kind":"Name","value":"exerciseIdx"}},{"kind":"Field","name":{"kind":"Name","value":"setIdx"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"statistic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkoutSetStatisticPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lot"}}]}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkoutSetStatisticPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkoutSetStatistic"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"distance"}},{"kind":"Field","name":{"kind":"Name","value":"reps"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"oneRm"}},{"kind":"Field","name":{"kind":"Name","value":"pace"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}}]} as unknown as DocumentNode<UserExerciseDetailsQuery, UserExerciseDetailsQueryVariables>;
 export const UserMeasurementsListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMeasurementsList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserMeasurementsListInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMeasurementsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}},{"kind":"Field","name":{"kind":"Name","value":"custom"}}]}}]}}]}}]} as unknown as DocumentNode<UserMeasurementsListQuery, UserMeasurementsListQueryVariables>;
 export const UserMetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"averageRating"}},{"kind":"Field","name":{"kind":"Name","value":"unitsConsumed"}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"seenByAllCount"}},{"kind":"Field","name":{"kind":"Name","value":"seenByUserCount"}},{"kind":"Field","name":{"kind":"Name","value":"nextEntry"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"chapter"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastProgress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timesSeen"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Seen"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"providerWatchedOn"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"startedOn"}},{"kind":"Field","name":{"kind":"Name","value":"finishedOn"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedOn"}},{"kind":"Field","name":{"kind":"Name","value":"totalTimeSpent"}},{"kind":"Field","name":{"kind":"Name","value":"numTimesUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}}]} as unknown as DocumentNode<UserMetadataDetailsQuery, UserMetadataDetailsQueryVariables>;
 export const UserMetadataGroupDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserMetadataGroupDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userMetadataGroupDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataGroupId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataGroupId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"collections"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"CollectionPart"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CollectionPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Collection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}}]}}]} as unknown as DocumentNode<UserMetadataGroupDetailsQuery, UserMetadataGroupDetailsQueryVariables>;
diff --git a/libs/graphql/src/backend/queries/UserExerciseDetails.gql b/libs/graphql/src/backend/queries/UserExerciseDetails.gql
index e29b83b4f1..509abc3eea 100644
--- a/libs/graphql/src/backend/queries/UserExerciseDetails.gql
+++ b/libs/graphql/src/backend/queries/UserExerciseDetails.gql
@@ -1,5 +1,5 @@
-query UserExerciseDetails($input: UserExerciseDetailsInput!) {
-  userExerciseDetails(input: $input) {
+query UserExerciseDetails($exerciseId: String!) {
+  userExerciseDetails(exerciseId: $exerciseId) {
     collections {
       ...CollectionPart
     }
diff --git a/yarn.lock b/yarn.lock
index 765b5faa0e..4c78141b6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3559,35 +3559,45 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@mantine/carousel@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/carousel@npm:7.11.0"
+"@lukemorales/query-key-factory@npm:1.3.4":
+  version: 1.3.4
+  resolution: "@lukemorales/query-key-factory@npm:1.3.4"
   peerDependencies:
-    "@mantine/core": 7.11.0
-    "@mantine/hooks": 7.11.0
+    "@tanstack/query-core": ">= 4.0.0"
+    "@tanstack/react-query": ">= 4.0.0"
+  checksum: 10/8954bbce9c5fc7103ccb8bc546eee746896af47a1453cf9a2413c0accae296727db60329a3b75a8f2ef15c516cfadb4eb17530242274f20bfd129bc85f799149
+  languageName: node
+  linkType: hard
+
+"@mantine/carousel@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/carousel@npm:7.11.1"
+  peerDependencies:
+    "@mantine/core": 7.11.1
+    "@mantine/hooks": 7.11.1
     embla-carousel-react: ">=7.0.0"
     react: ^18.2.0
     react-dom: ^18.2.0
-  checksum: 10/fb3df66d32d2664f3c9f52020b099a6491e2bc966ff296fe754f54621b9f3800e88735edbbe78a6635c43a2ebe097ff5cb24aaf3236c8942546e36ff80bb8950
+  checksum: 10/ae31b4b8e7c62b89e0a58fbd39aaa1c329f43e469b3dd6b30ae0163b9aebe602aa55787b9d164a7d79c6306fb832f2f9117a9b6ef29a7e5d1dc614718510802b
   languageName: node
   linkType: hard
 
-"@mantine/charts@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/charts@npm:7.11.0"
+"@mantine/charts@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/charts@npm:7.11.1"
   peerDependencies:
-    "@mantine/core": 7.11.0
-    "@mantine/hooks": 7.11.0
+    "@mantine/core": 7.11.1
+    "@mantine/hooks": 7.11.1
     react: ^18.2.0
     react-dom: ^18.2.0
     recharts: ^2.10.3
-  checksum: 10/e89ee25bab803f864754af42f971892a4a266e76bba930b4912854e9b1ffbfb076b4dcb7330ed816ebb074d6f9415184ecdb6f91c38ef2b43c880ceb5b9edffd
+  checksum: 10/b9b81d7061c89f5f035d3abaa9b618bcd82f76096f9c16942ca752af7d4ad71801b91ffd754d30fc9139e1825e48c1669df9b4cebeb26d29ede7095fbb475fce
   languageName: node
   linkType: hard
 
-"@mantine/core@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/core@npm:7.11.0"
+"@mantine/core@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/core@npm:7.11.1"
   dependencies:
     "@floating-ui/react": "npm:^0.26.9"
     clsx: "npm:^2.1.1"
@@ -3596,70 +3606,70 @@ __metadata:
     react-textarea-autosize: "npm:8.5.3"
     type-fest: "npm:^4.12.0"
   peerDependencies:
-    "@mantine/hooks": 7.11.0
+    "@mantine/hooks": 7.11.1
     react: ^18.2.0
     react-dom: ^18.2.0
-  checksum: 10/fa093abde60d3edc63d46f63a5abccf4969a26cb6e054f7c4d1975d84becaf4b709f1d206428643e08d669d9f3780b16c4b7670d709797bbc687d601cd8f890b
+  checksum: 10/6eec3f859d4954250ed691a6b41ef50941236339e613e831b4e6e3a30d05994ee1492f77a849c7cd0084e7160eb9239f8a03ee1d7326a06350747d288b7319bf
   languageName: node
   linkType: hard
 
-"@mantine/dates@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/dates@npm:7.11.0"
+"@mantine/dates@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/dates@npm:7.11.1"
   dependencies:
     clsx: "npm:^2.1.1"
   peerDependencies:
-    "@mantine/core": 7.11.0
-    "@mantine/hooks": 7.11.0
+    "@mantine/core": 7.11.1
+    "@mantine/hooks": 7.11.1
     dayjs: ">=1.0.0"
     react: ^18.2.0
     react-dom: ^18.2.0
-  checksum: 10/be7fe910d49f1a0ea8f76ab2e651f95ee6c30f954c243ca6cbb2b111936fa1879407155677ff7bc16c84f8753ea896ed40ce49d4aaa929b0b1642a08c3d04102
+  checksum: 10/a4a0fd032e77350bbe50ff041b1e4c9a02a07360dadb577f96e1a6e69ef524e70bdbc2324ffaf84b45929247fc163e9f56ce3462d5067ed3cc4a8d03a9a62514
   languageName: node
   linkType: hard
 
-"@mantine/form@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/form@npm:7.11.0"
+"@mantine/form@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/form@npm:7.11.1"
   dependencies:
     fast-deep-equal: "npm:^3.1.3"
     klona: "npm:^2.0.6"
   peerDependencies:
     react: ^18.2.0
-  checksum: 10/a88db9bd7883b1e4af5064dab010bef83861d89d7668d4cdb50e918f245b9f3fb7fae6ad99856911f816f6d5839088f60cc3e2424de66db9674b8a4f8b0b6bb6
+  checksum: 10/196bf3d9b5a699536b431e57fb01548d090e441546eebbe2910862c158a0646c592adcbaedeb352be136caa813b58ba2c471f25613812219ed1ffcdcad6fa5d7
   languageName: node
   linkType: hard
 
-"@mantine/hooks@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/hooks@npm:7.11.0"
+"@mantine/hooks@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/hooks@npm:7.11.1"
   peerDependencies:
     react: ^18.2.0
-  checksum: 10/276d7601bf8013bd2a2a54c385396d5ea4bfa532ef3b49bcce258a8544268656e60e1946f4db5187f824fe0ec0065940a5479953a2cc1745412096605fc38e4e
+  checksum: 10/7e26298996720d12d118bc34091334b77475d4b2db1989f24896bcb0efe87304e3fc58d85bc73cfb08ab5dd947046d133d7fc7e6aca6dc104d4ec1b1c4f206fd
   languageName: node
   linkType: hard
 
-"@mantine/notifications@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/notifications@npm:7.11.0"
+"@mantine/notifications@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/notifications@npm:7.11.1"
   dependencies:
-    "@mantine/store": "npm:7.11.0"
+    "@mantine/store": "npm:7.11.1"
     react-transition-group: "npm:4.4.5"
   peerDependencies:
-    "@mantine/core": 7.11.0
-    "@mantine/hooks": 7.11.0
+    "@mantine/core": 7.11.1
+    "@mantine/hooks": 7.11.1
     react: ^18.2.0
     react-dom: ^18.2.0
-  checksum: 10/59821ca528fed455c8a48de7a045a248712420e3eb07f95f24e33833317b84a808cc69be8d9555e9309a02be4f3edebbc9251b483a40b95d7399e79a637b887b
+  checksum: 10/72d012ad90134b655c99ae907de451d564ce0d516037c0643ed0c73d6fa7224a35d2710d5c71eda95793bbb58d75701dd19b73800ebe9ca6be204bc7593a8d2a
   languageName: node
   linkType: hard
 
-"@mantine/store@npm:7.11.0":
-  version: 7.11.0
-  resolution: "@mantine/store@npm:7.11.0"
+"@mantine/store@npm:7.11.1":
+  version: 7.11.1
+  resolution: "@mantine/store@npm:7.11.1"
   peerDependencies:
     react: ^18.2.0
-  checksum: 10/d28ffd70a8d8c21fcdc6783d9720496ed87b3134a155bd6765fea2d04c63e1067466dea0e2092a25f6274bad501f8286d545a50148746c143b8fc48e6f79c6f2
+  checksum: 10/b362d03f0335f64e136001915eda7b2f2bca5635e597de403ab5e589a2098f0a999ef57830167fc6d3e06447637efea212b2d2b5a085b7254feae5d3b7fc6838
   languageName: node
   linkType: hard
 
@@ -4191,13 +4201,14 @@ __metadata:
     "@formkit/auto-animate": "npm:0.8.2"
     "@hello-pangea/dnd": "npm:16.6.0"
     "@ignisda/remix-routes": "npm:3.3.0"
-    "@mantine/carousel": "npm:7.11.0"
-    "@mantine/charts": "npm:7.11.0"
-    "@mantine/core": "npm:7.11.0"
-    "@mantine/dates": "npm:7.11.0"
-    "@mantine/form": "npm:7.11.0"
-    "@mantine/hooks": "npm:7.11.0"
-    "@mantine/notifications": "npm:7.11.0"
+    "@lukemorales/query-key-factory": "npm:1.3.4"
+    "@mantine/carousel": "npm:7.11.1"
+    "@mantine/charts": "npm:7.11.1"
+    "@mantine/core": "npm:7.11.1"
+    "@mantine/dates": "npm:7.11.1"
+    "@mantine/form": "npm:7.11.1"
+    "@mantine/hooks": "npm:7.11.1"
+    "@mantine/notifications": "npm:7.11.1"
     "@remix-run/dev": "npm:2.10.0"
     "@remix-run/node": "npm:2.10.0"
     "@remix-run/react": "npm:2.10.0"


Test patch: