Make the modal pretty

This commit is contained in:
Captain Jack Sparrow 2024-06-14 19:06:44 +00:00
parent 0f7fcc0fa1
commit af7a2b9477
4 changed files with 166 additions and 30 deletions

View File

@ -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<Icons, string> = {
star: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill=currentColor><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <rect id="Rectangle_4" data-name="Rectangle 4" width="24" height="24" fill="none"></rect> <path id="Star" d="M10,15,4.122,18.09l1.123-6.545L.489,6.91l6.572-.955L10,0l2.939,5.955,6.572.955-4.755,4.635,1.123,6.545Z" transform="translate(2 3)" stroke=currentColor stroke-miterlimit="10" stroke-width="1.5"></path> </g></svg>`,
starBorder: `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <rect id="Rectangle_4" data-name="Rectangle 4" width="24" height="24" fill="none"></rect> <path id="Star" d="M10,15,4.122,18.09l1.123-6.545L.489,6.91l6.572-.955L10,0l2.939,5.955,6.572.955-4.755,4.635,1.123,6.545Z" transform="translate(2 3)" stroke="currentColor" stroke-miterlimit="10" stroke-width="1.5" fill="none"></path> </g></svg>`,
search: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/></svg>`,
bookmark: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M384 48V512l-192-112L0 512V48C0 21.5 21.5 0 48 0h288C362.5 0 384 21.5 384 48z"/></svg>`,
clock: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512zM232 256C232 264 236 271.5 242.7 275.1L338.7 339.1C349.7 347.3 364.6 344.3 371.1 333.3C379.3 322.3 376.3 307.4 365.3 300L280 243.2V120C280 106.7 269.3 96 255.1 96C242.7 96 231.1 106.7 231.1 120L232 256z"/></svg>`,

View File

@ -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<any>(null);
const [error, setError] = useState<string | null>(null); // State for storing API errors
const [mediaInfo, setMediaInfo] = useState<any>(null);
const [error, setError] = useState<string | null>(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<any>(`/${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<any>(
`/${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 <div>Error: {error}</div>;
}
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 (
<div
className="fixed inset-0 bg-opacity-50 flex justify-center items-center z-50 transition-opacity duration-100"
@ -92,14 +149,97 @@ export function PopupModal({
>
<div
ref={modalRef}
className="rounded-xl p-3 bg-mediaCard-hoverBackground flex justify-center items-center transition-opacity duration-200 w-full max-w-3xl"
className="rounded-xl p-3 m-6 bg-modal-background flex justify-center items-center transition-opacity duration-200 w-full max-w-3xl"
style={{ opacity: style.opacity }}
>
<div className="aspect-w-16 aspect-h-9 w-full">
<img
src={`https://image.tmdb.org/t/p/original/${data?.backdrop_path}`}
className="rounded-xl object-cover w-full h-full"
<img
src={`https://image.tmdb.org/t/p/original/${data?.backdrop_path}`}
className="rounded-xl object-cover w-full h-full"
/>
<div className="flex pt-3 items-center gap-4">
<h1 className="relative text-2xl whitespace-normal font-bold text-white">
{data?.title || data?.name}
</h1>
<div className="font-semibold">
{media.type === "movie" ? (
<div className="px-2 py-1 bg-search-background rounded">
<span>{displayCertification}</span>
</div>
) : (
<div className="px-2 py-1 bg-search-background rounded">
<span>
{(() => {
mediaInfo?.results?.find(
(result: any) => result.iso_3166_1 === "US",
);
return usReleaseInfo && usReleaseInfo.rating
? usReleaseInfo.rating
: "Not Rated";
})()}
</span>
</div>
)}
</div>
<div className="flex flex-row gap-2 font-bold">
{media.type === "movie" &&
data?.runtime &&
formatRuntime(data.runtime)}
<div>
{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}
</div>
</div>
</div>
<div className="flex flex-row gap-3 pb-3 pt-2">
<div className="flex items-center space-x-[2px] font-semibold">
{Array.from({ length: 5 }, (_, index) => {
return (
<span key={index}>
{index < Math.round(Number(data?.vote_average) / 2) ? (
<Icon icon={Icons.STAR} />
) : (
<Icon icon={Icons.STAR_BORDER} />
)}
</span>
);
})}
</div>
{data?.genres?.map((genre: { name: string }) => (
<div
key={genre.name}
className="px-2 py-1 bg-mediaCard-hoverBackground rounded hover:bg-search-background cursor-default duration-200 transition-colors"
>
{genre.name}
</div>
))}
</div>
<p className="relative whitespace-normal font-medium">
{data?.overview}
</p>
<div className="flex justify-center items-center mt-4 mb-1">
<Button
theme="purple"
onClick={() =>
navigate(
`/media/tmdb-${isTVShow ? "tv" : "movie"}-${media.id}-${media.title}`,
)
}
className="text-2xl font-bold hover:bg-purple-700 transition-all duration-[0.3s] hover:scale-105"
>
<div className="flex flex-row gap-2 items-center">
<Icon icon={Icons.PLAY} />
Watch
</div>
</Button>
</div>
</div>
</div>
</div>

View File

@ -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 (
<>

View File

@ -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<PlayingSlice> = (set) => ({
@ -49,11 +48,10 @@ export const createPlayingSlice: MakeSlice<PlayingSlice> = (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;
});
},
});