diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 51bd0de6..1ff91ea8 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -2,6 +2,8 @@ import classNames from "classnames"; import { memo, useEffect, useRef } from "react"; export enum Icons { + STAR = "star", + STAR_BORDER = "starBorder", SEARCH = "search", BOOKMARK = "bookmark", BOOKMARK_OUTLINE = "bookmark_outline", @@ -74,6 +76,8 @@ export interface IconProps { } const iconList: Record = { + star: ` `, + starBorder: ` `, search: ``, bookmark: ``, clock: ``, diff --git a/src/components/media/PopupModal.tsx b/src/components/media/PopupModal.tsx index 5f0d4efc..c3319a53 100644 --- a/src/components/media/PopupModal.tsx +++ b/src/components/media/PopupModal.tsx @@ -1,15 +1,18 @@ import React, { useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; -import { MediaItem } from "@/utils/mediaTypes"; import { get } from "@/backend/metadata/tmdb"; import { conf } from "@/setup/config"; +import { MediaItem } from "@/utils/mediaTypes"; + +import { Button } from "../buttons/Button"; +import { Icon, Icons } from "../Icon"; interface PopupModalProps { isVisible: boolean; onClose: () => void; playingTitle: { id: string; - title: string; type: string; }; media: MediaItem; @@ -20,6 +23,12 @@ type StyleState = { visibility: "visible" | "hidden" | undefined; }; +const formatRuntime = (minutes: number): string => { + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return `${hours}h ${mins}m`; +}; + export function PopupModal({ isVisible, onClose, @@ -31,13 +40,17 @@ export function PopupModal({ opacity: 0, visibility: "hidden", }); - // Use any or a more generic type here const [data, setData] = useState(null); - const [error, setError] = useState(null); // State for storing API errors + const [mediaInfo, setMediaInfo] = useState(null); + const [error, setError] = useState(null); + const navigate = useNavigate(); useEffect(() => { function handleClickOutside(event: MouseEvent) { - if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { onClose(); } } @@ -58,33 +71,77 @@ export function PopupModal({ useEffect(() => { const fetchData = async () => { - if (!isVisible) return; // Ensure fetchData does not proceed if the modal is not visible - + if (!isVisible) return; + try { - const mediaTypePath = media.type === 'show' ? 'tv' : media.type; + const mediaTypePath = media.type === "show" ? "tv" : media.type; const result = await get(`/${mediaTypePath}/${media.id}`, { api_key: conf().TMDB_READ_API_KEY, language: "en-US", }); setData(result); - setError(null); // Reset error state on successful fetch + setError(null); } catch (err) { - setError('Failed to fetch media data'); + setError("Failed to fetch media data"); console.error(err); } }; - + fetchData(); - }, [media.id, media.type, isVisible]); // Dependency array remains the same + }, [media.id, media.type, isVisible]); + + useEffect(() => { + const fetchContentRatingsOrReleaseDates = async () => { + if (!isVisible || (media.type !== "show" && media.type !== "movie")) + return; + + try { + const mediaTypeForAPI = media.type === "show" ? "tv" : media.type; + const endpointSuffix = + media.type === "show" ? "content_ratings" : "release_dates"; + const result = await get( + `/${mediaTypeForAPI}/${media.id}/${endpointSuffix}`, + { + api_key: conf().TMDB_READ_API_KEY, + language: "en-US", + }, + ); + setMediaInfo(result); + setError(null); + } catch (err) { + setError("Failed to fetch content ratings or release dates"); + console.error(err); + } + }; + + fetchContentRatingsOrReleaseDates(); + }, [media.id, media.type, isVisible]); if (!isVisible && style.visibility === "hidden") return null; - console.log(data); - // Handle error state in the UI if (error) { return
Error: {error}
; } + const isTVShow = media.type === "show"; + const usReleaseInfo = mediaInfo?.results?.find( + (result: any) => result.iso_3166_1 === "US", + ); + const certifiedReleases = + usReleaseInfo?.release_dates?.filter((date: any) => date.certification) || + []; + const relevantRelease = + certifiedReleases.length > 0 + ? certifiedReleases.reduce((prev: any, current: any) => + new Date(prev.release_date) > new Date(current.release_date) + ? prev + : current, + ) + : null; + const displayCertification = relevantRelease + ? `${relevantRelease.certification}` + : "Not Rated"; + return (
- +
+

+ {data?.title || data?.name} +

+
+ {media.type === "movie" ? ( +
+ {displayCertification} +
+ ) : ( +
+ + {(() => { + mediaInfo?.results?.find( + (result: any) => result.iso_3166_1 === "US", + ); + return usReleaseInfo && usReleaseInfo.rating + ? usReleaseInfo.rating + : "Not Rated"; + })()} + +
+ )} +
+
+ {media.type === "movie" && + data?.runtime && + formatRuntime(data.runtime)} +
+ {media.type === "movie" + ? data?.release_date + ? String(data.release_date).split("-")[0] + : null + : media.type === "show" + ? data?.first_air_date + ? String(data.first_air_date).split("-")[0] + : null + : null} +
+
+
+
+
+ {Array.from({ length: 5 }, (_, index) => { + return ( + + {index < Math.round(Number(data?.vote_average) / 2) ? ( + + ) : ( + + )} + + ); + })} +
+ {data?.genres?.map((genre: { name: string }) => ( +
+ {genre.name} +
+ ))} +
+

+ {data?.overview} +

+
+ +
diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index efd54ad7..047bdf4a 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -51,15 +51,9 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) { const [isPopupVisible, setIsPopupVisible] = useState(false); const handleClick = useCallback(() => { - setPlayingTitle(props.media.id, props.media.title, props.media.type); + setPlayingTitle(props.media.id, props.media.type); setIsPopupVisible(!isPopupVisible); - }, [ - setPlayingTitle, - isPopupVisible, - props.media.id, - props.media.title, - props.media.type, - ]); + }, [setPlayingTitle, isPopupVisible, props.media.id, props.media.type]); return ( <> diff --git a/src/stores/player/slices/playing.ts b/src/stores/player/slices/playing.ts index 6e54c0ca..33ee0aef 100644 --- a/src/stores/player/slices/playing.ts +++ b/src/stores/player/slices/playing.ts @@ -13,12 +13,11 @@ export interface PlayingSlice { }; playingTitle: { id: string; - title: string; type: string; }; play(): void; pause(): void; - setPlayingTitle(id: string, title: string, type: string): void; + setPlayingTitle(id: string, type: string): void; } export const createPlayingSlice: MakeSlice = (set) => ({ @@ -49,11 +48,10 @@ export const createPlayingSlice: MakeSlice = (set) => ({ state.mediaPlaying.isPaused = true; }); }, - setPlayingTitle(id: string, title: string, type: string) { + setPlayingTitle(id: string, type: string) { set((state) => { state.playingTitle.id = id; state.playingTitle.type = type; - state.playingTitle.title = title; }); }, });