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"