Add Hero component for movie carousel: implement auto-rotation, navigation controls, and movie details display, including add/remove functionality for user favorites.
This commit is contained in:
parent
25e5b90ee8
commit
a230a4cf45
|
|
@ -0,0 +1,248 @@
|
||||||
|
"use client";
|
||||||
|
import { FC, useState, useEffect, useCallback } from "react";
|
||||||
|
import { Movie } from "@/types/global";
|
||||||
|
import {
|
||||||
|
FaPlus,
|
||||||
|
FaFire,
|
||||||
|
FaChevronLeft,
|
||||||
|
FaChevronRight,
|
||||||
|
FaMinus,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
|
||||||
|
import { useGlobalStore } from "@/app/store/globalStore";
|
||||||
|
import { addMovie, deleteMovie } from "@/lib/db";
|
||||||
|
import { ReadMore } from "@/components/atoms/ReadMore";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
movies: Movie[];
|
||||||
|
preheading?: string;
|
||||||
|
autoRotate?: boolean;
|
||||||
|
rotateInterval?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Hero: FC<Props> = ({
|
||||||
|
movies,
|
||||||
|
preheading,
|
||||||
|
autoRotate = true,
|
||||||
|
rotateInterval = 10000,
|
||||||
|
}) => {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
movies: storedMovies,
|
||||||
|
addMovie: addMovieToStore,
|
||||||
|
deleteMovie: deleteMovieInStore,
|
||||||
|
} = useGlobalStore();
|
||||||
|
|
||||||
|
const currentMovie = movies[currentIndex];
|
||||||
|
|
||||||
|
if (!currentMovie) return null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
overview,
|
||||||
|
backdrop_path,
|
||||||
|
poster_path,
|
||||||
|
release_date,
|
||||||
|
popularity,
|
||||||
|
vote_average,
|
||||||
|
} = currentMovie;
|
||||||
|
|
||||||
|
const alreadyInStore = storedMovies.find((m) => m.id === id);
|
||||||
|
const isReleased = new Date(release_date) < new Date();
|
||||||
|
const releaseDate = new Date(release_date);
|
||||||
|
|
||||||
|
const nextSlide = useCallback(() => {
|
||||||
|
if (isTransitioning) return;
|
||||||
|
setIsTransitioning(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % movies.length);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}, 500);
|
||||||
|
}, [movies.length, isTransitioning]);
|
||||||
|
|
||||||
|
const prevSlide = useCallback(() => {
|
||||||
|
if (isTransitioning) return;
|
||||||
|
setIsTransitioning(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentIndex((prev) => (prev - 1 + movies.length) % movies.length);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}, 500);
|
||||||
|
}, [movies.length, isTransitioning]);
|
||||||
|
|
||||||
|
const goToSlide = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (isTransitioning || index === currentIndex) return;
|
||||||
|
setIsTransitioning(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCurrentIndex(index);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
[currentIndex, isTransitioning]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-rotate functionality.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRotate || movies.length <= 1) return;
|
||||||
|
|
||||||
|
const interval = setInterval(nextSlide, rotateInterval);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [autoRotate, rotateInterval, nextSlide, movies.length]);
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
|
await addMovie(currentMovie);
|
||||||
|
addMovieToStore(currentMovie);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
await deleteMovie(id);
|
||||||
|
deleteMovieInStore(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative min-h-[70vh] flex items-center overflow-hidden pt-10 pb-20">
|
||||||
|
{/* Background carousel */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
{movies.map((movie, index) => (
|
||||||
|
<div
|
||||||
|
key={movie.id}
|
||||||
|
className={`absolute inset-0 transition-opacity duration-500 ${
|
||||||
|
index === currentIndex ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`http://image.tmdb.org/t/p/w1280${backdrop_path}`}
|
||||||
|
alt={movie.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-black/80 via-black/50 to-transparent" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-black/30" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation arrows */}
|
||||||
|
{movies.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={prevSlide}
|
||||||
|
disabled={isTransitioning}
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 z-20 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<FaChevronLeft size={20} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextSlide}
|
||||||
|
disabled={isTransitioning}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 z-20 p-3 bg-black/50 hover:bg-black/70 text-white rounded-full transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<FaChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content with fade transitions */}
|
||||||
|
<div className="container relative z-10">
|
||||||
|
<div
|
||||||
|
className={`flex flex-col lg:flex-row items-center gap-8 lg:gap-12 transition-opacity duration-500 ${
|
||||||
|
isTransitioning ? "opacity-0" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Poster */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<img
|
||||||
|
src={`http://image.tmdb.org/t/p/w500${poster_path}`}
|
||||||
|
alt={title}
|
||||||
|
className="w-64 h-96 object-cover rounded-lg shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Movie details */}
|
||||||
|
<div className="flex-1 text-center lg:text-left">
|
||||||
|
{preheading && (
|
||||||
|
<h3 className="font-bold text-white leading-tight mb-2">
|
||||||
|
{preheading}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
<h2 className="text-4xl lg:text-6xl font-bold text-white mb-4 leading-tight">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Movie meta info */}
|
||||||
|
<div className="flex flex-wrap items-center justify-center lg:justify-start gap-4 mb-6 text-white/80">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`flex items-center gap-1 text-sm ${
|
||||||
|
isReleased ? "text-green-400" : "text-yellow-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isReleased ? (
|
||||||
|
<RiCalendarCheckLine />
|
||||||
|
) : (
|
||||||
|
<RiCalendarScheduleLine />
|
||||||
|
)}
|
||||||
|
{releaseDate.toLocaleDateString("pl-PL", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 text-sm">
|
||||||
|
<FaFire className="text-orange-400" />
|
||||||
|
<span>{popularity.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 text-sm">
|
||||||
|
<span className="text-yellow-400">★</span>
|
||||||
|
<span>{vote_average.toFixed(1)}/10</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overview */}
|
||||||
|
<div className="text-lg lg:text-xl text-white/90 mb-8 max-w-2xl leading-relaxed">
|
||||||
|
<ReadMore text={overview} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||||
|
<button
|
||||||
|
onClick={alreadyInStore ? handleRemove : handleAdd}
|
||||||
|
className={`flex items-center justify-center gap-3 px-8 py-3 rounded-lg font-semibold text-lg text-white transition-colors cursor-pointer ${
|
||||||
|
alreadyInStore
|
||||||
|
? "bg-red-500 hover:bg-red-600"
|
||||||
|
: "bg-primary hover:bg-primary/80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{alreadyInStore ? <FaMinus /> : <FaPlus />}
|
||||||
|
{alreadyInStore ? "Usuń z listy" : "Dodaj do listy"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dot indicators */}
|
||||||
|
{movies.length > 1 && (
|
||||||
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20 flex gap-2">
|
||||||
|
{movies.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => goToSlide(index)}
|
||||||
|
disabled={isTransitioning}
|
||||||
|
className={`w-3 h-3 rounded-full transition-all duration-300 disabled:cursor-not-allowed cursor-pointer ${
|
||||||
|
index === currentIndex
|
||||||
|
? "bg-secondary scale-125"
|
||||||
|
: "bg-secondary/50 hover:bg-secondary/70"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue