Skip to content

Commit

Permalink
Array items in collection information (#897)
Browse files Browse the repository at this point in the history
* feat(backend): remove useless param

* chore(frontend): adapt to gql new schema

* feat(backend): allow new input type

* fix(frontend): remove indicators for mobile carousel

* feat(frontend): put menu in loading state when repeating

* fix(frontend): better debounced input

* chore(frontend): order of imports

* feat(frontend): allow adding fields

* fix(frontend): correct name for collections

* fix(backend): allow putting anything into information

* fix(frontend): small current workout errors

* build(frontend): add query key factory deps

* feat(frontend): use query key factory for invalidation

* feat(frontend): set sane staleTimes

* feat(frontend): remove separate tabs for system collections

* build(frontend): upgrade mantine deps

* fix(frontend): do not redirect to root

* feat(frontend): new layout for collection details

* feat(frontend): use function instead of `as const`

* feat(frontend): add filter for collections

* feat(frontend): virtualize collections display

* fix(frontend): remove useless ternary condition

* feat(frontend): component for pro feature

* feat(frontend): add alerts about pro features

* feat(frontend): display collection images

* fix(frontend): set a staleTime for collections

* feat(frontend): epic animations

* fix(frontend): change transition timings

* feat(frontend): scale image when hovered

* fix(frontend): remove new image stuff

* feat(frontend): add pro label

* fix(frontend): add radius to image

* fix(frontend): change tooltip text

* fix(frontend): use correct color scheme

* refactor(frontend): change function format

* refactor(frontend): extract hook for fallback image url

* refactor(frontend): remove extra indirection

* fix(frontend): better line clamping defaults

* feat(frontend): add description to collection creation part

* fix(frontend): spacing issues for seen item

* chore(frontend): remove current workout indicator

* feat(frontend): change name of stuff

* feat(frontend): show affix when workout active
  • Loading branch information
IgnisDa authored Jul 3, 2024
1 parent c1fc326 commit 01b0b0a
Show file tree
Hide file tree
Showing 25 changed files with 500 additions and 356 deletions.
18 changes: 5 additions & 13 deletions apps/backend/src/fitness/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -382,15 +375,15 @@ 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,
&user_id,
None,
None,
None,
Some(input.exercise_id.clone()),
Some(exercise_id.clone()),
)
.await?;
let mut resp = UserExerciseDetails {
Expand All @@ -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?
{
Expand All @@ -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?;
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/miscellaneous/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub enum CollectionExtraInformationLot {
Number,
Date,
DateTime,
StringArray,
}

#[derive(
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 1 addition & 5 deletions apps/backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
PartialModelTrait, QueryFilter,
};
use serde_json::Value;

use crate::{
background::{ApplicationJob, CoreApplicationJob},
Expand Down Expand Up @@ -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 {
Expand Down
75 changes: 38 additions & 37 deletions apps/frontend/app/components/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Carousel } from "@mantine/carousel";
import "@mantine/carousel/styles.css";
import {
ActionIcon,
Alert,
Anchor,
Badge,
Box,
Expand All @@ -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: {
Expand All @@ -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">
Expand All @@ -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(
Expand All @@ -82,7 +79,7 @@ export const MediaDetailsLayout = (props: {
src={props.images[0]}
height={400}
radius="lg"
fallbackSrc={getFallbackImageUrl(colorScheme)}
fallbackSrc={fallbackImageUrl}
/>
</Box>
)}
Expand Down Expand Up @@ -141,44 +138,48 @@ 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
}
/>
);
};

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;
};
2 changes: 1 addition & 1 deletion apps/frontend/app/components/confirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 4 additions & 11 deletions apps/frontend/app/components/media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
TextInput,
ThemeIcon,
Tooltip,
useComputedColorScheme,
} from "@mantine/core";
import "@mantine/dates/styles.css";
import { useDisclosure } from "@mantine/hooks";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand Down
46 changes: 37 additions & 9 deletions apps/frontend/app/lib/generals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { MantineColorScheme } from "@mantine/core";
import {
createQueryKeys,
mergeQueryKeys,
} from "@lukemorales/query-key-factory";
import {
MediaLot,
MediaSource,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,
);
Loading

0 comments on commit 01b0b0a

Please sign in to comment.