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
+55
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>
);
};
+3
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";
+46
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>;
+43 -4
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);
+29 -16
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 (
"?" +
Object.entries(params)
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
.join("&")
);
const toQueryKey = <Data>(query: QueryIdentifier<Data>) => {
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 = <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)],
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);
};