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:
Norbert Maciaszek 2025-08-15 16:18:47 +02:00
parent 25e5b90ee8
commit a230a4cf45
1 changed files with 248 additions and 0 deletions

View File

@ -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>
);
};