mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Fix skeletons and errors on show page
This commit is contained in:
parent
6cd976e057
commit
f95d8b565e
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -20,14 +20,22 @@
|
||||
|
||||
import { styled, experimental_sx as sx } from "@mui/system";
|
||||
|
||||
export const Container = styled("div")(sx({
|
||||
export const Container = styled("div")(
|
||||
sx({
|
||||
display: "flex",
|
||||
pl: "15px",
|
||||
pr: "15px",
|
||||
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)",
|
||||
};
|
||||
|
@ -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 (
|
||||
<>
|
||||
<Link
|
||||
@ -59,10 +62,17 @@ export const EpisodeLine = ({ episode, sx }: { episode?: Episode; sx?: SxProps }
|
||||
{episode ? displayNumber(episode) : <Skeleton />}
|
||||
</Typography>
|
||||
<Image img={episode?.thumbnail} width="18%" aspectRatio="16/9" sx={{ flexShrink: 0 }} />
|
||||
<Box>
|
||||
<Typography variant="h6">{episode?.name ?? <Skeleton />}</Typography>
|
||||
<Typography variant="body2">{episode?.overview ?? <Skeleton />}</Typography>
|
||||
{episode ? (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="h6">{episode.name ?? t("show.episodeNoMetadata")}</Typography>
|
||||
{episode.overview && <Typography variant="body2">{episode.overview}</Typography>}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant="h6">{<Skeleton />}</Typography>
|
||||
<Typography variant="body2">{<Skeleton />}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Link>
|
||||
<Divider />
|
||||
</>
|
||||
|
@ -18,20 +18,38 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Typography variant="h1">Error</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
backgroundColor: "error.light",
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<Typography variant="h1" component="h1" sx={{ fontWeight: 500 }}>Error</Typography>
|
||||
{errors.map((x, i) => (
|
||||
<Typography variant="h2" key={i}>
|
||||
<Typography variant="h2" component="h2" key={i}>
|
||||
{x}
|
||||
</Typography>
|
||||
))}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorPage = ({ errors }: { errors: string[] }) => {
|
||||
return (
|
||||
<Box sx={{ height: "100vh" }}>
|
||||
<ErrorComponent errors={errors} sx={{ backgroundColor: "unset" }} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,7 @@ export const PersonAvatar = ({ person, sx }: { person?: Person; sx?: SxProps })
|
||||
if (!person) {
|
||||
return (
|
||||
<Box sx={sx}>
|
||||
<Skeleton variant="circular" sx={{ width: "100%", aspectRatio: "1/1" }}/>
|
||||
<Skeleton variant="circular" sx={{ width: "100%", aspectRatio: "1/1", height: "unset" }}/>
|
||||
<Typography align="center"><Skeleton/></Typography>
|
||||
<Typography variant="body2" align="center"><Skeleton/></Typography>
|
||||
</Box>
|
||||
|
@ -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 (
|
||||
<Box
|
||||
@ -73,7 +74,7 @@ const _Image = ({
|
||||
aspectRatio,
|
||||
width,
|
||||
height,
|
||||
backgroundColor: "primary.dark",
|
||||
backgroundColor: "grey.300",
|
||||
"& > *": { 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 {
|
||||
|
@ -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(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 ? (
|
||||
<Skeleton width="10rem" sx={{ display: "inline-flex" }} />
|
||||
) : data?.genres ? (
|
||||
) : data?.genres && data.genres.length ? (
|
||||
data.genres.map((genre, i) => [
|
||||
i > 0 && ", ",
|
||||
<Link key={genre.id} href={`/genres/${genre.slug}`}>
|
||||
@ -211,7 +210,9 @@ const ShowHeader = ({ data }: { data?: Show }) => {
|
||||
|
||||
<Container sx={{ pt: 2 }}>
|
||||
<Typography align="justify" sx={{ flexBasis: 0, flexGrow: 1, pt: { sm: 2 } }}>
|
||||
{data?.overview ?? [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
||||
{data
|
||||
? data.overview ?? t("show.noOverview")
|
||||
: [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
||||
</Typography>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
@ -229,7 +230,7 @@ const ShowHeader = ({ data }: { data?: Show }) => {
|
||||
<Typography variant="h4" component="h2">
|
||||
{t("show.genre")}
|
||||
</Typography>
|
||||
{!data || data.genres ? (
|
||||
{!data || data.genres?.length ? (
|
||||
<ul>
|
||||
{(data ? data.genres! : [...Array(3)]).map((genre, i) => (
|
||||
<li key={genre?.id ?? 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 <ErrorComponent {...error} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", py: 3 }}>
|
||||
<Container
|
||||
sx={{ display: "flex", flexDirection: "row", justifyContent: "space-between", py: 3 }}
|
||||
>
|
||||
<Typography variant="h4" component="h2">
|
||||
{t("show.staff")}
|
||||
</Typography>
|
||||
@ -287,16 +286,31 @@ const ShowStaff = ({ slug }: { slug: string }) => {
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Container>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", maxWidth: "100%", overflowY: "auto", py: 1, }}>
|
||||
{(data ? data.pages.flatMap((x) => x.items) : [...Array(6)]).map((x, i) => (
|
||||
{data && data?.pages.at(0)?.count === 0 ? (
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<Typography sx={{ py: 3 }}>{t("show.staff-none")}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Container
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
maxWidth: "100%",
|
||||
overflowY: "auto",
|
||||
pt: 1,
|
||||
pb: 2,
|
||||
overflowX: "visible",
|
||||
}}
|
||||
>
|
||||
{(data ? data.pages.flatMap((x) => x.items) : [...Array(20)]).map((x, i) => (
|
||||
<PersonAvatar
|
||||
key={x ? x.id : i}
|
||||
person={x}
|
||||
sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
|
||||
/>
|
||||
))}
|
||||
<div ref={ref} />
|
||||
</Box>
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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 <ErrorComponent {...error} />;
|
||||
|
||||
if (data && data.pages.at(0)?.count === 0) {
|
||||
return (
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<Typography sx={{ py: 3 }}>{t("show.episode-none")}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", backgroundColor: "background.paper" }}>
|
||||
@ -339,14 +358,12 @@ const SeasonTab = ({ slug, seasons, sx }: { slug: string; seasons?: Season[]; sx
|
||||
// TODO: handle absolute number only shows (without seasons)
|
||||
return (
|
||||
<Container sx={sx}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider", width: "100%" }}>
|
||||
<Tabs value={season} onChange={(_, i) => setSeason(i)} aria-label="List of seasons">
|
||||
{seasons
|
||||
? seasons.map((x) => <Tab key={x.seasonNumber} label={x.name} value={x.seasonNumber} />)
|
||||
: [...Array(3)].map((_, i) => (
|
||||
<Typography key={i} variant="button">
|
||||
<Skeleton />
|
||||
</Typography>
|
||||
<Tab key={i} label={<Skeleton width="5rem" />} value={i + 1} disabled />
|
||||
))}
|
||||
</Tabs>
|
||||
<EpisodeGrid slug={slug} season={season} />
|
||||
@ -372,7 +389,7 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
<>
|
||||
<Head>
|
||||
<title>{makeTitle(data?.name)}</title>
|
||||
<meta name="description" content={data?.overview} />
|
||||
<meta name="description" content={data?.overview!} />
|
||||
</Head>
|
||||
<ShowHeader data={data} />
|
||||
<ShowStaff slug={slug} />
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user