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