feat: replace MovieGallery with new Gallery component for improved image handling; update styles and functionality in related components
This commit is contained in:
parent
f15137fd6c
commit
e891b37384
|
|
@ -1,7 +1,7 @@
|
||||||
import { MovieCard } from "@/components/atoms/MovieCard";
|
import { MovieCard } from "@/components/atoms/MovieCard";
|
||||||
import { ActorHero } from "@/components/molecules/ActorHero";
|
import { ActorHero } from "@/components/molecules/ActorHero";
|
||||||
import { Carousel } from "@/components/molecules/Carousel";
|
import { Carousel } from "@/components/molecules/Carousel";
|
||||||
import { MovieGallery } from "@/components/molecules/MovieGallery";
|
import { Gallery } from "@/components/molecules/Gallery";
|
||||||
import { convertToMovie } from "@/helpers/convertToMovie";
|
import { convertToMovie } from "@/helpers/convertToMovie";
|
||||||
import { TMDB } from "@/lib/tmdb";
|
import { TMDB } from "@/lib/tmdb";
|
||||||
import { FaStar } from "react-icons/fa";
|
import { FaStar } from "react-icons/fa";
|
||||||
|
|
@ -14,16 +14,11 @@ export default async function Page({
|
||||||
const actorId = Number((await params).id);
|
const actorId = Number((await params).id);
|
||||||
|
|
||||||
const personDetails = await TMDB.getPersonDetails(actorId);
|
const personDetails = await TMDB.getPersonDetails(actorId);
|
||||||
const images = {
|
|
||||||
backdrops: personDetails.images.profiles,
|
|
||||||
posters: [],
|
|
||||||
logos: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 pb-16">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 pb-16">
|
||||||
<ActorHero personDetails={personDetails} />
|
<ActorHero personDetails={personDetails} />
|
||||||
<MovieGallery images={images} movieTitle={personDetails.name} />
|
<Gallery images={personDetails.images.profiles} heading="Galeria" />
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<Carousel
|
<Carousel
|
||||||
heading={`Filmy z udziałem ${personDetails.name}`}
|
heading={`Filmy z udziałem ${personDetails.name}`}
|
||||||
|
|
|
||||||
|
|
@ -82,5 +82,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility grid-auto-cols-* {
|
@utility grid-auto-cols-* {
|
||||||
grid-template-columns: repeat(--value(integer), 1fr);
|
/* auto px size */
|
||||||
|
grid-template-columns: repeat(auto-fill, calc(--value(integer) * 1px));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
"use client";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import { FaImages } from "react-icons/fa";
|
||||||
|
import LightGallery from "lightgallery/react";
|
||||||
|
|
||||||
|
// import styles
|
||||||
|
import "lightgallery/css/lightgallery.css";
|
||||||
|
import "lightgallery/css/lg-zoom.css";
|
||||||
|
import "lightgallery/css/lg-thumbnail.css";
|
||||||
|
import { Button } from "@/components/atoms/Button";
|
||||||
|
|
||||||
|
type ImageData = {
|
||||||
|
aspect_ratio: number;
|
||||||
|
file_path: string;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
heading?: string;
|
||||||
|
images: ImageData[] | Record<string, ImageData[]>;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Gallery: FC<Props> = ({
|
||||||
|
images,
|
||||||
|
heading,
|
||||||
|
limit: initialLimit = 14,
|
||||||
|
}) => {
|
||||||
|
const categories = Array.isArray(images) ? [] : Object.keys(images);
|
||||||
|
const [limit, setLimit] = useState(initialLimit);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(
|
||||||
|
categories[0] || null
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentImages: ImageData[] =
|
||||||
|
selectedCategory && typeof images === "object"
|
||||||
|
? (images[selectedCategory as keyof typeof images] as ImageData[])
|
||||||
|
: (images as ImageData[]);
|
||||||
|
|
||||||
|
const getImageUrl = (path: string, size: string = "w500") => {
|
||||||
|
return `https://image.tmdb.org/t/p/${size}${path}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-16">
|
||||||
|
<div className="px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{heading && (
|
||||||
|
<div className="flex items-center gap-3 mb-8">
|
||||||
|
<div className="p-2 rounded-lg bg-gradient-to-r from-purple-500 to-pink-500">
|
||||||
|
<FaImages className="text-white" size={20} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
||||||
|
{heading}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Category tabs */}
|
||||||
|
{categories.length > 0 && (
|
||||||
|
<div className="flex gap-4 mb-8">
|
||||||
|
{Object.entries(images).map(([category, categoryImages]) => {
|
||||||
|
if (!categoryImages.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
onClick={() =>
|
||||||
|
setSelectedCategory(category as keyof typeof images)
|
||||||
|
}
|
||||||
|
className={`px-6 py-3 rounded-xl font-medium transition-all duration-300 ${
|
||||||
|
selectedCategory === category
|
||||||
|
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg"
|
||||||
|
: "bg-white/10 text-gray-300 hover:bg-white/20 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category} ({categoryImages.length})
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image grid */}
|
||||||
|
<div className={`grid gap-4 grid-auto-cols-160 [&>div]:contents`}>
|
||||||
|
<LightGallery>
|
||||||
|
{currentImages.slice(0, limit).map((image, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={getImageUrl(image.file_path)}
|
||||||
|
className="group relative overflow-hidden rounded-xl cursor-pointer bg-slate-800"
|
||||||
|
>
|
||||||
|
<img src={getImageUrl(image.file_path, "w185")} />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</LightGallery>
|
||||||
|
</div>
|
||||||
|
{limit < currentImages.length && (
|
||||||
|
<div className="flex justify-center mt-6">
|
||||||
|
<Button
|
||||||
|
theme="teal"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setLimit(currentImages.length)}
|
||||||
|
>
|
||||||
|
Pokaż wszystkie ({currentImages.length - limit})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -10,11 +10,11 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||||
const [limitCast, setLimitCast] = useState(8);
|
const [limit, setLimit] = useState(8);
|
||||||
const director = movieDetails?.credits.crew.find(
|
const director = movieDetails?.credits.crew.find(
|
||||||
(member) => member.job === "Director"
|
(member) => member.job === "Director"
|
||||||
);
|
);
|
||||||
const mainCast = movieDetails?.credits.cast.slice(0, limitCast) || [];
|
const mainCast = movieDetails?.credits.cast.slice(0, limit) || [];
|
||||||
|
|
||||||
const formatCurrency = (amount: number) =>
|
const formatCurrency = (amount: number) =>
|
||||||
new Intl.NumberFormat("pl-PL", {
|
new Intl.NumberFormat("pl-PL", {
|
||||||
|
|
@ -67,18 +67,18 @@ export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{limitCast < movieDetails.credits.cast.length && (
|
{limit < movieDetails.credits.cast.length && (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
theme="teal"
|
theme="teal"
|
||||||
size="small"
|
size="small"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLimitCast(movieDetails.credits.cast.length);
|
setLimit(movieDetails.credits.cast.length);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Pokaż wszystkich (
|
Pokaż wszystkich ({movieDetails.credits.cast.length - limit}
|
||||||
{movieDetails.credits.cast.length - limitCast})
|
)
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
|
||||||
import {
|
|
||||||
FaImages,
|
|
||||||
FaTimes,
|
|
||||||
FaChevronLeft,
|
|
||||||
FaChevronRight,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
|
|
||||||
type ImageData = {
|
|
||||||
aspect_ratio: number;
|
|
||||||
file_path: string;
|
|
||||||
height: number;
|
|
||||||
width: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
images: {
|
|
||||||
backdrops: ImageData[];
|
|
||||||
logos: ImageData[];
|
|
||||||
posters: ImageData[];
|
|
||||||
};
|
|
||||||
movieTitle: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MovieGallery: FC<Props> = ({ images, movieTitle }) => {
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<
|
|
||||||
"backdrops" | "posters" | "logos"
|
|
||||||
>("backdrops");
|
|
||||||
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
||||||
const [lightboxIndex, setLightboxIndex] = useState(0);
|
|
||||||
|
|
||||||
const allImages = {
|
|
||||||
backdrops: images.backdrops.slice(0, 12), // Limit to first 12 for performance.
|
|
||||||
posters: images.posters.slice(0, 12),
|
|
||||||
logos: images.logos.slice(0, 8),
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentImages = allImages[selectedCategory];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!images.backdrops.length &&
|
|
||||||
!images.posters.length &&
|
|
||||||
!images.logos.length
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const openLightbox = (index: number) => {
|
|
||||||
setLightboxIndex(index);
|
|
||||||
setLightboxOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeLightbox = () => {
|
|
||||||
setLightboxOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const nextImage = () => {
|
|
||||||
setLightboxIndex((prev) => (prev + 1) % currentImages.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevImage = () => {
|
|
||||||
setLightboxIndex(
|
|
||||||
(prev) => (prev - 1 + currentImages.length) % currentImages.length
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getImageUrl = (path: string, size: string = "w500") => {
|
|
||||||
return `https://image.tmdb.org/t/p/${size}${path}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="py-16">
|
|
||||||
<div className="px-6 lg:px-8">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<div className="flex items-center gap-3 mb-8">
|
|
||||||
<div className="p-2 rounded-lg bg-gradient-to-r from-purple-500 to-pink-500">
|
|
||||||
<FaImages className="text-white" size={20} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent">
|
|
||||||
Galeria
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category tabs */}
|
|
||||||
<div className="flex gap-4 mb-8">
|
|
||||||
{Object.entries(allImages).map(([category, categoryImages]) => {
|
|
||||||
if (!categoryImages.length) return null;
|
|
||||||
|
|
||||||
const labels = {
|
|
||||||
backdrops: "Kadry",
|
|
||||||
posters: "Plakaty",
|
|
||||||
logos: "Loga",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={category}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedCategory(category as keyof typeof allImages)
|
|
||||||
}
|
|
||||||
className={`px-6 py-3 rounded-xl font-medium transition-all duration-300 ${
|
|
||||||
selectedCategory === category
|
|
||||||
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg"
|
|
||||||
: "bg-white/10 text-gray-300 hover:bg-white/20 hover:text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{labels[category as keyof typeof labels]} (
|
|
||||||
{categoryImages.length})
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image grid */}
|
|
||||||
<div
|
|
||||||
className={`grid gap-4 ${
|
|
||||||
selectedCategory === "backdrops"
|
|
||||||
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
|
||||||
: selectedCategory === "posters"
|
|
||||||
? "grid-cols-2 md:grid-cols-4 lg:grid-cols-6"
|
|
||||||
: "grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{currentImages.map((image, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="group relative overflow-hidden rounded-xl cursor-pointer bg-slate-800"
|
|
||||||
onClick={() => openLightbox(index)}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={getImageUrl(
|
|
||||||
image.file_path,
|
|
||||||
selectedCategory === "backdrops" ? "w780" : "w342"
|
|
||||||
)}
|
|
||||||
alt={`${movieTitle} - ${selectedCategory}`}
|
|
||||||
className="w-full h-full object-cover transition-all duration-500 group-hover:scale-110"
|
|
||||||
style={{
|
|
||||||
aspectRatio:
|
|
||||||
selectedCategory === "backdrops"
|
|
||||||
? "16/9"
|
|
||||||
: selectedCategory === "posters"
|
|
||||||
? "2/3"
|
|
||||||
: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
||||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
||||||
<div className="p-3 rounded-full bg-white/20 backdrop-blur-sm">
|
|
||||||
<FaImages className="text-white" size={20} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lightbox */}
|
|
||||||
{lightboxOpen && (
|
|
||||||
<div className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4">
|
|
||||||
<div className="relative max-w-6xl max-h-full">
|
|
||||||
{/* Close button */}
|
|
||||||
<button
|
|
||||||
onClick={closeLightbox}
|
|
||||||
className="absolute top-4 right-4 z-10 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors"
|
|
||||||
>
|
|
||||||
<FaTimes size={20} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Navigation buttons */}
|
|
||||||
{currentImages.length > 1 && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={prevImage}
|
|
||||||
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 p-3 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors"
|
|
||||||
>
|
|
||||||
<FaChevronLeft size={20} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={nextImage}
|
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 p-3 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors"
|
|
||||||
>
|
|
||||||
<FaChevronRight size={20} />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Image */}
|
|
||||||
<img
|
|
||||||
src={getImageUrl(
|
|
||||||
currentImages[lightboxIndex].file_path,
|
|
||||||
"original"
|
|
||||||
)}
|
|
||||||
alt={`${movieTitle} - ${selectedCategory}`}
|
|
||||||
className="max-w-full max-h-full object-contain rounded-lg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Image counter */}
|
|
||||||
{currentImages.length > 1 && (
|
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-full bg-black/50 text-white text-sm">
|
|
||||||
{lightboxIndex + 1} / {currentImages.length}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Background click to close */}
|
|
||||||
<div className="absolute inset-0 -z-10" onClick={closeLightbox} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue