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",
|
"next-translate": "^1.5.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-intersection-observer": "^9.4.0",
|
||||||
"react-query": "^4.0.0-beta.23",
|
"react-query": "^4.0.0-beta.23",
|
||||||
"superjson": "^1.9.1",
|
"superjson": "^1.9.1",
|
||||||
"zod": "^3.18.0"
|
"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 "./show";
|
||||||
export * from "./movie";
|
export * from "./movie";
|
||||||
export * from "./collection";
|
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 Head from "next/head";
|
||||||
import { Navbar } from "~/components/navbar";
|
import { Navbar } from "~/components/navbar";
|
||||||
import { Image, Poster } from "~/components/poster";
|
import { Image, Poster } from "~/components/poster";
|
||||||
import { Show, ShowP } from "~/models";
|
import { Page, Show, ShowP } from "~/models";
|
||||||
import { QueryIdentifier, QueryPage, useFetch } from "~/utils/query";
|
import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query";
|
||||||
import { getDisplayDate } from "~/models/utils";
|
import { getDisplayDate } from "~/models/utils";
|
||||||
import { useScroll } from "~/utils/hooks/use-scroll";
|
import { useScroll } from "~/utils/hooks/use-scroll";
|
||||||
import { withRoute } from "~/utils/router";
|
import { withRoute } from "~/utils/router";
|
||||||
@ -44,6 +44,9 @@ import { Container } from "~/components/container";
|
|||||||
import { makeTitle } from "~/utils/utils";
|
import { makeTitle } from "~/utils/utils";
|
||||||
import { Link } from "~/utils/link";
|
import { Link } from "~/utils/link";
|
||||||
import { Studio } from "~/models/resources/studio";
|
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 = ({
|
const StudioText = ({
|
||||||
studio,
|
studio,
|
||||||
@ -72,7 +75,6 @@ const StudioText = ({
|
|||||||
const ShowHeader = ({ data }: { data?: Show }) => {
|
const ShowHeader = ({ data }: { data?: Show }) => {
|
||||||
/* const scroll = useScroll(); */
|
/* const scroll = useScroll(); */
|
||||||
const { t } = useTranslation("browse");
|
const { t } = useTranslation("browse");
|
||||||
console.log(data);
|
|
||||||
// TODO: tweek the navbar color with the theme.
|
// TODO: tweek the navbar color with the theme.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -81,6 +83,7 @@ const ShowHeader = ({ data }: { data?: Show }) => {
|
|||||||
{/* TODO: Put the navbar outside of the scrollbox */}
|
{/* TODO: Put the navbar outside of the scrollbox */}
|
||||||
<Navbar
|
<Navbar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
|
elevation={0}
|
||||||
sx={{ backgroundColor: `rgba(0, 0, 0, ${0 /*0.4 + scroll / 1000*/})` }}
|
sx={{ backgroundColor: `rgba(0, 0, 0, ${0 /*0.4 + scroll / 1000*/})` }}
|
||||||
/>
|
/>
|
||||||
<Image
|
<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> => ({
|
const query = (slug: string): QueryIdentifier<Show> => ({
|
||||||
parser: ShowP,
|
parser: ShowP,
|
||||||
path: ["shows", slug],
|
path: ["shows", slug],
|
||||||
@ -264,10 +302,11 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
|||||||
<meta name="description" content={data?.overview} />
|
<meta name="description" content={data?.overview} />
|
||||||
</Head>
|
</Head>
|
||||||
<ShowHeader data={data} />
|
<ShowHeader data={data} />
|
||||||
|
<ShowStaff slug={slug} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ShowDetails.getFetchUrls = ({ slug }) => [query(slug)];
|
ShowDetails.getFetchUrls = ({ slug }) => [query(slug), staffQuery(slug)];
|
||||||
|
|
||||||
export default withRoute(ShowDetails);
|
export default withRoute(ShowDetails);
|
||||||
|
@ -76,33 +76,39 @@ export type QueryIdentifier<T = unknown> = {
|
|||||||
parser: z.ZodType<T>;
|
parser: z.ZodType<T>;
|
||||||
path: string[];
|
path: string[];
|
||||||
params?: { [query: string]: boolean | number | string | string[] };
|
params?: { [query: string]: boolean | number | string | string[] };
|
||||||
|
infinite?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
||||||
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
|
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const toQuery = (params?: { [query: string]: boolean | number | string | string[] }) => {
|
const toQueryKey = <Data>(query: QueryIdentifier<Data>) => {
|
||||||
if (!params) return undefined;
|
if (query.params) {
|
||||||
return (
|
return [
|
||||||
"?" +
|
...query.path,
|
||||||
Object.entries(params)
|
"?" +
|
||||||
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
Object.entries(query.params)
|
||||||
.join("&")
|
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
||||||
);
|
.join("&"),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return query.path;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetch = <Data>(query: QueryIdentifier<Data>) => {
|
export const useFetch = <Data>(query: QueryIdentifier<Data>) => {
|
||||||
return useQuery<Data, KyooErrors>({
|
return useQuery<Data, KyooErrors>({
|
||||||
queryKey: [...query.path, toQuery(query.params)],
|
queryKey: toQueryKey(query),
|
||||||
queryFn: (ctx) => queryFn(query.parser, ctx),
|
queryFn: (ctx) => queryFn(query.parser, ctx),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfiniteFetch = <Data>(query: QueryIdentifier<Data>) => {
|
export const useInfiniteFetch = <Data>(query: QueryIdentifier<Data>) => {
|
||||||
return useInfiniteQuery<Page<Data>, KyooErrors>({
|
return useInfiniteQuery<Page<Data>, KyooErrors>({
|
||||||
queryKey: [...query.path, toQuery(query.params)],
|
queryKey: toQueryKey(query),
|
||||||
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
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();
|
const client = createQueryClient();
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
queries.map((query) =>
|
queries.map((query) => {
|
||||||
client.prefetchQuery({
|
if (query.infinite) {
|
||||||
queryKey: [...query.path, toQuery(query.params)],
|
return client.prefetchInfiniteQuery({
|
||||||
queryFn: (ctx) => queryFn(query.parser, ctx),
|
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);
|
return dehydrate(client);
|
||||||
};
|
};
|
||||||
|
@ -2191,6 +2191,11 @@ react-dom@18.2.0:
|
|||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.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:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user