mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -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