mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-17 04:24:16 -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/>.
|
* 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 {
|
export enum MovieStatus {
|
||||||
/**
|
Unknown = 0,
|
||||||
* The title of this show.
|
Finished = 1,
|
||||||
*/
|
Planned = 3,
|
||||||
name: string;
|
}
|
||||||
|
|
||||||
|
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.
|
* The list of alternative titles of this movie.
|
||||||
*/
|
*/
|
||||||
aliases: string[];
|
aliases: z.array(z.string()),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The summary of this show.
|
* The summary of this movie.
|
||||||
*/
|
*/
|
||||||
overview: string;
|
overview: z.string().nullable(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this movie aired or planned
|
* Is this movie not aired yet or finished?
|
||||||
*/
|
*/
|
||||||
isPlanned: boolean;
|
status: z.nativeEnum(MovieStatus),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date this mavie aired. It can also be null if this is unknown.
|
* The date this movie aired. It can also be null if this is unknown.
|
||||||
*/
|
*/
|
||||||
airDate: Date | null;
|
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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const getDisplayDate = (startAir: Date, endAir?: Date | null) => {
|
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()) {
|
if (!endAir || startAir.getFullYear() === endAir.getFullYear()) {
|
||||||
return startAir.getFullYear();
|
return startAir.getFullYear();
|
||||||
}
|
}
|
||||||
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
|
return startAir.getFullYear() + (endAir ? ` - ${endAir.getFullYear()}` : "");
|
||||||
|
}
|
||||||
|
else if (airDate) {
|
||||||
|
return airDate.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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LocalMovies, PlayArrow } from "@mui/icons-material";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Divider,
|
|
||||||
Fab,
|
|
||||||
IconButton,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
SxProps,
|
SxProps,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tooltip,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import useTranslation from "next-translate/useTranslation";
|
import useTranslation from "next-translate/useTranslation";
|
||||||
import Head from "next/head";
|
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 { Episode, EpisodeP, Season, Show, ShowP } from "~/models";
|
||||||
import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query";
|
import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query";
|
||||||
import { getDisplayDate } from "~/models/utils";
|
|
||||||
import { withRoute } from "~/utils/router";
|
import { withRoute } from "~/utils/router";
|
||||||
import { Container } from "~/components/container";
|
import { Container } from "~/components/container";
|
||||||
import { makeTitle } from "~/utils/utils";
|
import { makeTitle } from "~/utils/utils";
|
||||||
import { Link } from "~/utils/link";
|
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 { ErrorComponent, ErrorPage } from "~/components/errors";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { EpisodeLine } from "~/components/episode";
|
import { EpisodeLine } from "~/components/episode";
|
||||||
import InfiniteScroll from "react-infinite-scroll-component";
|
import InfiniteScroll from "react-infinite-scroll-component";
|
||||||
import { useRouter } from "next/router";
|
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 EpisodeGrid = ({ slug, season }: { slug: string; season: number }) => {
|
||||||
const { data, isError, error, hasNextPage, fetchNextPage } = useInfiniteFetch(
|
const { data, isError, error, hasNextPage, fetchNextPage } = useInfiniteFetch(
|
||||||
episodesQuery(slug, season),
|
EpisodeGrid.query(slug, season),
|
||||||
);
|
);
|
||||||
const { t } = useTranslation("browse");
|
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 SeasonTab = ({ slug, seasons, sx }: { slug: string; seasons?: Season[]; sx?: SxProps }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const seasonQuery = typeof router.query.season === "string" ? parseInt(router.query.season) : NaN;
|
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 }) => [
|
ShowDetails.getFetchUrls = ({ slug, season = 1 }) => [
|
||||||
query(slug),
|
query(slug),
|
||||||
staffQuery(slug),
|
ShowStaff.query(slug),
|
||||||
episodesQuery(slug, season),
|
EpisodeGrid.query(slug, season),
|
||||||
];
|
];
|
||||||
|
|
||||||
export default withRoute(ShowDetails);
|
export default withRoute(ShowDetails);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user