Add staff list on show page

This commit is contained in:
Zoe Roux 2022-09-06 03:26:06 +02:00
parent 9b80b340e3
commit 608bc15e1f
7 changed files with 182 additions and 20 deletions

View File

@ -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"

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (
<Box sx={sx}>
<Skeleton variant="circular" sx={{ width: "100%", aspectRatio: "1/1" }}/>
<Typography align="center"><Skeleton/></Typography>
<Typography variant="body2" align="center"><Skeleton/></Typography>
</Box>
)
}
return (
<Link href={`/person/${person.slug}`} color="inherit" sx={sx}>
<Avatar
src={person.poster!}
alt={person.name}
sx={{ width: "100%", height: "unset", aspectRatio: "1/1" }}
/>
<Typography align="center">{person.name}</Typography>
{person.role && person.type && (
<Typography variant="body2" align="center">
{person.type} ({person.role})
</Typography>
)}
{person.role && !person.type && (
<Typography variant="body2" align="center">
{person.role}
</Typography>
)}
</Link>
);
};

View File

@ -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";

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<typeof PersonP>;

View File

@ -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 */}
<Navbar
position="fixed"
elevation={0}
sx={{ backgroundColor: `rgba(0, 0, 0, ${0 /*0.4 + scroll / 1000*/})` }}
/>
<Image
@ -244,6 +247,41 @@ const ShowHeader = ({ data }: { data?: Show }) => {
);
};
const staffQuery = (slug: string): QueryIdentifier<Person> => ({
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 (
<>
<Typography variant="h4" component="h2" sx={{ py: 3, pl: 4 }}>
Staff
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", maxWidth: "100%", overflowY: "auto" }}>
{(data ? data.pages.flatMap((x) => x.items) : [...Array(6)]).map((x) => (
<PersonAvatar
key={x.id}
person={x}
sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
/>
))}
<div ref={ref} />
</Box>
</>
);
};
const query = (slug: string): QueryIdentifier<Show> => ({
parser: ShowP,
path: ["shows", slug],
@ -264,10 +302,11 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
<meta name="description" content={data?.overview} />
</Head>
<ShowHeader data={data} />
<ShowStaff slug={slug} />
</>
);
};
ShowDetails.getFetchUrls = ({ slug }) => [query(slug)];
ShowDetails.getFetchUrls = ({ slug }) => [query(slug), staffQuery(slug)];
export default withRoute(ShowDetails);

View File

@ -76,33 +76,39 @@ export type QueryIdentifier<T = unknown> = {
parser: z.ZodType<T>;
path: string[];
params?: { [query: string]: boolean | number | string | string[] };
infinite?: boolean;
};
export type QueryPage<Props = {}> = ComponentType<Props> & {
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
};
const toQuery = (params?: { [query: string]: boolean | number | string | string[] }) => {
if (!params) return undefined;
return (
const toQueryKey = <Data>(query: QueryIdentifier<Data>) => {
if (query.params) {
return [
...query.path,
"?" +
Object.entries(params)
Object.entries(query.params)
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
.join("&")
);
.join("&"),
];
} else {
return query.path;
}
};
export const useFetch = <Data>(query: QueryIdentifier<Data>) => {
return useQuery<Data, KyooErrors>({
queryKey: [...query.path, toQuery(query.params)],
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn(query.parser, ctx),
});
};
export const useInfiniteFetch = <Data>(query: QueryIdentifier<Data>) => {
return useInfiniteQuery<Page<Data>, KyooErrors>({
queryKey: [...query.path, toQuery(query.params)],
queryKey: toQueryKey(query),
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
getNextPageParam: (page: Page<Data>) => 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)],
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);
};

View File

@ -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"