Fix skeletons and errors on show page

This commit is contained in:
Zoe Roux 2022-09-19 12:31:54 +09:00
parent 6cd976e057
commit f95d8b565e
13 changed files with 146 additions and 75 deletions

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"

View File

@ -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)",
};

View File

@ -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>
</Box>
{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 />
</>

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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 {

View File

@ -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(),
}),
);

View File

@ -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.

View File

@ -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.
*/

View File

@ -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) => (
<PersonAvatar
key={x ? x.id : i}
person={x}
sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
/>
))}
<div ref={ref} />
</Box>
{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 }}
/>
))}
</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} />

View File

@ -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"