feat: corrigir botoes hero banner e mudar visual dos cards para retrato com transicao horizontal no hover/selecao
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s Details

This commit is contained in:
Sidney Gomes 2026-07-24 23:11:51 -03:00
parent 43643d5930
commit c44ac79a35
4 changed files with 116 additions and 86 deletions

View File

@ -290,7 +290,18 @@ export default function App() {
{/* Hero Banner (Featured last course or first catalogue course) */} {/* Hero Banner (Featured last course or first catalogue course) */}
<HeroBanner <HeroBanner
course={courses.length > 0 ? courses[0] : null} course={courses.length > 0 ? courses[0] : null}
onPlay={handleSelectCourse} onPlay={(courseId) => {
const selected = courses.find(c => c.id === courseId);
if (selected && (selected as any).isUnlocked === false) {
setUnlockingCourse(selected);
return;
}
setSelectedCourseId(courseId);
const firstLesson = selected?.lessons && selected.lessons.length > 0 ? selected.lessons[0].id : null;
setSelectedLessonId(firstLesson);
setView('course-view');
}}
onDetail={handleSelectCourse}
subscriptionStatus={user.subscriptionStatus} subscriptionStatus={user.subscriptionStatus}
setView={setView} setView={setView}
/> />
@ -318,6 +329,7 @@ export default function App() {
onSelectCourse={handleSelectCourse} onSelectCourse={handleSelectCourse}
onUnlockCourse={(course) => setUnlockingCourse(course)} onUnlockCourse={(course) => setUnlockingCourse(course)}
subscriptionStatus={user.subscriptionStatus} subscriptionStatus={user.subscriptionStatus}
selectedCourseId={expandedCourseId}
/> />
{/* Expanded Lessons Shelf */} {/* Expanded Lessons Shelf */}

View File

@ -8,12 +8,25 @@ interface CourseCardProps {
onSelect: (courseId: string) => void; onSelect: (courseId: string) => void;
onUnlockRequest?: (course: any) => void; onUnlockRequest?: (course: any) => void;
subscriptionStatus: string; subscriptionStatus: string;
isSelected?: boolean;
} }
export default function CourseCard({ course, onSelect, onUnlockRequest, subscriptionStatus }: CourseCardProps) { export default function CourseCard({ course, onSelect, onUnlockRequest, subscriptionStatus, isSelected = false }: CourseCardProps) {
const isBlocked = subscriptionStatus === 'OVERDUE' || subscriptionStatus === 'CANCELED'; const isBlocked = subscriptionStatus === 'OVERDUE' || subscriptionStatus === 'CANCELED';
const isUnlocked = course.isUnlocked !== false; // defaults to true if undefined const isUnlocked = course.isUnlocked !== false; // defaults to true if undefined
const cardClasses = isSelected
? "absolute top-0 left-0 flex flex-row w-[380px] max-w-[90vw] h-[220px] bg-zinc-900 border-2 border-[#E50914] rounded-xl overflow-hidden cursor-pointer shadow-2xl z-20 transition-all duration-300"
: "absolute top-0 left-0 w-full h-full flex flex-col bg-zinc-950/40 border border-white/10 rounded-xl overflow-hidden cursor-pointer transition-all duration-300 z-10 md:group-hover:z-30 md:group-hover:flex-row md:group-hover:scale-105 md:group-hover:w-[380px] md:group-hover:h-[220px] md:group-hover:shadow-2xl shadow-lg";
const imgContainerClasses = isSelected
? "relative w-[150px] h-full flex-shrink-0 overflow-hidden"
: "relative w-full h-[180px] md:group-hover:w-[150px] md:group-hover:h-full flex-shrink-0 overflow-hidden transition-all duration-300";
const infoContainerClasses = isSelected
? "p-4 flex-grow flex flex-col justify-between min-w-0 h-full bg-zinc-900"
: "p-3 flex-grow flex flex-col justify-between min-w-0 h-full bg-zinc-900/60 md:group-hover:bg-zinc-900 md:group-hover:p-4 transition-all duration-300";
return ( return (
<div <div
id={`course-card-${course.id}`} id={`course-card-${course.id}`}
@ -24,95 +37,97 @@ export default function CourseCard({ course, onSelect, onUnlockRequest, subscrip
onSelect(course.id); onSelect(course.id);
} }
}} }}
className={`group relative glass-card glass-card-hover rounded-xl overflow-hidden cursor-pointer flex flex-col h-full transition-all duration-300 ${!isUnlocked ? 'filter grayscale brightness-75 hover:brightness-100 hover:grayscale-0' : ''}`} className={`relative h-[320px] w-full group ${!isUnlocked ? 'filter grayscale brightness-75 hover:brightness-100 hover:grayscale-0' : ''}`}
> >
{/* Thumbnail */} <div className={cardClasses}>
<div className="relative w-full aspect-video overflow-hidden"> {/* Thumbnail */}
<img <div className={imgContainerClasses}>
src={course.thumbnail} <img
alt={course.title} src={course.thumbnail}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" alt={course.title}
referrerPolicy="no-referrer" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
/> referrerPolicy="no-referrer"
/>
{/* Lock Overlay */} {/* Lock Overlay */}
{!isUnlocked && ( {!isUnlocked && (
<div className="absolute inset-0 bg-black/60 flex flex-col items-center justify-center space-y-1.5 z-10 transition-opacity"> <div className="absolute inset-0 bg-black/60 flex flex-col items-center justify-center space-y-1.5 z-10 transition-opacity">
<div className="bg-[#E50914] text-white p-2.5 rounded-full shadow-[0_0_15px_rgba(229,9,20,0.4)] border border-white/10 animate-pulse"> <div className="bg-[#E50914] text-white p-2 rounded-full shadow-[0_0_15px_rgba(229,9,20,0.4)] border border-white/10 animate-pulse">
<Lock className="w-5 h-5" /> <Lock className="w-4 h-4" />
</div> </div>
<span className="text-[10px] uppercase tracking-widest font-extrabold text-white bg-black/50 px-2.5 py-0.5 rounded-full border border-white/5"> <span className="text-[9px] uppercase tracking-widest font-extrabold text-white bg-black/50 px-2 py-0.5 rounded-full border border-white/5">
Curso Trancado Bloqueado
</span>
</div>
)}
{/* Play Overlay */}
{isUnlocked && (
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<div className="bg-[#E50914] text-white p-3 rounded-full transform scale-75 group-hover:scale-100 transition-all duration-300 shadow-lg">
<Play className="w-6 h-6 fill-white" />
</div>
</div>
)}
{/* Progress bar at the bottom of the thumbnail */}
{course.progress !== undefined && course.progress > 0 && (
<div className="absolute bottom-0 left-0 right-0 h-1 bg-white/10 backdrop-blur-sm">
<div
className="bg-[#E50914] h-full transition-all duration-300 shadow-[0_0_10px_rgba(229,9,20,0.5)]"
style={{ width: `${course.progress}%` }}
/>
</div>
)}
</div>
{/* Course Info */}
<div className="p-4 flex-grow flex flex-col justify-between space-y-3">
<div className="space-y-1">
{/* Category */}
<span className="text-[10px] font-bold text-[#E50914] tracking-wider uppercase flex items-center justify-between">
<span>{course.category}</span>
{!isUnlocked && course.price !== undefined && (
<span className="text-white bg-zinc-800 px-1.5 py-0.5 rounded text-[9px] font-mono border border-white/5">
R$ {course.price.toFixed(2)}
</span> </span>
)} </div>
</span> )}
{/* Title */}
<h3 className="font-display font-bold text-sm md:text-base text-white tracking-tight leading-snug group-hover:text-[#E50914] transition-colors line-clamp-1"> {/* Play Overlay */}
{course.title} {isUnlocked && (
</h3> <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
{/* Description */} <div className="bg-[#E50914] text-white p-2.5 rounded-full transform scale-75 group-hover:scale-100 transition-all duration-300 shadow-lg">
<p className="text-xs text-zinc-400 font-normal line-clamp-2 leading-relaxed"> <Play className="w-5 h-5 fill-white" />
{course.description} </div>
</p> </div>
)}
{/* Progress bar at the bottom of the thumbnail */}
{course.progress !== undefined && course.progress > 0 && (
<div className="absolute bottom-0 left-0 right-0 h-1 bg-white/10 backdrop-blur-sm">
<div
className="bg-[#E50914] h-full transition-all duration-300 shadow-[0_0_10px_rgba(229,9,20,0.5)]"
style={{ width: `${course.progress}%` }}
/>
</div>
)}
</div> </div>
{/* Footer Metrics */} {/* Course Info */}
<div className="flex items-center justify-between pt-2 border-t border-white/5 text-[11px] text-zinc-500"> <div className={infoContainerClasses}>
<div className="flex items-center space-x-1 font-medium"> <div className="space-y-1.5">
<BookOpen className="w-3.5 h-3.5 text-zinc-400" /> {/* Category */}
<span>{course.totalLessons || 0} Aulas</span> <span className="text-[9px] font-bold text-[#E50914] tracking-wider uppercase flex items-center justify-between">
<span>{course.category}</span>
{!isUnlocked && course.price !== undefined && (
<span className="text-white bg-zinc-800 px-1.5 py-0.5 rounded text-[8px] font-mono border border-white/5">
R$ {course.price.toFixed(2)}
</span>
)}
</span>
{/* Title */}
<h3 className="font-display font-bold text-sm md:text-base text-white tracking-tight leading-snug group-hover:text-[#E50914] transition-colors line-clamp-1">
{course.title}
</h3>
{/* Description */}
<p className="text-[11px] text-zinc-400 font-normal line-clamp-2 md:group-hover:line-clamp-3 leading-relaxed">
{course.description}
</p>
</div> </div>
{course.progress !== undefined && course.progress > 0 ? ( {/* Footer Metrics */}
<div className="flex items-center space-x-1 text-[#E50914] font-semibold"> <div className="flex items-center justify-between pt-1.5 border-t border-white/5 text-[10px] text-zinc-500">
{course.progress === 100 ? ( <div className="flex items-center space-x-1 font-medium">
<div className="flex items-center space-x-1 text-emerald-500"> <BookOpen className="w-3 h-3 text-zinc-400" />
<CheckCircle2 className="w-3.5 h-3.5" /> <span>{course.totalLessons || 0} Aulas</span>
<span>Concluído</span>
</div>
) : (
<span>{course.progress}% assistido</span>
)}
</div> </div>
) : (
<span className="text-zinc-500 text-[10px] uppercase font-bold flex items-center gap-1"> {course.progress !== undefined && course.progress > 0 ? (
{!isUnlocked && <Lock className="w-2.5 h-2.5 text-[#E50914]" />} <div className="flex items-center space-x-1 text-[#E50914] font-semibold">
{!isUnlocked ? 'Bloqueado' : 'Não iniciado'} {course.progress === 100 ? (
</span> <div className="flex items-center space-x-1 text-emerald-500">
)} <CheckCircle2 className="w-3.5 h-3.5" />
<span>Concluído</span>
</div>
) : (
<span>{course.progress}%</span>
)}
</div>
) : (
<span className="text-zinc-500 text-[9px] uppercase font-bold flex items-center gap-1">
{!isUnlocked && <Lock className="w-2 text-[#E50914]" />}
{!isUnlocked ? 'Bloqueado' : 'Não iniciado'}
</span>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,9 +9,10 @@ interface CourseCarouselProps {
onSelectCourse: (courseId: string) => void; onSelectCourse: (courseId: string) => void;
onUnlockCourse?: (course: any) => void; onUnlockCourse?: (course: any) => void;
subscriptionStatus: string; subscriptionStatus: string;
selectedCourseId?: string | null;
} }
export default function CourseCarousel({ title, courses, onSelectCourse, onUnlockCourse, subscriptionStatus }: CourseCarouselProps) { export default function CourseCarousel({ title, courses, onSelectCourse, onUnlockCourse, subscriptionStatus, selectedCourseId }: CourseCarouselProps) {
if (courses.length === 0) return null; if (courses.length === 0) return null;
return ( return (
@ -35,6 +36,7 @@ export default function CourseCarousel({ title, courses, onSelectCourse, onUnloc
onSelect={onSelectCourse} onSelect={onSelectCourse}
onUnlockRequest={onUnlockCourse} onUnlockRequest={onUnlockCourse}
subscriptionStatus={subscriptionStatus} subscriptionStatus={subscriptionStatus}
isSelected={selectedCourseId === course.id}
/> />
))} ))}
</div> </div>

View File

@ -5,11 +5,12 @@ import { Course } from '../types';
interface HeroBannerProps { interface HeroBannerProps {
course: (Course & { progress?: number; totalLessons?: number }) | null; course: (Course & { progress?: number; totalLessons?: number }) | null;
onPlay: (courseId: string) => void; onPlay: (courseId: string) => void;
onDetail: (courseId: string) => void;
subscriptionStatus: string; subscriptionStatus: string;
setView: (view: string) => void; setView: (view: string) => void;
} }
export default function HeroBanner({ course, onPlay, subscriptionStatus, setView }: HeroBannerProps) { export default function HeroBanner({ course, onPlay, onDetail, subscriptionStatus, setView }: HeroBannerProps) {
if (!course) { if (!course) {
return ( return (
<div className="h-[30vh] md:h-[45vh] w-full bg-zinc-900 flex items-center justify-center text-zinc-500 rounded-lg border border-zinc-800"> <div className="h-[30vh] md:h-[45vh] w-full bg-zinc-900 flex items-center justify-center text-zinc-500 rounded-lg border border-zinc-800">
@ -91,7 +92,7 @@ export default function HeroBanner({ course, onPlay, subscriptionStatus, setView
)} )}
<button <button
onClick={() => onPlay(course.id)} onClick={() => onDetail(course.id)}
className="glass-btn-secondary text-white font-medium px-5 py-2.5 rounded-lg flex items-center space-x-2 cursor-pointer" className="glass-btn-secondary text-white font-medium px-5 py-2.5 rounded-lg flex items-center space-x-2 cursor-pointer"
> >
<Info className="w-5 h-5 text-zinc-300" /> <Info className="w-5 h-5 text-zinc-300" />