add network images
BIN
public/platforms/appletv.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/platforms/disney.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/platforms/hulu.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
public/platforms/max.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/platforms/netflix.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/platforms/paramount.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/platforms/prime.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -35,6 +35,12 @@ export interface TraktDiscoverResponse {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TraktNetworkResponse {
|
||||||
|
type: string;
|
||||||
|
platforms: string[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Pagination utility
|
// Pagination utility
|
||||||
export function paginateResults(
|
export function paginateResults(
|
||||||
results: TraktLatestResponse,
|
results: TraktLatestResponse,
|
||||||
|
|
@ -106,6 +112,10 @@ export const getPopularMovies = () => fetchFromTrakt("/popularmovies");
|
||||||
export const getDiscoverContent = () =>
|
export const getDiscoverContent = () =>
|
||||||
fetchFromTrakt<TraktDiscoverResponse>("/discover");
|
fetchFromTrakt<TraktDiscoverResponse>("/discover");
|
||||||
|
|
||||||
|
// Network content
|
||||||
|
export const getNetworkContent = (tmdbId: string) =>
|
||||||
|
fetchFromTrakt<TraktNetworkResponse>(`/network/${tmdbId}`);
|
||||||
|
|
||||||
// Type conversion utilities
|
// Type conversion utilities
|
||||||
export function convertToMediaType(type: TraktContentType): MWMediaType {
|
export function convertToMediaType(type: TraktContentType): MWMediaType {
|
||||||
return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES;
|
return type === "movie" ? MWMediaType.MOVIE : MWMediaType.SERIES;
|
||||||
|
|
@ -130,3 +140,14 @@ export const GENRE_TO_TRAKT_MAP = {
|
||||||
"28": "action", // Action
|
"28": "action", // Action
|
||||||
"18": "drama", // Drama
|
"18": "drama", // Drama
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// Map provider names to their image filenames
|
||||||
|
export const PROVIDER_TO_IMAGE_MAP: Record<string, string> = {
|
||||||
|
Max: "max",
|
||||||
|
"Prime Video": "prime",
|
||||||
|
Netflix: "netflix",
|
||||||
|
"Disney+": "disney",
|
||||||
|
Hulu: "hulu",
|
||||||
|
"Apple TV+": "appletv",
|
||||||
|
"Paramount+": "paramount",
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { t } from "i18next";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useCopyToClipboard } from "react-use";
|
import { useCopyToClipboard } from "react-use";
|
||||||
|
|
||||||
|
import { getNetworkContent } from "@/backend/metadata/traktApi";
|
||||||
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useLanguageStore } from "@/stores/language";
|
import { useLanguageStore } from "@/stores/language";
|
||||||
|
|
@ -22,6 +23,9 @@ import { DetailsContentProps } from "./types";
|
||||||
export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
||||||
const [imdbData, setImdbData] = useState<any>(null);
|
const [imdbData, setImdbData] = useState<any>(null);
|
||||||
const [rtData, setRtData] = useState<any>(null);
|
const [rtData, setRtData] = useState<any>(null);
|
||||||
|
const [providerData, setProviderData] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
const [, setIsLoadingImdb] = useState(false);
|
const [, setIsLoadingImdb] = useState(false);
|
||||||
const [showTrailer, setShowTrailer] = useState(false);
|
const [showTrailer, setShowTrailer] = useState(false);
|
||||||
const [selectedSeason, setSelectedSeason] = useState<number>(1);
|
const [selectedSeason, setSelectedSeason] = useState<number>(1);
|
||||||
|
|
@ -62,6 +66,30 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchNetworkData = async () => {
|
||||||
|
if (!data.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const networkData = await getNetworkContent(data.id.toString());
|
||||||
|
if (
|
||||||
|
networkData &&
|
||||||
|
networkData.platforms &&
|
||||||
|
networkData.platforms.length > 0
|
||||||
|
) {
|
||||||
|
setProviderData(networkData.platforms[0]);
|
||||||
|
} else {
|
||||||
|
setProviderData(undefined);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch network data:", error);
|
||||||
|
setProviderData(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchNetworkData();
|
||||||
|
}, [data.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchExternalData = async () => {
|
const fetchExternalData = async () => {
|
||||||
if (!data.imdbId) return;
|
if (!data.imdbId) return;
|
||||||
|
|
@ -273,7 +301,12 @@ export function DetailsContent({ data, minimal = false }: DetailsContentProps) {
|
||||||
|
|
||||||
{/* Right Column - Details Info (1/3) */}
|
{/* Right Column - Details Info (1/3) */}
|
||||||
<div className="md:col-span-1">
|
<div className="md:col-span-1">
|
||||||
<DetailsInfo data={data} imdbData={imdbData} rtData={rtData} />
|
<DetailsInfo
|
||||||
|
data={data}
|
||||||
|
imdbData={imdbData}
|
||||||
|
rtData={rtData}
|
||||||
|
provider={providerData}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ import { Trans } from "react-i18next";
|
||||||
import { DetailsRatings } from "./DetailsRatings";
|
import { DetailsRatings } from "./DetailsRatings";
|
||||||
import { DetailsInfoProps } from "./types";
|
import { DetailsInfoProps } from "./types";
|
||||||
|
|
||||||
export function DetailsInfo({ data, imdbData, rtData }: DetailsInfoProps) {
|
export function DetailsInfo({
|
||||||
|
data,
|
||||||
|
imdbData,
|
||||||
|
rtData,
|
||||||
|
provider,
|
||||||
|
}: DetailsInfoProps) {
|
||||||
const [isShiftPressed, setIsShiftPressed] = useState(false);
|
const [isShiftPressed, setIsShiftPressed] = useState(false);
|
||||||
const [showCopied, setShowCopied] = useState(false);
|
const [showCopied, setShowCopied] = useState(false);
|
||||||
|
|
||||||
|
|
@ -122,6 +127,7 @@ export function DetailsInfo({ data, imdbData, rtData }: DetailsInfoProps) {
|
||||||
imdbId={data.imdbId}
|
imdbId={data.imdbId}
|
||||||
voteAverage={data.voteAverage}
|
voteAverage={data.voteAverage}
|
||||||
voteCount={data.voteCount}
|
voteCount={data.voteCount}
|
||||||
|
provider={provider}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
|
||||||
|
import { PROVIDER_TO_IMAGE_MAP } from "@/backend/metadata/traktApi";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { getRTIcon } from "@/utils/rottenTomatoesScraper";
|
import { getRTIcon } from "@/utils/rottenTomatoesScraper";
|
||||||
|
|
||||||
|
|
@ -10,19 +11,44 @@ export function DetailsRatings({
|
||||||
mediaId,
|
mediaId,
|
||||||
mediaType,
|
mediaType,
|
||||||
imdbId,
|
imdbId,
|
||||||
|
provider,
|
||||||
}: DetailsRatingsProps) {
|
}: DetailsRatingsProps) {
|
||||||
|
const getProviderImage = (providerName: string) => {
|
||||||
|
const imageKey =
|
||||||
|
PROVIDER_TO_IMAGE_MAP[providerName] ||
|
||||||
|
providerName.toLowerCase().replace(/\s+/g, "");
|
||||||
|
return `/platforms/${imageKey}.png`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{/* External Links */}
|
{/* External Links */}
|
||||||
<div className="flex gap-3 mt-2">
|
<div className="flex gap-3 mt-2">
|
||||||
|
{provider && (
|
||||||
|
<div
|
||||||
|
className="w-8 h-8 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={provider}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={getProviderImage(provider)}
|
||||||
|
alt={provider}
|
||||||
|
className="w-8 h-8 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{mediaId && (
|
{mediaId && (
|
||||||
<a
|
<a
|
||||||
href={`https://www.themoviedb.org/${mediaType === "show" ? "tv" : "movie"}/${mediaId}`}
|
href={`https://www.themoviedb.org/${mediaType === "show" ? "tv" : "movie"}/${mediaId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
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]"
|
className="w-8 h-8 rounded-md bg-[#0d253f] flex items-center justify-center transition-transform hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: "0ms",
|
animationDelay: "60ms",
|
||||||
transform: "scale(0)",
|
transform: "scale(0)",
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
}}
|
}}
|
||||||
|
|
@ -36,9 +62,9 @@ export function DetailsRatings({
|
||||||
href={`https://www.imdb.com/title/${imdbId}`}
|
href={`https://www.imdb.com/title/${imdbId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
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]"
|
className="w-8 h-8 rounded-md bg-yellow-500 flex items-center justify-center transition-transform hover:scale-110 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: "60ms",
|
animationDelay: "120ms",
|
||||||
transform: "scale(0)",
|
transform: "scale(0)",
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
}}
|
}}
|
||||||
|
|
@ -53,7 +79,7 @@ export function DetailsRatings({
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 animate-[scaleIn_0.6s_ease-out_forwards]"
|
className="flex items-center gap-1 animate-[scaleIn_0.6s_ease-out_forwards]"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: "120ms",
|
animationDelay: "180ms",
|
||||||
transform: "scale(0)",
|
transform: "scale(0)",
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export interface DetailsInfoProps {
|
||||||
data: DetailsContent;
|
data: DetailsContent;
|
||||||
imdbData?: any;
|
imdbData?: any;
|
||||||
rtData?: any;
|
rtData?: any;
|
||||||
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DetailsRatingsProps {
|
export interface DetailsRatingsProps {
|
||||||
|
|
@ -130,4 +131,5 @@ export interface DetailsRatingsProps {
|
||||||
mediaId?: number;
|
mediaId?: number;
|
||||||
mediaType?: "movie" | "show";
|
mediaType?: "movie" | "show";
|
||||||
imdbId?: string;
|
imdbId?: string;
|
||||||
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||