mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-04 14:14:12 -04:00
Add movie page
This commit is contained in:
parent
9f6cdcdf91
commit
aeef6bd44d
@ -18,34 +18,62 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Resource, Images } from "../traits";
|
||||
import { z } from "zod";
|
||||
import { zdate } from "~/utils/zod";
|
||||
import { ImagesP, ResourceP } from "../traits";
|
||||
import { GenreP } from "./genre";
|
||||
import { StudioP } from "./studio";
|
||||
|
||||
/**
|
||||
* A series or a movie.
|
||||
* The enum containing movie's status.
|
||||
*/
|
||||
export interface Movie extends Resource, Images {
|
||||
/**
|
||||
* The title of this show.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The list of alternative titles of this movie.
|
||||
*/
|
||||
aliases: string[];
|
||||
|
||||
/**
|
||||
* The summary of this show.
|
||||
*/
|
||||
overview: string;
|
||||
|
||||
/**
|
||||
* Is this movie aired or planned
|
||||
*/
|
||||
isPlanned: boolean;
|
||||
|
||||
/**
|
||||
* The date this mavie aired. It can also be null if this is unknown.
|
||||
*/
|
||||
airDate: Date | null;
|
||||
export enum MovieStatus {
|
||||
Unknown = 0,
|
||||
Finished = 1,
|
||||
Planned = 3,
|
||||
}
|
||||
|
||||
export const MovieP = z.preprocess(
|
||||
(x: any) => {
|
||||
// Waiting for the API to be updaded
|
||||
x.name = x.title;
|
||||
if (x.aliases === null) x.aliases = [];
|
||||
x.airDate = x.startAir;
|
||||
return x;
|
||||
},
|
||||
ResourceP.merge(ImagesP).extend({
|
||||
/**
|
||||
* The title of this movie.
|
||||
*/
|
||||
name: z.string(),
|
||||
/**
|
||||
* The list of alternative titles of this movie.
|
||||
*/
|
||||
aliases: z.array(z.string()),
|
||||
/**
|
||||
* The summary of this movie.
|
||||
*/
|
||||
overview: z.string().nullable(),
|
||||
/**
|
||||
* Is this movie not aired yet or finished?
|
||||
*/
|
||||
status: z.nativeEnum(MovieStatus),
|
||||
/**
|
||||
* The date this movie aired. It can also be null if this is unknown.
|
||||
*/
|
||||
airDate: zdate().nullable(),
|
||||
/**
|
||||
* The list of genres (themes) this movie has.
|
||||
*/
|
||||
genres: z.array(GenreP).optional(),
|
||||
/**
|
||||
* The studio that made this movie.
|
||||
*/
|
||||
studio: StudioP.optional().nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* A Movie type
|
||||
*/
|
||||
export type Movie = z.infer<typeof MovieP>;
|
||||
|
@ -18,9 +18,22 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const getDisplayDate = (startAir: Date, endAir?: Date | null) => {
|
||||
if (!endAir || startAir.getFullYear() === endAir.getFullYear()) {
|
||||
return startAir.getFullYear();
|
||||
import { Movie, Show } from "./resources";
|
||||
|
||||
export const getDisplayDate = (data: Show | Movie) => {
|
||||
const {
|
||||
startAir,
|
||||
endAir,
|
||||
airDate,
|
||||
}: { startAir?: Date | null; endAir?: Date | null; airDate?: Date | null } = data;
|
||||
|
||||
if (startAir) {
|
||||
if (!endAir || startAir.getFullYear() === endAir.getFullYear()) {
|
||||
return startAir.getFullYear();
|
||||
}
|
||||
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
|
||||
}
|
||||
else if (airDate) {
|
||||
return airDate.getFullYear();
|
||||
}
|
||||
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
|
||||
};
|
||||
|
309
front/src/pages/movie/[slug].tsx
Normal file
309
front/src/pages/movie/[slug].tsx
Normal file
@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LocalMovies, PlayArrow } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Fab,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
SxProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Head from "next/head";
|
||||
import { Navbar } from "~/components/navbar";
|
||||
import { Image, Poster } from "~/components/poster";
|
||||
import { Movie, MovieP, Show } from "~/models";
|
||||
import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query";
|
||||
import { getDisplayDate } from "~/models/utils";
|
||||
import { withRoute } from "~/utils/router";
|
||||
import { Container } from "~/components/container";
|
||||
import { makeTitle } from "~/utils/utils";
|
||||
import { Link } from "~/utils/link";
|
||||
import { Studio } from "~/models/resources/studio";
|
||||
import { Person, PersonP } from "~/models";
|
||||
import { PersonAvatar } from "~/components/person";
|
||||
import { ErrorComponent, ErrorPage } from "~/components/errors";
|
||||
import { HorizontalList } from "~/components/horizontal-list";
|
||||
|
||||
const StudioText = ({
|
||||
studio,
|
||||
loading = false,
|
||||
sx,
|
||||
}: {
|
||||
studio?: Studio | null;
|
||||
loading?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
if (!loading && !studio) return null;
|
||||
return (
|
||||
<Typography sx={sx}>
|
||||
{t("show.studio")}:{" "}
|
||||
{loading ? (
|
||||
<Skeleton width="5rem" sx={{ display: "inline-flex" }} />
|
||||
) : (
|
||||
<Link href={`/studio/${studio!.slug}`}>{studio!.name}</Link>
|
||||
)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShowHeader = ({ data }: { data?: Show | Movie }) => {
|
||||
/* const scroll = useScroll(); */
|
||||
const { t } = useTranslation("browse");
|
||||
// TODO: tweek the navbar color with the theme.
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: Add a shadow on navbar items */}
|
||||
{/* TODO: Put the navbar outside of the scrollbox */}
|
||||
<Navbar
|
||||
position="fixed"
|
||||
elevation={0}
|
||||
sx={{ backgroundColor: `rgba(0, 0, 0, ${0.4 /*+ scroll / 1000*/})` }}
|
||||
/>
|
||||
<Image
|
||||
img={data?.thumbnail}
|
||||
alt=""
|
||||
loading={!data}
|
||||
width="100%"
|
||||
height={{ xs: "40vh", sm: "60vh", lg: "70vh" }}
|
||||
sx={{
|
||||
minHeight: { xs: "350px", sm: "400px", lg: "550px" },
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
background: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.6) 100%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container
|
||||
sx={{
|
||||
position: "relative",
|
||||
marginTop: { xs: "-30%", sm: "-25%", md: "-15rem", lg: "-21rem", xl: "-23rem" },
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "center", sm: "unset" },
|
||||
textAlign: { xs: "center", sm: "unset" },
|
||||
}}
|
||||
>
|
||||
<Poster
|
||||
img={data?.poster}
|
||||
alt={data?.name ?? ""}
|
||||
loading={!data}
|
||||
width={{ xs: "50%", md: "25%" }}
|
||||
sx={{ maxWidth: { xs: "175px", sm: "unset" }, flexShrink: 0 }}
|
||||
/>
|
||||
<Box sx={{ alignSelf: { xs: "center", sm: "end", md: "center" }, pl: { sm: "2.5rem" } }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
color: { md: "white" },
|
||||
fontWeight: { md: 900 },
|
||||
mb: ".5rem",
|
||||
}}
|
||||
>
|
||||
{data?.name ?? <Skeleton width="15rem" />}
|
||||
</Typography>
|
||||
{(!data || getDisplayDate(data)) && (
|
||||
<Typography variant="h5" sx={{ color: { md: "white" }, fontWeight: 300, mb: ".5rem" }}>
|
||||
{data != undefined ? (
|
||||
getDisplayDate(data)
|
||||
) : (
|
||||
<Skeleton width="5rem" sx={{ mx: { xs: "auto", sm: "unset" } }} />
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ "& > *": { m: ".3rem !important" } }}>
|
||||
<Tooltip title={t("show.play")}>
|
||||
<Fab color="primary" size="small" aria-label={t("show.play")}>
|
||||
<PlayArrow />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("show.trailer")} aria-label={t("show.trailer")}>
|
||||
<IconButton>
|
||||
<LocalMovies sx={{ color: { md: "white" } }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: "none", md: "flex" },
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "25%",
|
||||
flexDirection: "column",
|
||||
alignSelf: "end",
|
||||
pr: "15px",
|
||||
}}
|
||||
>
|
||||
{data?.logo && (
|
||||
<Image
|
||||
img={data.logo}
|
||||
alt=""
|
||||
width="100%"
|
||||
height="100px"
|
||||
sx={{ display: { xs: "none", lg: "unset" } }}
|
||||
/>
|
||||
)}
|
||||
<StudioText loading={!data} studio={data?.studio} sx={{ mt: "auto", mb: 3 }} />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
<Container sx={{ display: { xs: "block", sm: "none" }, pt: 3 }}>
|
||||
<StudioText loading={!data} studio={data?.studio} sx={{ mb: 1 }} />
|
||||
<Typography sx={{ mb: 1 }}>
|
||||
{t("show.genre")}
|
||||
{": "}
|
||||
{!data ? (
|
||||
<Skeleton width="10rem" sx={{ display: "inline-flex" }} />
|
||||
) : data?.genres && data.genres.length ? (
|
||||
data.genres.map((genre, i) => [
|
||||
i > 0 && ", ",
|
||||
<Link key={genre.id} href={`/genres/${genre.slug}`}>
|
||||
{genre.name}
|
||||
</Link>,
|
||||
])
|
||||
) : (
|
||||
t("show.genre-none")
|
||||
)}
|
||||
</Typography>
|
||||
</Container>
|
||||
|
||||
<Container sx={{ pt: 2 }}>
|
||||
<Typography align="justify" sx={{ flexBasis: 0, flexGrow: 1, pt: { sm: 2 } }}>
|
||||
{data
|
||||
? data.overview ?? t("show.noOverview")
|
||||
: [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
||||
</Typography>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ mx: 2, display: { xs: "none", sm: "block" } }}
|
||||
/>
|
||||
<Box sx={{ flexBasis: "25%", display: { xs: "none", sm: "block" } }}>
|
||||
<StudioText
|
||||
loading={!data}
|
||||
studio={data?.studio}
|
||||
sx={{ display: { xs: "none", sm: "block", md: "none" }, pb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="h4" component="h2">
|
||||
{t("show.genre")}
|
||||
</Typography>
|
||||
{!data || data.genres?.length ? (
|
||||
<ul>
|
||||
{(data ? data.genres! : [...Array(3)]).map((genre, i) => (
|
||||
<li key={genre?.id ?? i}>
|
||||
<Typography>
|
||||
{genre ? (
|
||||
<Link href={`/genres/${genre.slug}`}>{genre.name}</Link>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Typography>{t("show.genre-none")}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const ShowStaff = ({ slug }: { slug: string }) => {
|
||||
const { data, isError, error } = useInfiniteFetch(ShowStaff.query(slug));
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
// TODO: handle infinite scroll
|
||||
|
||||
if (isError) return <ErrorComponent {...error} />;
|
||||
|
||||
return (
|
||||
<HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}>
|
||||
{(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 }}
|
||||
/>
|
||||
))}
|
||||
</HorizontalList>
|
||||
);
|
||||
};
|
||||
|
||||
ShowStaff.query = (slug: string): QueryIdentifier<Person> => ({
|
||||
parser: PersonP,
|
||||
path: ["shows", slug, "people"],
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
const query = (slug: string): QueryIdentifier<Movie> => ({
|
||||
parser: MovieP,
|
||||
path: ["shows", slug],
|
||||
params: {
|
||||
fields: ["genres", "studio"],
|
||||
},
|
||||
});
|
||||
|
||||
const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
const { data, error } = useFetch(query(slug));
|
||||
|
||||
if (error) return <ErrorPage {...error} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{makeTitle(data?.name)}</title>
|
||||
<meta name="description" content={data?.overview!} />
|
||||
</Head>
|
||||
<ShowHeader data={data} />
|
||||
<ShowStaff slug={slug} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
MovieDetails.getFetchUrls = ({ slug }) => [
|
||||
query(slug),
|
||||
ShowStaff.query(slug),
|
||||
];
|
||||
|
||||
export default withRoute(MovieDetails);
|
@ -18,280 +18,33 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LocalMovies, PlayArrow } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Fab,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
SxProps,
|
||||
Tab,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Head from "next/head";
|
||||
import { Navbar } from "~/components/navbar";
|
||||
import { Image, Poster } from "~/components/poster";
|
||||
import { Episode, EpisodeP, Season, Show, ShowP } from "~/models";
|
||||
import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query";
|
||||
import { getDisplayDate } from "~/models/utils";
|
||||
import { withRoute } from "~/utils/router";
|
||||
import { Container } from "~/components/container";
|
||||
import { makeTitle } from "~/utils/utils";
|
||||
import { Link } from "~/utils/link";
|
||||
import { Studio } from "~/models/resources/studio";
|
||||
import { Person, PersonP } from "~/models";
|
||||
import { PersonAvatar } from "~/components/person";
|
||||
import { ErrorComponent, ErrorPage } from "~/components/errors";
|
||||
import { useState } from "react";
|
||||
import { EpisodeLine } from "~/components/episode";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import { useRouter } from "next/router";
|
||||
import { HorizontalList } from "~/components/horizontal-list";
|
||||
import { ShowHeader, ShowStaff } from "../movie/[slug]";
|
||||
|
||||
const StudioText = ({
|
||||
studio,
|
||||
loading = false,
|
||||
sx,
|
||||
}: {
|
||||
studio?: Studio | null;
|
||||
loading?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
if (!loading && !studio) return null;
|
||||
return (
|
||||
<Typography sx={sx}>
|
||||
{t("show.studio")}:{" "}
|
||||
{loading ? (
|
||||
<Skeleton width="5rem" sx={{ display: "inline-flex" }} />
|
||||
) : (
|
||||
<Link href={`/studio/${studio!.slug}`}>{studio!.name}</Link>
|
||||
)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const ShowHeader = ({ data }: { data?: Show }) => {
|
||||
/* const scroll = useScroll(); */
|
||||
const { t } = useTranslation("browse");
|
||||
// TODO: tweek the navbar color with the theme.
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: Add a shadow on navbar items */}
|
||||
{/* TODO: Put the navbar outside of the scrollbox */}
|
||||
<Navbar
|
||||
position="fixed"
|
||||
elevation={0}
|
||||
sx={{ backgroundColor: `rgba(0, 0, 0, ${0.4 /*+ scroll / 1000*/})` }}
|
||||
/>
|
||||
<Image
|
||||
img={data?.thumbnail}
|
||||
alt=""
|
||||
loading={!data}
|
||||
width="100%"
|
||||
height={{ xs: "40vh", sm: "60vh", lg: "70vh" }}
|
||||
sx={{
|
||||
minHeight: { xs: "350px", sm: "400px", lg: "550px" },
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
background: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.6) 100%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container
|
||||
sx={{
|
||||
position: "relative",
|
||||
marginTop: { xs: "-30%", sm: "-25%", md: "-15rem", lg: "-21rem", xl: "-23rem" },
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "center", sm: "unset" },
|
||||
textAlign: { xs: "center", sm: "unset" },
|
||||
}}
|
||||
>
|
||||
<Poster
|
||||
img={data?.poster}
|
||||
alt={data?.name ?? ""}
|
||||
loading={!data}
|
||||
width={{ xs: "50%", md: "25%" }}
|
||||
sx={{ maxWidth: { xs: "175px", sm: "unset" }, flexShrink: 0 }}
|
||||
/>
|
||||
<Box sx={{ alignSelf: { xs: "center", sm: "end", md: "center" }, pl: { sm: "2.5rem" } }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
color: { md: "white" },
|
||||
fontWeight: { md: 900 },
|
||||
mb: ".5rem",
|
||||
}}
|
||||
>
|
||||
{data?.name ?? <Skeleton width="15rem" />}
|
||||
</Typography>
|
||||
{(!data || data.startAir) && (
|
||||
<Typography variant="h5" sx={{ color: { md: "white" }, fontWeight: 300, mb: ".5rem" }}>
|
||||
{data != undefined ? (
|
||||
getDisplayDate(data.startAir!, data.endAir)
|
||||
) : (
|
||||
<Skeleton width="5rem" sx={{ mx: { xs: "auto", sm: "unset" } }} />
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ "& > *": { m: ".3rem !important" } }}>
|
||||
<Tooltip title={t("show.play")}>
|
||||
<Fab color="primary" size="small" aria-label={t("show.play")}>
|
||||
<PlayArrow />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("show.trailer")} aria-label={t("show.trailer")}>
|
||||
<IconButton>
|
||||
<LocalMovies sx={{ color: { md: "white" } }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: "none", md: "flex" },
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "25%",
|
||||
flexDirection: "column",
|
||||
alignSelf: "end",
|
||||
pr: "15px",
|
||||
}}
|
||||
>
|
||||
{data?.logo && (
|
||||
<Image
|
||||
img={data.logo}
|
||||
alt=""
|
||||
width="100%"
|
||||
height="100px"
|
||||
sx={{ display: { xs: "none", lg: "unset" } }}
|
||||
/>
|
||||
)}
|
||||
<StudioText loading={!data} studio={data?.studio} sx={{ mt: "auto", mb: 3 }} />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
<Container sx={{ display: { xs: "block", sm: "none" }, pt: 3 }}>
|
||||
<StudioText loading={!data} studio={data?.studio} sx={{ mb: 1 }} />
|
||||
<Typography sx={{ mb: 1 }}>
|
||||
{t("show.genre")}
|
||||
{": "}
|
||||
{!data ? (
|
||||
<Skeleton width="10rem" sx={{ display: "inline-flex" }} />
|
||||
) : data?.genres && data.genres.length ? (
|
||||
data.genres.map((genre, i) => [
|
||||
i > 0 && ", ",
|
||||
<Link key={genre.id} href={`/genres/${genre.slug}`}>
|
||||
{genre.name}
|
||||
</Link>,
|
||||
])
|
||||
) : (
|
||||
t("show.genre-none")
|
||||
)}
|
||||
</Typography>
|
||||
</Container>
|
||||
|
||||
<Container sx={{ pt: 2 }}>
|
||||
<Typography align="justify" sx={{ flexBasis: 0, flexGrow: 1, pt: { sm: 2 } }}>
|
||||
{data
|
||||
? data.overview ?? t("show.noOverview")
|
||||
: [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
||||
</Typography>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ mx: 2, display: { xs: "none", sm: "block" } }}
|
||||
/>
|
||||
<Box sx={{ flexBasis: "25%", display: { xs: "none", sm: "block" } }}>
|
||||
<StudioText
|
||||
loading={!data}
|
||||
studio={data?.studio}
|
||||
sx={{ display: { xs: "none", sm: "block", md: "none" }, pb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="h4" component="h2">
|
||||
{t("show.genre")}
|
||||
</Typography>
|
||||
{!data || data.genres?.length ? (
|
||||
<ul>
|
||||
{(data ? data.genres! : [...Array(3)]).map((genre, i) => (
|
||||
<li key={genre?.id ?? i}>
|
||||
<Typography>
|
||||
{genre ? (
|
||||
<Link href={`/genres/${genre.slug}`}>{genre.name}</Link>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Typography>{t("show.genre-none")}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const staffQuery = (slug: string): QueryIdentifier<Person> => ({
|
||||
parser: PersonP,
|
||||
path: ["shows", slug, "people"],
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
const ShowStaff = ({ slug }: { slug: string }) => {
|
||||
const { data, isError, error } = useInfiniteFetch(staffQuery(slug));
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
// TODO: handle infinite scroll
|
||||
|
||||
if (isError) return <ErrorComponent {...error} />;
|
||||
|
||||
return (
|
||||
<HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}>
|
||||
{(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 }}
|
||||
/>
|
||||
))}
|
||||
</HorizontalList>
|
||||
);
|
||||
};
|
||||
|
||||
const episodesQuery = (slug: string, season: string | number): QueryIdentifier<Episode> => ({
|
||||
parser: EpisodeP,
|
||||
path: ["shows", slug, "episode"],
|
||||
params: {
|
||||
seasonNumber: season,
|
||||
},
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
const EpisodeGrid = ({ slug, season }: { slug: string; season: number }) => {
|
||||
const { data, isError, error, hasNextPage, fetchNextPage } = useInfiniteFetch(
|
||||
episodesQuery(slug, season),
|
||||
EpisodeGrid.query(slug, season),
|
||||
);
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
@ -321,6 +74,16 @@ const EpisodeGrid = ({ slug, season }: { slug: string; season: number }) => {
|
||||
);
|
||||
};
|
||||
|
||||
EpisodeGrid.query = (slug: string, season: string | number): QueryIdentifier<Episode> => ({
|
||||
parser: EpisodeP,
|
||||
path: ["shows", slug, "episode"],
|
||||
params: {
|
||||
seasonNumber: season,
|
||||
},
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
|
||||
const SeasonTab = ({ slug, seasons, sx }: { slug: string; seasons?: Season[]; sx?: SxProps }) => {
|
||||
const router = useRouter();
|
||||
const seasonQuery = typeof router.query.season === "string" ? parseInt(router.query.season) : NaN;
|
||||
@ -380,8 +143,8 @@ const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
|
||||
ShowDetails.getFetchUrls = ({ slug, season = 1 }) => [
|
||||
query(slug),
|
||||
staffQuery(slug),
|
||||
episodesQuery(slug, season),
|
||||
ShowStaff.query(slug),
|
||||
EpisodeGrid.query(slug, season),
|
||||
];
|
||||
|
||||
export default withRoute(ShowDetails);
|
||||
|
Loading…
x
Reference in New Issue
Block a user