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"