From 608bc15e1f0a717f92f6fbfd243e0cd8a3b4aedb Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 6 Sep 2022 03:26:06 +0200 Subject: [PATCH] Add staff list on show page --- front/package.json | 1 + front/src/components/person.tsx | 55 ++++++++++++++++++++++++++++ front/src/models/resources/index.ts | 3 ++ front/src/models/resources/person.ts | 46 +++++++++++++++++++++++ front/src/pages/show/[slug].tsx | 47 ++++++++++++++++++++++-- front/src/utils/query.ts | 45 +++++++++++++++-------- front/yarn.lock | 5 +++ 7 files changed, 182 insertions(+), 20 deletions(-) create mode 100644 front/src/components/person.tsx create mode 100644 front/src/models/resources/person.ts diff --git a/front/package.json b/front/package.json index 01a55569..b986b08c 100644 --- a/front/package.json +++ b/front/package.json @@ -29,6 +29,7 @@ "next-translate": "^1.5.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-intersection-observer": "^9.4.0", "react-query": "^4.0.0-beta.23", "superjson": "^1.9.1", "zod": "^3.18.0" diff --git a/front/src/components/person.tsx b/front/src/components/person.tsx new file mode 100644 index 00000000..7c909440 --- /dev/null +++ b/front/src/components/person.tsx @@ -0,0 +1,55 @@ +/* + * 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 { Avatar, Box, Skeleton, SxProps, Typography } from "@mui/material"; +import { Person } from "~/models/resources/person"; +import { Link } from "~/utils/link"; + +export const PersonAvatar = ({ person, sx }: { person?: Person; sx?: SxProps }) => { + if (!person) { + return ( + + + + + + ) + } + return ( + + + {person.name} + {person.role && person.type && ( + + {person.type} ({person.role}) + + )} + {person.role && !person.type && ( + + {person.role} + + )} + + ); +}; diff --git a/front/src/models/resources/index.ts b/front/src/models/resources/index.ts index 38a8af5f..7d43a1e1 100644 --- a/front/src/models/resources/index.ts +++ b/front/src/models/resources/index.ts @@ -23,3 +23,6 @@ export * from "./library-item"; export * from "./show"; export * from "./movie"; export * from "./collection"; +export * from "./genre"; +export * from "./person"; +export * from "./studio"; diff --git a/front/src/models/resources/person.ts b/front/src/models/resources/person.ts new file mode 100644 index 00000000..bcef4d61 --- /dev/null +++ b/front/src/models/resources/person.ts @@ -0,0 +1,46 @@ +/* + * 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 { z } from "zod"; +import { ImagesP } from "../traits"; +import { ResourceP } from "../traits/resource"; + +export const PersonP = ResourceP.merge(ImagesP).extend({ + /** + * The name of this person. + */ + name: z.string(), + /** + * The type of work the person has done for the show. That can be something like "Actor", + * "Writer", "Music", "Voice Actor"... + */ + type: z.string().optional(), + + /** + * The role the People played. This is mostly used to inform witch character was played for actor + * and voice actors. + */ + role: z.string().optional(), +}); + +/** + * A studio that make shows. + */ +export type Person = z.infer; diff --git a/front/src/pages/show/[slug].tsx b/front/src/pages/show/[slug].tsx index e0ca6fdf..88a53822 100644 --- a/front/src/pages/show/[slug].tsx +++ b/front/src/pages/show/[slug].tsx @@ -35,8 +35,8 @@ import useTranslation from "next-translate/useTranslation"; import Head from "next/head"; import { Navbar } from "~/components/navbar"; import { Image, Poster } from "~/components/poster"; -import { Show, ShowP } from "~/models"; -import { QueryIdentifier, QueryPage, useFetch } from "~/utils/query"; +import { Page, Show, ShowP } from "~/models"; +import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query"; import { getDisplayDate } from "~/models/utils"; import { useScroll } from "~/utils/hooks/use-scroll"; import { withRoute } from "~/utils/router"; @@ -44,6 +44,9 @@ import { Container } from "~/components/container"; import { makeTitle } from "~/utils/utils"; import { Link } from "~/utils/link"; import { Studio } from "~/models/resources/studio"; +import { Paged, Person, PersonP } from "~/models"; +import { PersonAvatar } from "~/components/person"; +import { useInView } from "react-intersection-observer"; const StudioText = ({ studio, @@ -72,7 +75,6 @@ const StudioText = ({ const ShowHeader = ({ data }: { data?: Show }) => { /* const scroll = useScroll(); */ const { t } = useTranslation("browse"); - console.log(data); // TODO: tweek the navbar color with the theme. return ( @@ -81,6 +83,7 @@ const ShowHeader = ({ data }: { data?: Show }) => { {/* TODO: Put the navbar outside of the scrollbox */} { ); }; +const staffQuery = (slug: string): QueryIdentifier => ({ + parser: PersonP, + path: ["shows", slug, "people"], + infinite: true, +}); + +const ShowStaff = ({ slug }: { slug: string }) => { + const { data, isError, error, isFetching, hasNextPage, fetchNextPage } = useInfiniteFetch( + staffQuery(slug), + ); + const { ref } = useInView({ + onChange: () => !isFetching && hasNextPage && fetchNextPage(), + }); + + /* if (isError) return null; */ + + return ( + <> + + Staff + + + {(data ? data.pages.flatMap((x) => x.items) : [...Array(6)]).map((x) => ( + + ))} +
+ + + ); +}; + const query = (slug: string): QueryIdentifier => ({ parser: ShowP, path: ["shows", slug], @@ -264,10 +302,11 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => { + ); }; -ShowDetails.getFetchUrls = ({ slug }) => [query(slug)]; +ShowDetails.getFetchUrls = ({ slug }) => [query(slug), staffQuery(slug)]; export default withRoute(ShowDetails); diff --git a/front/src/utils/query.ts b/front/src/utils/query.ts index 03343ecf..1fdcc68d 100644 --- a/front/src/utils/query.ts +++ b/front/src/utils/query.ts @@ -76,33 +76,39 @@ export type QueryIdentifier = { parser: z.ZodType; path: string[]; params?: { [query: string]: boolean | number | string | string[] }; + infinite?: boolean; }; export type QueryPage = ComponentType & { getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[]; }; -const toQuery = (params?: { [query: string]: boolean | number | string | string[] }) => { - if (!params) return undefined; - return ( - "?" + - Object.entries(params) - .map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`) - .join("&") - ); +const toQueryKey = (query: QueryIdentifier) => { + if (query.params) { + return [ + ...query.path, + "?" + + Object.entries(query.params) + .map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`) + .join("&"), + ]; + } else { + return query.path; + } }; export const useFetch = (query: QueryIdentifier) => { return useQuery({ - queryKey: [...query.path, toQuery(query.params)], + queryKey: toQueryKey(query), queryFn: (ctx) => queryFn(query.parser, ctx), }); }; export const useInfiniteFetch = (query: QueryIdentifier) => { return useInfiniteQuery, KyooErrors>({ - queryKey: [...query.path, toQuery(query.params)], + queryKey: toQueryKey(query), queryFn: (ctx) => queryFn(Paged(query.parser), ctx), + getNextPageParam: (page: Page) => page?.next || undefined, }); }; @@ -113,12 +119,19 @@ export const fetchQuery = async (queries: QueryIdentifier[]) => { const client = createQueryClient(); await Promise.all( - queries.map((query) => - client.prefetchQuery({ - queryKey: [...query.path, toQuery(query.params)], - queryFn: (ctx) => queryFn(query.parser, ctx), - }), - ), + queries.map((query) => { + if (query.infinite) { + return client.prefetchInfiniteQuery({ + queryKey: toQueryKey(query), + queryFn: (ctx) => queryFn(Paged(query.parser), ctx), + }); + } else { + return client.prefetchQuery({ + queryKey: toQueryKey(query), + queryFn: (ctx) => queryFn(query.parser, ctx), + }); + } + }), ); return dehydrate(client); }; diff --git a/front/yarn.lock b/front/yarn.lock index 40e8b0f0..bfc9bf17 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2191,6 +2191,11 @@ react-dom@18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-intersection-observer@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.0.tgz#f6b6e616e625f9bf255857c5cba9dbf7b1825ec7" + integrity sha512-v0403CmomOVlzhqFXlzOxg0ziLcVq8mfbP0AwAcEQWgZmR2OulOT79Ikznw4UlB3N+jlUYqLMe4SDHUOyp0t2A== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"