mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-07 18:24:14 -04:00
Add staff list on show page
This commit is contained in:
parent
9b80b340e3
commit
608bc15e1f
@ -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"
|
||||
|
55
front/src/components/person.tsx
Normal file
55
front/src/components/person.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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
front/src/models/resources/person.ts
Normal file
46
front/src/models/resources/person.ts
Normal 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>;
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user