feat: manage homepage banner carousel
This commit is contained in:
@@ -1,86 +1,24 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Play, Plus, Sparkles } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Play, Sparkles } from 'lucide-react';
|
||||
import { Course } from '../types';
|
||||
import { getCourses } from '../services/db';
|
||||
import { getCourses, getHomeBanners, PublicHomeBanner } from '../services/db';
|
||||
import { CourseCoverImage } from './CourseCoverImage';
|
||||
|
||||
interface HeroProps {
|
||||
onPlay?: (course: Course) => void;
|
||||
}
|
||||
interface HeroProps { onPlay?: (course: Course) => void; }
|
||||
|
||||
export const Hero: React.FC<HeroProps> = ({ onPlay }) => {
|
||||
const [featuredCourse, setFeaturedCourse] = useState<Course | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getCourses().then((courses) => setFeaturedCourse(courses[0] || null)).catch(() => setFeaturedCourse(null));
|
||||
}, []);
|
||||
|
||||
if (!featuredCourse) return null;
|
||||
|
||||
return (
|
||||
<div className="relative w-full min-h-[75vh] lg:min-h-[82vh] max-h-[800px] h-auto overflow-hidden group flex flex-col justify-end pt-28 pb-12 sm:pb-16 px-6 md:px-12">
|
||||
{/* Background Image with Cinematic Gradient Overlay */}
|
||||
<div className="absolute inset-0 transition-transform duration-[20s] ease-in-out group-hover:scale-105">
|
||||
<CourseCoverImage
|
||||
source={featuredCourse.thumbnail}
|
||||
alt={featuredCourse.title}
|
||||
className="w-full h-full object-cover object-center"
|
||||
style={{
|
||||
transform: `scale(${featuredCourse.coverImageZoom || 1})`,
|
||||
objectPosition: `${featuredCourse.coverImagePositionX ?? 50}% ${featuredCourse.coverImagePositionY ?? 50}%`,
|
||||
}}
|
||||
/>
|
||||
{/* Gradients for text readability and seamless transition */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/60 to-black/75" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black/95 via-black/65 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 max-w-[1440px] w-full mx-auto">
|
||||
<div className="max-w-3xl lg:max-w-4xl animate-fade-in-up flex flex-col items-start">
|
||||
|
||||
{/* Metadata Badges / Tags */}
|
||||
<div className="flex items-center gap-2.5 text-xs font-semibold uppercase tracking-wider text-white/90 mb-3.5">
|
||||
<span className="bg-orange-500 text-white px-3.5 py-1 rounded-[980px] shadow-xl flex items-center gap-1">
|
||||
<Sparkles className="w-3 h-3 fill-current" />
|
||||
Destaque
|
||||
</span>
|
||||
<span className="text-gray-300">{featuredCourse.category}</span>
|
||||
<span className="text-gray-500">•</span>
|
||||
<span className="text-gray-300">{featuredCourse.duration}</span>
|
||||
</div>
|
||||
|
||||
{/* Main Display Headline */}
|
||||
<h1 className="font-display text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-extrabold text-white tracking-tight leading-tight drop-shadow-2xl mb-4">
|
||||
{featuredCourse.title}
|
||||
</h1>
|
||||
|
||||
{/* Subhead / Lead Description */}
|
||||
<p className="text-sm sm:text-base md:text-lg font-normal text-gray-200 line-clamp-2 sm:line-clamp-3 max-w-2xl leading-relaxed opacity-90 mb-6 sm:mb-8">
|
||||
{featuredCourse.description}
|
||||
</p>
|
||||
|
||||
{/* Action Buttons with comfortable width, borders, and spacing */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* Primary CTA */}
|
||||
<button
|
||||
onClick={() => onPlay?.(featuredCourse)}
|
||||
className="flex items-center justify-center gap-3 bg-white text-black px-8 sm:px-10 py-3.5 sm:py-4 rounded-[980px] text-sm sm:text-base font-bold hover:bg-gray-100 active:scale-95 transition-all duration-200 shadow-xl min-w-[200px]"
|
||||
>
|
||||
<Play className="w-4 h-4 sm:w-5 sm:h-5 fill-current" />
|
||||
<span>Assistir Aula 1</span>
|
||||
</button>
|
||||
|
||||
{/* Add to List */}
|
||||
<button
|
||||
className="flex items-center justify-center w-12 h-12 sm:w-14 sm:h-14 rounded-[980px] bg-white/10 hover:bg-white/20 border border-white/20 backdrop-blur-xl text-white transition-all duration-200 active:scale-95 group shadow-xl shrink-0"
|
||||
title="Adicionar aos Favoritos"
|
||||
>
|
||||
<Plus className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const [banners, setBanners] = useState<PublicHomeBanner[]>([]);
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
const [index, setIndex] = useState(0);
|
||||
useEffect(() => { Promise.all([getHomeBanners(), getCourses()]).then(([loadedBanners, loadedCourses]) => { setBanners(loadedBanners); setCourses(loadedCourses); }).catch(() => setBanners([])); }, []);
|
||||
useEffect(() => { if (banners.length < 2) return; const timer = window.setInterval(() => setIndex((current) => (current + 1) % banners.length), 7000); return () => window.clearInterval(timer); }, [banners.length]);
|
||||
const fallback = courses[0];
|
||||
const banner = banners[index];
|
||||
const course = banner?.courseId ? courses.find((item) => item.id === banner.courseId) : fallback;
|
||||
if (!banner && !fallback) return null;
|
||||
const title = banner ? (banner.kind === 'course' ? banner.courseTitle || banner.title : banner.title) : fallback.title;
|
||||
const description = banner ? (banner.kind === 'course' ? banner.courseDescription || banner.description : banner.description) : fallback.description;
|
||||
const image = banner ? (banner.kind === 'course' ? banner.courseImageUrl : banner.imageUrl) : fallback.thumbnail;
|
||||
const canPlay = Boolean(course && (banner?.kind !== 'custom' || banner.courseId));
|
||||
return <div className="relative flex h-[75vh] max-h-[800px] min-h-[560px] w-full flex-col justify-end overflow-hidden px-6 pb-12 pt-28 md:px-12"><div className="absolute inset-0">{image && <CourseCoverImage source={image} alt={title} className="h-full w-full object-cover" style={{ transform: `scale(${banner?.coverImageZoom || course?.coverImageZoom || 1})`, objectPosition: `${banner?.coverImagePositionX ?? course?.coverImagePositionX ?? 50}% ${banner?.coverImagePositionY ?? course?.coverImagePositionY ?? 50}%` }} />}<div className="absolute inset-0 bg-gradient-to-t from-black via-black/60 to-black/75" /><div className="absolute inset-0 bg-gradient-to-r from-black/95 via-black/60 to-transparent" /></div><div className="relative z-10 mx-auto w-full max-w-[1440px]"><div className="max-w-3xl"><div className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-white/90"><span className="flex items-center gap-1 rounded-full bg-orange-500 px-3 py-1"><Sparkles className="h-3 w-3" /> Destaque</span>{banner?.kind === 'course' && <span>{course?.category}</span>}</div><h1 className="font-display text-4xl font-extrabold leading-tight tracking-tight text-white md:text-6xl">{title}</h1>{description && <p className="mt-4 max-w-2xl text-base leading-relaxed text-gray-200 md:text-lg">{description}</p>}{canPlay && <button onClick={() => onPlay?.(course!)} className="mt-7 inline-flex items-center gap-3 rounded-full bg-white px-8 py-3.5 text-sm font-bold text-black"><Play className="h-5 w-5 fill-current" /> Assistir curso</button>}</div></div>{banners.length > 1 && <div className="absolute bottom-7 right-6 z-20 flex items-center gap-3 md:right-12"><button onClick={() => setIndex((index - 1 + banners.length) % banners.length)} className="rounded-full bg-black/50 p-2 text-white"><ChevronLeft className="h-5 w-5" /></button><div className="flex gap-1.5">{banners.map((item, itemIndex) => <button key={item.id} onClick={() => setIndex(itemIndex)} className={`h-2 rounded-full transition-all ${itemIndex === index ? 'w-6 bg-orange-500' : 'w-2 bg-white/50'}`} aria-label={`Banner ${itemIndex + 1}`} />)}</div><button onClick={() => setIndex((index + 1) % banners.length)} className="rounded-full bg-black/50 p-2 text-white"><ChevronRight className="h-5 w-5" /></button></div>}</div>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user