mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-20 10:12:09 +00:00
update details modal
This commit is contained in:
parent
f1d97d4892
commit
0b54fc5182
8 changed files with 238 additions and 167 deletions
|
|
@ -210,6 +210,7 @@
|
|||
"tmdb": "View on TMDB",
|
||||
"imdb": "View on IMDb",
|
||||
"episodes": "Episodes",
|
||||
"seasons": "Season/s",
|
||||
"season": "Season",
|
||||
"episode": "Episode",
|
||||
"airs": "Airs",
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export function OverlayPortal(props: {
|
|||
darken?: boolean;
|
||||
show?: boolean;
|
||||
close?: () => void;
|
||||
durationClass?: string;
|
||||
}) {
|
||||
const [portalElement, setPortalElement] = useState<Element | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -106,6 +107,7 @@ export function OverlayPortal(props: {
|
|||
animation="slide-up"
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
isChild
|
||||
durationClass={props.durationClass ?? "duration-200"}
|
||||
>
|
||||
{/* a tabable index that does nothing - used so focus trap doesn't error when nothing is rendered yet */}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { t } from "i18next";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { useLanguageStore } from "@/stores/language";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { shouldShowProgress } from "@/stores/progress/utils";
|
||||
import { scrapeIMDb } from "@/utils/imdbScraper";
|
||||
|
|
@ -26,7 +27,12 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
const [selectedSeason, setSelectedSeason] = useState<number>(1);
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const [hasCopiedShare, setHasCopiedShare] = useState(false);
|
||||
const [logoHeight, setLogoHeight] = useState<number>(0);
|
||||
const logoRef = useRef<HTMLDivElement>(null);
|
||||
const progress = useProgressStore((s) => s.items);
|
||||
const enableImageLogos = usePreferencesStore(
|
||||
(state) => state.enableImageLogos,
|
||||
);
|
||||
|
||||
const showProgress = useMemo(() => {
|
||||
if (!data.id) return null;
|
||||
|
|
@ -42,6 +48,20 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
}
|
||||
}, [showProgress]);
|
||||
|
||||
// Add effect to measure logo height
|
||||
useEffect(() => {
|
||||
if (logoRef.current) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setLogoHeight(entry.contentRect.height);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(logoRef.current);
|
||||
return () => resizeObserver.disconnect();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchExternalData = async () => {
|
||||
if (!data.imdbId) return;
|
||||
|
|
@ -133,40 +153,87 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Backdrop - Even taller */}
|
||||
<div className="h-64 lg:h-80 xl:h-96 relative -mt-12">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="relative -mt-12 z-20"
|
||||
style={{
|
||||
height: `${Math.max(500, logoHeight + 400)}px`,
|
||||
}}
|
||||
>
|
||||
{/* Title/Logo positioned on backdrop */}
|
||||
<div ref={logoRef} className="absolute inset-x-0 bottom-20 z-30 px-6">
|
||||
{data.logoUrl && enableImageLogos ? (
|
||||
<img
|
||||
src={data.logoUrl}
|
||||
alt={data.title}
|
||||
className="max-w-[16rem] md:max-w-[20rem] lg:max-w-[30rem] max-h-[12rem] object-contain drop-shadow-lg bg-transparent"
|
||||
style={{ background: "none" }}
|
||||
/>
|
||||
) : (
|
||||
<h3 className="text-3xl md:text-4xl font-bold text-white drop-shadow-lg">
|
||||
{data.title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center"
|
||||
className="absolute inset-0 bg-cover bg-center opacity-60 before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_center,_transparent_0%,_rgba(0,0,0,0.5)_80%)]"
|
||||
style={{
|
||||
backgroundImage: data.backdrop
|
||||
? `url(${data.backdrop})`
|
||||
: undefined,
|
||||
maskImage:
|
||||
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 60px)",
|
||||
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 120px)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 60px)",
|
||||
"linear-gradient(to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 120px)",
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-6 pb-6 mt-[-70px] flex-grow">
|
||||
<div className="px-6 pb-6 mt-[-70px] flex-grow relative z-30">
|
||||
<DetailsHeader
|
||||
data={data}
|
||||
onPlayClick={handlePlayClick}
|
||||
onTrailerClick={() => setShowTrailer(true)}
|
||||
onShareClick={handleShareClick}
|
||||
showProgress={showProgress}
|
||||
voteAverage={data.voteAverage}
|
||||
voteCount={data.voteCount}
|
||||
releaseDate={data.releaseDate}
|
||||
seasons={
|
||||
data.type === "show" ? data.seasonData?.seasons.length : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Two Column Layout - Stacked on Mobile */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 md:gap-6">
|
||||
{/* Left Column - Description */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 md:gap-6 pt-4">
|
||||
{/* Left Column - Main Content (2/3) */}
|
||||
<div className="md:col-span-2">
|
||||
{/* Description */}
|
||||
{data.overview && (
|
||||
<p className="text-sm text-white/90 mb-6">{data.overview}</p>
|
||||
)}
|
||||
|
||||
{/* Genres */}
|
||||
{data.genres && data.genres.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{data.genres.map((genre, index) => (
|
||||
<span
|
||||
key={genre.id}
|
||||
className="text-[11px] px-2 py-0.5 rounded-full bg-white/20 text-white/80 transition-all duration-300 hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||
style={{
|
||||
animationDelay: `${((data.genres?.length ?? 0) - 1 - index) * 60}ms`,
|
||||
transform: "scale(0)",
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
{genre.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Director and Cast */}
|
||||
<div className="space-y-4 mb-6">
|
||||
{data.director && (
|
||||
|
|
@ -190,8 +257,10 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Details */}
|
||||
<DetailsInfo data={data} imdbData={imdbData} rtData={rtData} />
|
||||
{/* Right Column - Details Info (1/3) */}
|
||||
<div className="md:col-span-1">
|
||||
<DetailsInfo data={data} imdbData={imdbData} rtData={rtData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Episodes Carousel for TV Shows */}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { Button } from "@/components/buttons/Button";
|
|||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { MediaBookmarkButton } from "@/components/media/MediaBookmark";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
|
||||
import { DetailsHeaderProps } from "./types";
|
||||
|
||||
|
|
@ -15,127 +14,107 @@ export function DetailsHeader({
|
|||
onTrailerClick,
|
||||
onShareClick,
|
||||
showProgress,
|
||||
voteAverage,
|
||||
voteCount,
|
||||
releaseDate,
|
||||
seasons,
|
||||
}: DetailsHeaderProps) {
|
||||
const enableImageLogos = usePreferencesStore(
|
||||
(state) => state.enableImageLogos,
|
||||
);
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return null;
|
||||
return new Date(dateString).getFullYear();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Title and Genres Row */}
|
||||
<div className="pb-2">
|
||||
{data.logoUrl && enableImageLogos ? (
|
||||
<img
|
||||
src={data.logoUrl}
|
||||
alt={data.title}
|
||||
className="max-w-[12rem] md:max-w-[20rem] object-contain drop-shadow-lg bg-transparent"
|
||||
style={{ background: "none" }}
|
||||
/>
|
||||
) : (
|
||||
<h3 className="text-2xl font-bold text-white z-[999]">
|
||||
{data.title}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* TMDB Rating and Year/Seasons */}
|
||||
<div className="flex items-center gap-2 text-sm text-white/80">
|
||||
{voteAverage && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon icon={Icons.RISING_STAR} className="text-yellow-400" />
|
||||
<span>{voteAverage.toFixed(1)}</span>
|
||||
{voteCount && (
|
||||
<span className="text-white/60">
|
||||
({voteCount.toLocaleString()})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{releaseDate && (
|
||||
<>
|
||||
<span className="text-white/60">•</span>
|
||||
<span>{formatDate(releaseDate)}</span>
|
||||
</>
|
||||
)}
|
||||
{seasons && (
|
||||
<>
|
||||
<span className="text-white/60">•</span>
|
||||
<span>
|
||||
{seasons} {t("details.seasons")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start mb-6 w-full">
|
||||
<div className="flex items-center gap-4 w-full sm:w-auto">
|
||||
<Button
|
||||
onClick={onPlayClick}
|
||||
theme="purple"
|
||||
className={classNames(
|
||||
"flex-1 sm:flex-initial sm:w-auto",
|
||||
"gap-2 h-12 rounded-lg px-4 py-2 my-1 transition-transform hover:scale-105 duration-100",
|
||||
"text-md text-white flex items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<Icon icon={Icons.PLAY} className="text-white" />
|
||||
<span className="text-white text-sm pr-1">
|
||||
{data.type === "movie"
|
||||
? !data.releaseDate || new Date(data.releaseDate) > new Date()
|
||||
? t("media.unreleased")
|
||||
: showProgress
|
||||
? t("details.resume")
|
||||
: t("details.play")
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
onClick={onPlayClick}
|
||||
theme="purple"
|
||||
className={classNames(
|
||||
"flex-1 sm:flex-initial sm:w-auto",
|
||||
"gap-2 h-12 rounded-lg px-4 py-2 my-1 transition-transform hover:scale-105 duration-100",
|
||||
"text-md text-white flex items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<Icon icon={Icons.PLAY} className="text-white" />
|
||||
<span className="text-white text-sm pr-1">
|
||||
{data.type === "movie"
|
||||
? !data.releaseDate || new Date(data.releaseDate) > new Date()
|
||||
? t("media.unreleased")
|
||||
: showProgress
|
||||
? t("details.resume")
|
||||
: t("details.play")}
|
||||
</span>
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTrailerClick}
|
||||
className="p-2 opacity-75 transition-opacity duration-300 hover:scale-110 hover:cursor-pointer hover:opacity-95"
|
||||
title={t("details.trailer")}
|
||||
>
|
||||
<IconPatch
|
||||
icon={Icons.FILM}
|
||||
className="transition-transform duration-300 hover:scale-110 hover:cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
<MediaBookmarkButton
|
||||
media={{
|
||||
id: data.id?.toString() || "",
|
||||
title: data.title,
|
||||
year: data.releaseDate
|
||||
? new Date(data.releaseDate).getFullYear()
|
||||
: undefined,
|
||||
poster: data.backdrop,
|
||||
type: data.type || "movie",
|
||||
}}
|
||||
: t("details.play")
|
||||
: showProgress
|
||||
? t("details.resume")
|
||||
: t("details.play")}
|
||||
</span>
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onTrailerClick}
|
||||
className="p-2 opacity-75 transition-opacity duration-300 hover:scale-110 hover:cursor-pointer hover:opacity-95"
|
||||
title={t("details.trailer")}
|
||||
>
|
||||
<IconPatch
|
||||
icon={Icons.FILM}
|
||||
className="transition-transform duration-300 hover:scale-110 hover:cursor-pointer"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onShareClick}
|
||||
className="p-2 opacity-75 transition-opacity duration-300 hover:scale-110 hover:cursor-pointer hover:opacity-95"
|
||||
title="Share"
|
||||
>
|
||||
<IconPatch
|
||||
icon={Icons.IOS_SHARE}
|
||||
className="transition-transform duration-300 hover:scale-110 hover:cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
<MediaBookmarkButton
|
||||
media={{
|
||||
id: data.id?.toString() || "",
|
||||
title: data.title,
|
||||
year: data.releaseDate
|
||||
? new Date(data.releaseDate).getFullYear()
|
||||
: undefined,
|
||||
poster: data.backdrop,
|
||||
type: data.type || "movie",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onShareClick}
|
||||
className="p-2 opacity-75 transition-opacity duration-300 hover:scale-110 hover:cursor-pointer hover:opacity-95"
|
||||
title="Share"
|
||||
>
|
||||
<IconPatch
|
||||
icon={Icons.IOS_SHARE}
|
||||
className="transition-transform duration-300 hover:scale-110 hover:cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Genres on the right side of the button row for larger screens */}
|
||||
{data.genres && data.genres.length > 0 && (
|
||||
<div className="hidden sm:flex flex-wrap gap-2 justify-end items-center">
|
||||
{data.genres.map((genre, index) => (
|
||||
<span
|
||||
key={genre.id}
|
||||
className="text-[11px] px-2 py-0.5 rounded-full bg-white/20 text-white/80 transition-all duration-300 hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||
style={{
|
||||
animationDelay: `${((data.genres?.length ?? 0) - 1 - index) * 60}ms`,
|
||||
transform: "scale(0)",
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
{genre.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Genres below for small screens */}
|
||||
{data.genres && data.genres.length > 0 && (
|
||||
<div className="flex sm:hidden flex-wrap gap-2 justify-start items-center mb-6 -mt-3">
|
||||
{data.genres.map((genre, index) => (
|
||||
<span
|
||||
key={genre.id}
|
||||
className="text-[11px] px-2 py-0.5 rounded-full bg-white/20 text-white/80 transition-all duration-300 hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||
style={{
|
||||
animationDelay: `${((data.genres?.length ?? 0) - 1 - index) * 60}ms`,
|
||||
transform: "scale(0)",
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
{genre.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,15 +72,15 @@ export function DetailsInfo({ data, imdbData, rtData }: DetailsInfoProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Ratings and External Links */}
|
||||
{/* External Ratings */}
|
||||
<DetailsRatings
|
||||
voteAverage={data.voteAverage}
|
||||
voteCount={data.voteCount}
|
||||
imdbData={imdbData}
|
||||
rtData={rtData}
|
||||
mediaId={data.id}
|
||||
mediaType={data.type}
|
||||
imdbId={data.imdbId}
|
||||
voteAverage={data.voteAverage}
|
||||
voteCount={data.voteCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -113,19 +113,22 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
}, [modal, data, isLoading]);
|
||||
|
||||
return (
|
||||
<OverlayPortal darken close={modal.hide} show={modal.isShown}>
|
||||
<OverlayPortal
|
||||
darken
|
||||
close={modal.hide}
|
||||
show={modal.isShown}
|
||||
durationClass="duration-400"
|
||||
>
|
||||
<Helmet>
|
||||
<html data-no-scroll />
|
||||
</Helmet>
|
||||
<div className="flex absolute inset-0 items-center justify-center">
|
||||
<div className="flex absolute inset-0 items-center justify-center pt-safe">
|
||||
<Flare.Base
|
||||
className={classNames(
|
||||
"group -m-[0.705em] rounded-3xl bg-background-main transition-colors duration-300 focus:relative focus:z-10",
|
||||
"group -m-[0.705em] rounded-3xl bg-background-main",
|
||||
"max-h-[900px] max-w-[1200px]",
|
||||
"bg-mediaCard-hoverBackground bg-opacity-60 backdrop-filter backdrop-blur-lg shadow-lg overflow-hidden",
|
||||
minimal
|
||||
? "h-[90%] md:h-[70%] lg:h-fit w-[90%] md:w-[70%] lg:w-[50%]"
|
||||
: "h-[90%] w-[90%] md:w-[70%] lg:w-[60%]",
|
||||
"h-[97%] w-[95%]",
|
||||
)}
|
||||
>
|
||||
<div className="transition-transform duration-300 h-full">
|
||||
|
|
@ -136,7 +139,7 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
className="rounded-3xl bg-background-main group-hover:opacity-100"
|
||||
/>
|
||||
<Flare.Child className="pointer-events-auto relative h-full overflow-y-auto scrollbar-none select-text">
|
||||
<div className="absolute right-4 top-4 z-10">
|
||||
<div className="absolute right-4 top-4 z-50">
|
||||
<button
|
||||
type="button"
|
||||
className="text-s font-semibold text-type-secondary hover:text-white transition-transform hover:scale-95 select-none"
|
||||
|
|
@ -145,7 +148,7 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
<IconPatch icon={Icons.X} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="pt-12 select-text">
|
||||
<div className="select-text">
|
||||
{isLoading || !detailsData ? (
|
||||
<DetailsSkeleton />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -31,34 +31,6 @@ export function DetailsRatings({
|
|||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{voteAverage !== undefined &&
|
||||
voteCount !== undefined &&
|
||||
voteCount > 0 && (
|
||||
<>
|
||||
<div className="flex items-center gap-1 text-white/80">
|
||||
<span className="font-medium">{t("details.rating")}</span>{" "}
|
||||
<span className="text-white/90">
|
||||
{imdbData?.imdb_rating
|
||||
? `${imdbData.imdb_rating.toFixed(1)}/10 (IMDb)`
|
||||
: `${voteAverage.toFixed(1)}/10 (TMDB)`}
|
||||
</span>
|
||||
</div>
|
||||
{/* Rating Progress Bar */}
|
||||
<div className="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${getRatingColor(imdbData?.imdb_rating || voteAverage)} transition-all duration-500`}
|
||||
style={{
|
||||
width: `${((imdbData?.imdb_rating || voteAverage) / 10) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white/60 text-[10px] text-right">
|
||||
{formatVoteCount(imdbData?.votes || voteCount)}{" "}
|
||||
{t("details.votes")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* External Links */}
|
||||
<div className="flex gap-3 mt-2">
|
||||
{mediaId && (
|
||||
|
|
@ -66,7 +38,12 @@ export function DetailsRatings({
|
|||
href={`https://www.themoviedb.org/${mediaType === "show" ? "tv" : "movie"}/${mediaId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 rounded-full bg-[#0d253f] flex items-center justify-center transition-transform hover:scale-110"
|
||||
className="w-8 h-8 rounded-full bg-[#0d253f] flex items-center justify-center transition-transform hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||
style={{
|
||||
animationDelay: "0ms",
|
||||
transform: "scale(0)",
|
||||
opacity: 0,
|
||||
}}
|
||||
title={t("details.tmdb")}
|
||||
>
|
||||
<Icon icon={Icons.TMDB} className="text-white" />
|
||||
|
|
@ -77,7 +54,12 @@ export function DetailsRatings({
|
|||
href={`https://www.imdb.com/title/${imdbId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-8 h-8 rounded-full bg-yellow-500 flex items-center justify-center transition-transform hover:scale-110"
|
||||
className="w-8 h-8 rounded-full bg-yellow-500 flex items-center justify-center transition-transform hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||
style={{
|
||||
animationDelay: "60ms",
|
||||
transform: "scale(0)",
|
||||
opacity: 0,
|
||||
}}
|
||||
title={t("details.imdb")}
|
||||
>
|
||||
<Icon icon={Icons.IMDB} className="text-black" />
|
||||
|
|
@ -86,7 +68,15 @@ export function DetailsRatings({
|
|||
{rtData && (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex flex-col items-center justify-center gap-1">
|
||||
<div className="flex items-center gap-1" title="Tomatometer">
|
||||
<div
|
||||
className="flex items-center gap-1 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||
style={{
|
||||
animationDelay: "120ms",
|
||||
transform: "scale(0)",
|
||||
opacity: 0,
|
||||
}}
|
||||
title="Tomatometer"
|
||||
>
|
||||
<img
|
||||
src={getRTIcon(rtData.tomatoIcon)}
|
||||
alt="Tomatometer"
|
||||
|
|
@ -100,6 +90,27 @@ export function DetailsRatings({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
{voteAverage !== undefined &&
|
||||
voteCount !== undefined &&
|
||||
voteCount > 0 && (
|
||||
<>
|
||||
{/* Rating Progress Bar */}
|
||||
<div className="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${getRatingColor(imdbData?.imdb_rating || voteAverage)} transition-all duration-500`}
|
||||
style={{
|
||||
width: `${((imdbData?.imdb_rating || voteAverage) / 10) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white/60 text-[10px] text-right">
|
||||
{formatVoteCount(imdbData?.votes || voteCount)}{" "}
|
||||
{t("details.votes")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { ShowProgressResult } from "@/stores/progress/utils";
|
||||
|
||||
export interface DetailsContent {
|
||||
title: string;
|
||||
overview?: string;
|
||||
|
|
@ -102,7 +104,11 @@ export interface DetailsHeaderProps {
|
|||
onPlayClick: () => void;
|
||||
onTrailerClick: () => void;
|
||||
onShareClick: () => void;
|
||||
showProgress?: any;
|
||||
showProgress: ShowProgressResult | null;
|
||||
voteAverage?: number;
|
||||
voteCount?: number;
|
||||
releaseDate?: string;
|
||||
seasons?: number;
|
||||
}
|
||||
|
||||
export interface DetailsInfoProps {
|
||||
|
|
|
|||
Loading…
Reference in a new issue