mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-30 18:22:41 -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