From 01f5b44b6046739ec13014ba7c40a96b17f4c324 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 18 Feb 2026 17:41:31 +0100 Subject: [PATCH] wip: Create /collections/{id} page --- front/src/app/(app)/collections/[slug].tsx | 3 + front/src/query/query.tsx | 5 +- front/src/ui/details/collection.tsx | 148 ++++----------------- front/src/ui/details/header.tsx | 7 +- front/src/ui/details/index.tsx | 1 + front/src/ui/details/part-of.tsx | 110 +++++++++++++++ 6 files changed, 146 insertions(+), 128 deletions(-) create mode 100644 front/src/app/(app)/collections/[slug].tsx create mode 100644 front/src/ui/details/part-of.tsx diff --git a/front/src/app/(app)/collections/[slug].tsx b/front/src/app/(app)/collections/[slug].tsx new file mode 100644 index 00000000..0134a106 --- /dev/null +++ b/front/src/app/(app)/collections/[slug].tsx @@ -0,0 +1,3 @@ +import { CollectionDetails } from "~/ui/details"; + +export default CollectionDetails; diff --git a/front/src/query/query.tsx b/front/src/query/query.tsx index 67deca0e..705970fd 100644 --- a/front/src/query/query.tsx +++ b/front/src/query/query.tsx @@ -159,7 +159,10 @@ const toQueryKey = (query: { ...query.path, query.params ? `?${Object.entries(query.params) - .filter(([_, v]) => v !== undefined) + .filter( + ([_, v]) => + v !== undefined && (Array.isArray(v) ? v.length > 0 : true), + ) .map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`) .join("&")}` : undefined, diff --git a/front/src/ui/details/collection.tsx b/front/src/ui/details/collection.tsx index c1e1a151..2edb2647 100644 --- a/front/src/ui/details/collection.tsx +++ b/front/src/ui/details/collection.tsx @@ -1,132 +1,30 @@ -/* - * Kyoo - A portable and vast media library solution. - * Copyright (c) Kyoo. - * - * See AUTHORS.md and LICENSE file in the project root for full license information. - * - * Kyoo is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * any later version. - * - * Kyoo is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Kyoo. If not, see . - */ +import { useState } from "react"; +import Animated from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useQueryState } from "~/utils"; +import { HeaderBackground, useScrollNavbar } from "../navbar"; +import { Header } from "./header"; -/** biome-ignore-all lint/correctness/noUnusedImports: TODO */ - -import { - type Collection, - CollectionP, - type KyooImage, - type QueryIdentifier, - useInfiniteFetch, -} from "@kyoo/models"; -import { - Container, - focusReset, - GradientImageBackground, - H2, - ImageBackground, - Link, - P, - ts, -} from "@kyoo/primitives"; -import { useTranslation } from "react-i18next"; -import { type Theme, useYoshiki } from "yoshiki/native"; - -export const PartOf = ({ - name, - overview, - thumbnail, - href, -}: { - name: string; - overview: string | null; - thumbnail: KyooImage | null; - href: string; -}) => { - const { css, theme } = useYoshiki("part-of-collection"); - const { t } = useTranslation(); +export const CollectionDetails = () => { + const [slug] = useQueryState("slug", undefined!); + const insets = useSafeAreaInsets(); + const [imageHeight, setHeight] = useState(300); + const { scrollHandler, headerProps } = useScrollNavbar({ imageHeight }); return ( - theme.background, - fover: { - self: { ...focusReset, borderColor: (theme: Theme) => theme.accent }, - title: { textDecorationLine: "underline" }, - }, - })} - > - + + -

- {t("show.partOf")} {name} -

-

{overview}

-
- - ); -}; - -export const DetailsCollections = ({ - type, - slug, -}: { - type: "movie" | "show"; - slug: string; -}) => { - const { items } = useInfiniteFetch(DetailsCollections.query(type, slug)); - const { css } = useYoshiki(); - - // Since most items dont have collections, not having a skeleton reduces layout shifts. - if (!items) return null; - - return ( - - {items.map((x) => ( - setHeight(e.nativeEvent.layout.height)} /> - ))} - + + ); }; - -DetailsCollections.query = ( - type: "movie" | "show", - slug: string, -): QueryIdentifier => ({ - parser: CollectionP, - path: [type, slug, "collections"], - params: { - limit: 0, - }, - infinite: true, -}); diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index a46fb7e0..7042fdf3 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -380,7 +380,7 @@ export const Header = ({ slug, onImageLayout, }: { - kind: "movie" | "serie"; + kind: "movie" | "serie" | "collection"; slug: string; onImageLayout?: ViewProps["onLayout"]; }) => { @@ -458,6 +458,9 @@ Header.query = ( parser: Show, path: ["api", `${kind}s`, slug], params: { - with: ["studios", ...(kind === "serie" ? ["firstEntry", "nextEntry"] : [])], + with: [ + ...(kind !== "collection" ? ["studios"] : []), + ...(kind === "serie" ? ["firstEntry", "nextEntry"] : []), + ], }, }); diff --git a/front/src/ui/details/index.tsx b/front/src/ui/details/index.tsx index d7b81206..4b165b80 100644 --- a/front/src/ui/details/index.tsx +++ b/front/src/ui/details/index.tsx @@ -1,2 +1,3 @@ +export { CollectionDetails } from "./collection"; export { MovieDetails } from "./movie"; export { SerieDetails } from "./serie"; diff --git a/front/src/ui/details/part-of.tsx b/front/src/ui/details/part-of.tsx new file mode 100644 index 00000000..864ab92e --- /dev/null +++ b/front/src/ui/details/part-of.tsx @@ -0,0 +1,110 @@ +import { + type Collection, + CollectionP, + type KyooImage, + type QueryIdentifier, + useInfiniteFetch, +} from "@kyoo/models"; +import { + Container, + focusReset, + GradientImageBackground, + H2, + ImageBackground, + Link, + P, + ts, +} from "@kyoo/primitives"; +import { useTranslation } from "react-i18next"; +import { type Theme, useYoshiki } from "yoshiki/native"; + +export const PartOf = ({ + name, + overview, + thumbnail, + href, +}: { + name: string; + overview: string | null; + thumbnail: KyooImage | null; + href: string; +}) => { + const { css, theme } = useYoshiki("part-of-collection"); + const { t } = useTranslation(); + + return ( + theme.background, + fover: { + self: { ...focusReset, borderColor: (theme: Theme) => theme.accent }, + title: { textDecorationLine: "underline" }, + }, + })} + > + +

+ {t("show.partOf")} {name} +

+

{overview}

+
+ + ); +}; + +export const DetailsCollections = ({ + type, + slug, +}: { + type: "movie" | "show"; + slug: string; +}) => { + const { items } = useInfiniteFetch(DetailsCollections.query(type, slug)); + const { css } = useYoshiki(); + + // Since most items dont have collections, not having a skeleton reduces layout shifts. + if (!items) return null; + + return ( + + {items.map((x) => ( + + ))} + + ); +}; + +DetailsCollections.query = ( + type: "movie" | "show", + slug: string, +): QueryIdentifier => ({ + parser: CollectionP, + path: [type, slug, "collections"], + params: { + limit: 0, + }, + infinite: true, +});