diff --git a/front/locales/en/browse.json b/front/locales/en/browse.json
index 998c9bb0..9bdabda1 100644
--- a/front/locales/en/browse.json
+++ b/front/locales/en/browse.json
@@ -5,6 +5,10 @@
"studio": "Studio",
"genre": "Genres",
"genre-none": "No genres",
- "staff": "Staff"
+ "staff": "Staff",
+ "staff-none": "The staff is unknown",
+ "noOverview": "No overview available",
+ "episode-none": "There is no episodes in this season",
+ "episodeNoMetadata": "No metadata available"
}
}
diff --git a/front/locales/fr/browse.json b/front/locales/fr/browse.json
index 545bff6d..6fe3e1dd 100644
--- a/front/locales/fr/browse.json
+++ b/front/locales/fr/browse.json
@@ -5,6 +5,10 @@
"studio": "Studio",
"genre": "Genres",
"genre-none": "Aucun genres",
- "staff": "Staff"
+ "staff": "Staff",
+ "staff-none": "Aucun membre du staff connu",
+ "noOverview": "Aucune description disponible",
+ "episode-none": "Il n'y a pas d'episodes dans cette saison",
+ "episodeNoMetadata": "Aucune metadonnée disponible"
}
}
diff --git a/front/package.json b/front/package.json
index b986b08c..773f96dc 100644
--- a/front/package.json
+++ b/front/package.json
@@ -29,7 +29,7 @@
"next-translate": "^1.5.0",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-intersection-observer": "^9.4.0",
+ "react-infinite-scroll-component": "^6.1.0",
"react-query": "^4.0.0-beta.23",
"superjson": "^1.9.1",
"zod": "^3.18.0"
diff --git a/front/src/components/container.tsx b/front/src/components/container.tsx
index ad55bb64..96afca42 100644
--- a/front/src/components/container.tsx
+++ b/front/src/components/container.tsx
@@ -20,14 +20,22 @@
import { styled, experimental_sx as sx } from "@mui/system";
-export const Container = styled("div")(sx({
- display: "flex",
- pl: "15px",
- pr: "15px",
- mx: "auto",
- width: {
- sm: "540px",
- md: "880px",
- lg: "1170px",
- },
-}));
+export const Container = styled("div")(
+ sx({
+ display: "flex",
+ px: "15px",
+ mx: "auto",
+ width: {
+ sm: "540px",
+ md: "880px",
+ lg: "1170px",
+ },
+ }),
+);
+
+export const containerPadding = {
+ xs: "15px",
+ sm: "calc((100vw - 540px) / 2)",
+ md: "calc((100vw - 880px) / 2)",
+ lg: "calc((100vw - 1170px) / 2)",
+};
diff --git a/front/src/components/episode.tsx b/front/src/components/episode.tsx
index 7cd900a8..d7dc4232 100644
--- a/front/src/components/episode.tsx
+++ b/front/src/components/episode.tsx
@@ -19,12 +19,13 @@
*/
import { Box, Divider, Skeleton, SxProps, Typography } from "@mui/material";
+import useTranslation from "next-translate/useTranslation";
import { Episode } from "~/models";
import { Link } from "~/utils/link";
import { Image } from "./poster";
const displayNumber = (episode: Episode) => {
- if (episode.seasonNumber && episode.episodeNumber)
+ if (typeof episode.seasonNumber === "number" && typeof episode.episodeNumber === "number")
return `S${episode.seasonNumber}:E${episode.episodeNumber}`;
if (episode.absoluteNumber) return episode.absoluteNumber.toString();
return "???";
@@ -41,6 +42,8 @@ export const EpisodeBox = ({ episode, sx }: { episode?: Episode; sx: SxProps })
};
export const EpisodeLine = ({ episode, sx }: { episode?: Episode; sx?: SxProps }) => {
+ const { t } = useTranslation("browse");
+
return (
<>
}
-
- {episode?.name ?? }
- {episode?.overview ?? }
-
+ {episode ? (
+
+ {episode.name ?? t("show.episodeNoMetadata")}
+ {episode.overview && {episode.overview}}
+
+ ) : (
+
+ {}
+ {}
+
+ )}
>
diff --git a/front/src/components/errors.tsx b/front/src/components/errors.tsx
index fb7b7826..19fa800b 100644
--- a/front/src/components/errors.tsx
+++ b/front/src/components/errors.tsx
@@ -18,20 +18,38 @@
* along with Kyoo. If not, see .
*/
-import { Alert, Snackbar, SnackbarCloseReason, Typography } from "@mui/material";
+import { Alert, Box, Snackbar, SnackbarCloseReason, Typography, SxProps } from "@mui/material";
import { SyntheticEvent, useState } from "react";
import { KyooErrors } from "~/models";
-export const ErrorPage = ({ errors }: { errors: string[] }) => {
+export const ErrorComponent = ({ errors, sx }: { errors: string[], sx?: SxProps }) => {
return (
- <>
- Error
+
+ Error
{errors.map((x, i) => (
-
+
{x}
))}
- >
+
+ );
+};
+
+export const ErrorPage = ({ errors }: { errors: string[] }) => {
+ return (
+
+
+
);
};
diff --git a/front/src/components/person.tsx b/front/src/components/person.tsx
index 7c909440..6b26ee9c 100644
--- a/front/src/components/person.tsx
+++ b/front/src/components/person.tsx
@@ -26,7 +26,7 @@ export const PersonAvatar = ({ person, sx }: { person?: Person; sx?: SxProps })
if (!person) {
return (
-
+
diff --git a/front/src/components/poster.tsx b/front/src/components/poster.tsx
index f5b78890..0c6b9f67 100644
--- a/front/src/components/poster.tsx
+++ b/front/src/components/poster.tsx
@@ -62,8 +62,9 @@ const _Image = ({
// This allow the loading bool to be false with SSR but still be on client-side
useLayoutEffect(() => {
- if (!imgRef.current?.complete) setLoading(true);
- }, []);
+ if (!imgRef.current?.complete && img) setLoading(true);
+ if (!img && !loading) setLoading(false);
+ }, [img, loading]);
return (
*": { width: "100%", height: "100%" },
}}
{...others}
@@ -100,9 +101,9 @@ const _Image = ({
export const Image = styled(_Image)({});
// eslint-disable-next-line jsx-a11y/alt-text
-const _Poster = (
- props: ImagePropsWithLoading & { width?: Width; height?: Height },
-) => <_Image aspectRatio="2 / 3" {...props} />;
+const _Poster = (props: ImagePropsWithLoading & { width?: Width; height?: Height }) => (
+ <_Image aspectRatio="2 / 3" {...props} />
+);
declare module "@mui/material/styles" {
interface ComponentsPropsList {
diff --git a/front/src/models/resources/episode.ts b/front/src/models/resources/episode.ts
index a5805f8a..e36fa67c 100644
--- a/front/src/models/resources/episode.ts
+++ b/front/src/models/resources/episode.ts
@@ -47,17 +47,17 @@ export const EpisodeP = z.preprocess(
/**
* The title of this episode.
*/
- name: z.string(),
+ name: z.string().nullable(),
/**
* The overview of this episode.
*/
- overview: z.string(),
+ overview: z.string().nullable(),
/**
* The release date of this episode. It can be null if unknown.
*/
- releaseDate: zdate(),
+ releaseDate: zdate().nullable(),
}),
);
diff --git a/front/src/models/resources/season.ts b/front/src/models/resources/season.ts
index f4878891..75e357f5 100644
--- a/front/src/models/resources/season.ts
+++ b/front/src/models/resources/season.ts
@@ -41,7 +41,7 @@ export const SeasonP = z.preprocess(
/**
* A quick overview of this season.
*/
- overview: z.string(),
+ overview: z.string().nullable(),
/**
* The starting air date of this season.
diff --git a/front/src/models/resources/show.ts b/front/src/models/resources/show.ts
index bac2798e..fbd94416 100644
--- a/front/src/models/resources/show.ts
+++ b/front/src/models/resources/show.ts
@@ -37,7 +37,9 @@ export enum Status {
export const ShowP = z.preprocess(
(x: any) => {
+ // Waiting for the API to be updaded
x.name = x.title;
+ if (x.aliases === null) x.aliases = [];
return x;
},
ResourceP.merge(ImagesP).extend({
@@ -52,7 +54,7 @@ export const ShowP = z.preprocess(
/**
* The summary of this show.
*/
- overview: z.string(),
+ overview: z.string().nullable(),
/**
* Is this show airing, not aired yet or finished?
*/
@@ -72,7 +74,7 @@ export const ShowP = z.preprocess(
/**
* The studio that made this show.
*/
- studio: StudioP.optional(),
+ studio: StudioP.optional().nullable(),
/**
* The list of seasons of this show.
*/
diff --git a/front/src/pages/show/[slug].tsx b/front/src/pages/show/[slug].tsx
index 41ce8592..51175030 100644
--- a/front/src/pages/show/[slug].tsx
+++ b/front/src/pages/show/[slug].tsx
@@ -42,14 +42,13 @@ import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/
import { getDisplayDate } from "~/models/utils";
import { useScroll } from "~/utils/hooks/use-scroll";
import { withRoute } from "~/utils/router";
-import { Container } from "~/components/container";
+import { Container, containerPadding } 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";
-import { ErrorPage } from "~/components/errors";
+import { ErrorComponent, ErrorPage } from "~/components/errors";
import { useState } from "react";
import { EpisodeBox, EpisodeLine } from "~/components/episode";
@@ -58,7 +57,7 @@ const StudioText = ({
loading = false,
sx,
}: {
- studio?: Studio;
+ studio?: Studio | null;
loading?: boolean;
sx?: SxProps;
}) => {
@@ -196,7 +195,7 @@ const ShowHeader = ({ data }: { data?: Show }) => {
{": "}
{!data ? (
- ) : data?.genres ? (
+ ) : data?.genres && data.genres.length ? (
data.genres.map((genre, i) => [
i > 0 && ", ",
@@ -211,7 +210,9 @@ const ShowHeader = ({ data }: { data?: Show }) => {
- {data?.overview ?? [...Array(4)].map((_, i) => )}
+ {data
+ ? data.overview ?? t("show.noOverview")
+ : [...Array(4)].map((_, i) => )}
{
{t("show.genre")}
- {!data || data.genres ? (
+ {!data || data.genres?.length ? (
{(data ? data.genres! : [...Array(3)]).map((genre, i) => (
-
@@ -262,19 +263,17 @@ const ShowStaff = ({ slug }: { slug: string }) => {
const { data, isError, error, isFetching, hasNextPage, fetchNextPage } = useInfiniteFetch(
staffQuery(slug),
);
- const { ref } = useInView({
- onChange: () => !isFetching && hasNextPage && fetchNextPage(),
- });
const { t } = useTranslation("browse");
// TODO: Unsure that the fetchNextPage is only used when needed (currently called way too mutch)
- // TODO: Handle errors
- /* if (isError) return null; */
+ if (isError) return ;
return (
<>
-
+
{t("show.staff")}
@@ -287,16 +286,31 @@ const ShowStaff = ({ slug }: { slug: string }) => {
-
- {(data ? data.pages.flatMap((x) => x.items) : [...Array(6)]).map((x, i) => (
-
- ))}
-
-
+ {data && data?.pages.at(0)?.count === 0 ? (
+
+ {t("show.staff-none")}
+
+ ) : (
+
+ {(data ? data.pages.flatMap((x) => x.items) : [...Array(20)]).map((x, i) => (
+
+ ))}
+
+ )}
>
);
};
@@ -314,14 +328,19 @@ const EpisodeGrid = ({ slug, season }: { slug: string; season: number }) => {
const { data, isError, error, isFetching, hasNextPage, fetchNextPage } = useInfiniteFetch(
episodesQuery(slug, season),
);
- const { ref } = useInView({
- onChange: () => !isFetching && hasNextPage && fetchNextPage(),
- });
+ const { t } = useTranslation("browse");
// TODO: Unsure that the fetchNextPage is only used when needed (currently called way too mutch)
- // TODO: Handle errors
- /* if (isError) return null; */
+ if (isError) return ;
+
+ if (data && data.pages.at(0)?.count === 0) {
+ return (
+
+ {t("show.episode-none")}
+
+ );
+ }
return (
@@ -339,14 +358,12 @@ const SeasonTab = ({ slug, seasons, sx }: { slug: string; seasons?: Season[]; sx
// TODO: handle absolute number only shows (without seasons)
return (
-
+
setSeason(i)} aria-label="List of seasons">
{seasons
? seasons.map((x) => )
: [...Array(3)].map((_, i) => (
-
-
-
+ } value={i + 1} disabled />
))}
@@ -372,7 +389,7 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
<>
{makeTitle(data?.name)}
-
+
diff --git a/front/yarn.lock b/front/yarn.lock
index bfc9bf17..7da73d9d 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -2191,10 +2191,12 @@ 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-infinite-scroll-component@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz#7e511e7aa0f728ac3e51f64a38a6079ac522407f"
+ integrity sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==
+ dependencies:
+ throttle-debounce "^2.1.0"
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
@@ -2457,6 +2459,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+throttle-debounce@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2"
+ integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==
+
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"