moviebox/src/components/organisms/Hero/index.tsx

247 lines
8.0 KiB
TypeScript

"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 Link from "next/link";
import { Button } from "@/components/atoms/Button";
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 () => {
addMovieToStore(currentMovie);
};
const handleRemove = async () => {
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">
<Link href={`/film/${id}`}>
<img
src={`http://image.tmdb.org/t/p/w500${poster_path}`}
alt={title}
className="w-64 h-96 object-cover rounded-lg shadow-2xl hover:scale-105 transition-transform duration-300"
/>
</Link>
</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 hover:text-secondary transition-colors duration-300">
<Link href={`/film/${id}`}>{title}</Link>
</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 */}
<p className="text-lg lg:text-xl text-white/90 mb-8 max-w-2xl leading-relaxed line-clamp-3 hover:text-secondary transition-all duration-300">
<Link href={`/film/${id}`}>{overview}</Link>
</p>
{/* Action buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
<Button
theme={alreadyInStore ? "primary" : "secondary"}
onClick={alreadyInStore ? handleRemove : handleAdd}
>
{alreadyInStore ? <FaMinus /> : <FaPlus />}
<span>
{alreadyInStore ? "Usuń z listy" : "Dodaj do listy"}
</span>
</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-white/50 hover:bg-secondary/70"
}`}
/>
))}
</div>
)}
</section>
);
};