first commit
All checks were successful
CodeAnt AI Review - Stage 1 / codeant-review (push) Successful in 58s
All checks were successful
CodeAnt AI Review - Stage 1 / codeant-review (push) Successful in 58s
This commit is contained in:
397
.gitea/workflows/src/components/UpcomingWebinarsSection.tsx
Normal file
397
.gitea/workflows/src/components/UpcomingWebinarsSection.tsx
Normal file
@@ -0,0 +1,397 @@
|
||||
import { ArrowRight, ChevronLeft, ChevronRight, ArrowUpRight } from "lucide-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import svgPaths from "../imports/svg-ec87ex3oms";
|
||||
import { PrimaryCTAButton } from "./PrimaryCTAButton";
|
||||
import { navigateTo } from "./Router";
|
||||
import { sharedWebinarsData, type WebinarData } from "../data/webinarsData";
|
||||
|
||||
// WebinarCard Component with unified data structure and navigation
|
||||
interface WebinarCardProps {
|
||||
webinar: WebinarData;
|
||||
}
|
||||
|
||||
function WebinarCard({ webinar }: WebinarCardProps) {
|
||||
const handleCardClick = () => {
|
||||
// All webinar cards now navigate to the same route for consistency
|
||||
navigateTo(`/webinar/${webinar.slug}`);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleCardClick();
|
||||
}
|
||||
};
|
||||
|
||||
// Format date for display
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Get status badge styling
|
||||
const getStatusBadge = () => {
|
||||
switch (webinar.status) {
|
||||
case 'live':
|
||||
return (
|
||||
<span className="px-3 py-1 text-xs font-semibold text-white bg-red-600 rounded-full animate-pulse">
|
||||
LIVE
|
||||
</span>
|
||||
);
|
||||
case 'upcoming':
|
||||
return (
|
||||
<span className="px-3 py-1 text-xs font-semibold text-white bg-blue-600 rounded-full">
|
||||
UPCOMING
|
||||
</span>
|
||||
);
|
||||
case 'recorded':
|
||||
return (
|
||||
<span className="px-3 py-1 text-xs font-semibold text-white bg-green-600 rounded-full">
|
||||
RECORDED
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Get action text based on status
|
||||
const getActionText = () => {
|
||||
switch (webinar.status) {
|
||||
case 'live':
|
||||
return 'Join Now';
|
||||
case 'upcoming':
|
||||
return 'Register';
|
||||
case 'recorded':
|
||||
return 'Watch Recording';
|
||||
default:
|
||||
return 'Learn More';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-shrink-0 w-80 bg-white rounded-lg shadow-lg overflow-hidden cursor-pointer transition-all duration-300 hover:shadow-xl hover:transform hover:-translate-y-2 group"
|
||||
onClick={handleCardClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`View webinar: ${webinar.title}`}
|
||||
style={{
|
||||
fontFamily: 'var(--font-family-base)'
|
||||
}}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={webinar.thumbnail}
|
||||
alt={webinar.title}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className="absolute top-4 left-4">
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
{/* Featured Badge */}
|
||||
{webinar.featured && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<span className="px-3 py-1 text-xs font-semibold text-yellow-800 bg-yellow-100 rounded-full border border-yellow-200">
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hover Overlay */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-40 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
<div className="text-white text-center">
|
||||
<ArrowUpRight className="w-8 h-8 mx-auto mb-2" />
|
||||
<span className="text-sm font-medium">View Details</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<h3
|
||||
className="text-lg font-semibold mb-3 text-gray-900 group-hover:text-blue-600 transition-colors duration-200 line-clamp-2"
|
||||
style={{
|
||||
color: 'var(--color-brand-black)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
fontSize: 'var(--font-h4)',
|
||||
fontWeight: 'var(--font-weight-h4)',
|
||||
lineHeight: 'var(--line-height-h4)'
|
||||
}}
|
||||
>
|
||||
{webinar.title}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2 mb-4">
|
||||
<p
|
||||
className="text-sm text-muted flex items-center"
|
||||
style={{
|
||||
color: 'var(--color-gray-muted)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
fontSize: 'var(--font-small)'
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
{webinar.presenter}
|
||||
</p>
|
||||
|
||||
<p
|
||||
className="text-sm text-muted flex items-center"
|
||||
style={{
|
||||
color: 'var(--color-gray-muted)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
fontSize: 'var(--font-small)'
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{formatDate(webinar.date)} at {webinar.time} {webinar.timezone}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p
|
||||
className="text-sm text-muted flex items-center"
|
||||
style={{
|
||||
color: 'var(--color-gray-muted)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
fontSize: 'var(--font-small)'
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{webinar.duration}
|
||||
</p>
|
||||
|
||||
<p
|
||||
className="text-sm text-muted flex items-center"
|
||||
style={{
|
||||
color: 'var(--color-gray-muted)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
fontSize: 'var(--font-small)'
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
||||
</svg>
|
||||
{webinar.attendees}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Indicator - Consistent with status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span
|
||||
className="text-sm font-medium group-hover:text-blue-600 transition-colors duration-200"
|
||||
style={{
|
||||
color: 'var(--color-primary)',
|
||||
fontFamily: 'var(--font-family-base)',
|
||||
fontSize: 'var(--font-small)',
|
||||
fontWeight: 'var(--font-weight-subhead)'
|
||||
}}
|
||||
>
|
||||
{getActionText()}
|
||||
</span>
|
||||
<ArrowRight className="w-4 h-4 text-blue-600 group-hover:translate-x-1 transition-transform duration-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UpcomingWebinarsSection() {
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(true);
|
||||
const carouselRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const checkScrollButtons = () => {
|
||||
if (carouselRef.current) {
|
||||
const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;
|
||||
setCanScrollLeft(scrollLeft > 0);
|
||||
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollLeft = () => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.scrollBy({ left: -340, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const scrollRight = () => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.scrollBy({ left: 340, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const carousel = carouselRef.current;
|
||||
if (carousel) {
|
||||
carousel.addEventListener('scroll', checkScrollButtons);
|
||||
checkScrollButtons(); // Initial check
|
||||
return () => carousel.removeEventListener('scroll', checkScrollButtons);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="py-20"
|
||||
style={{ backgroundColor: '#FFFFFF' }}
|
||||
>
|
||||
<div className="section-margin-x">
|
||||
<div className="grid grid-cols-12 gap-16 items-start">
|
||||
|
||||
{/* Left Column - Content */}
|
||||
<div className="col-span-12 lg:col-span-5">
|
||||
{/* Heading */}
|
||||
<h2
|
||||
className="text-4xl leading-tight mb-6"
|
||||
style={{
|
||||
color: 'var(--color-brand-black)',
|
||||
fontFamily: 'var(--font-family-brand)',
|
||||
fontWeight: '700'
|
||||
}}
|
||||
>
|
||||
Upcoming Corporate Webinars
|
||||
</h2>
|
||||
|
||||
{/* Description */}
|
||||
<p
|
||||
className="text-lg leading-relaxed mb-8"
|
||||
style={{
|
||||
color: 'var(--color-brand-black)',
|
||||
fontFamily: 'var(--font-family-brand)',
|
||||
fontWeight: '400'
|
||||
}}
|
||||
>
|
||||
Join live sessions led by leadership experts designed for professionals looking to elevate strategic thinking, decision-making, and people leadership.
|
||||
</p>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<div className="flex gap-3 mb-8">
|
||||
<button
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-lg border-2 transition-all duration-300 ${
|
||||
!canScrollLeft
|
||||
? 'opacity-40 cursor-not-allowed bg-gray-100 border-gray-300'
|
||||
: 'bg-white hover:scale-105 hover:shadow-lg active:scale-95'
|
||||
}`}
|
||||
onClick={scrollLeft}
|
||||
disabled={!canScrollLeft}
|
||||
aria-label="Previous webinar"
|
||||
style={{
|
||||
fontFamily: 'var(--font-family-brand)',
|
||||
borderColor: !canScrollLeft ? '#D1D5DB' : '#04045B',
|
||||
backgroundColor: !canScrollLeft ? '#F3F4F6' : 'white'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!canScrollLeft) return;
|
||||
e.currentTarget.style.backgroundColor = '#04045B';
|
||||
const icon = e.currentTarget.querySelector('svg');
|
||||
if (icon) icon.style.color = 'white';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!canScrollLeft) return;
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
const icon = e.currentTarget.querySelector('svg');
|
||||
if (icon) icon.style.color = '#04045B';
|
||||
}}
|
||||
>
|
||||
<ChevronLeft
|
||||
size={20}
|
||||
style={{
|
||||
color: !canScrollLeft ? '#9CA3AF' : '#04045B',
|
||||
transition: 'color 0.3s ease'
|
||||
}}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-lg border-2 transition-all duration-300 ${
|
||||
!canScrollRight
|
||||
? 'opacity-40 cursor-not-allowed bg-gray-100 border-gray-300'
|
||||
: 'bg-white hover:scale-105 hover:shadow-lg active:scale-95'
|
||||
}`}
|
||||
onClick={scrollRight}
|
||||
disabled={!canScrollRight}
|
||||
aria-label="Next webinar"
|
||||
style={{
|
||||
fontFamily: 'var(--font-family-brand)',
|
||||
borderColor: !canScrollRight ? '#D1D5DB' : '#04045B',
|
||||
backgroundColor: !canScrollRight ? '#F3F4F6' : 'white'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!canScrollRight) return;
|
||||
e.currentTarget.style.backgroundColor = '#04045B';
|
||||
const icon = e.currentTarget.querySelector('svg');
|
||||
if (icon) icon.style.color = 'white';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!canScrollRight) return;
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
const icon = e.currentTarget.querySelector('svg');
|
||||
if (icon) icon.style.color = '#04045B';
|
||||
}}
|
||||
>
|
||||
<ChevronRight
|
||||
size={20}
|
||||
style={{
|
||||
color: !canScrollRight ? '#9CA3AF' : '#04045B',
|
||||
transition: 'color 0.3s ease'
|
||||
}}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CTA Button - Navigate to main webinars page */}
|
||||
<PrimaryCTAButton
|
||||
text="Explore All"
|
||||
onClick={() => navigateTo('/webinars')}
|
||||
ariaLabel="Explore all upcoming webinars"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Carousel */}
|
||||
<div className="col-span-12 lg:col-span-7">
|
||||
<div className="relative">
|
||||
{/* Carousel Container */}
|
||||
<div
|
||||
ref={carouselRef}
|
||||
className="flex gap-6 overflow-x-hidden scroll-smooth"
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none'
|
||||
}}
|
||||
>
|
||||
{/* Use shared webinar data for consistency */}
|
||||
{sharedWebinarsData.map((webinar) => (
|
||||
<WebinarCard key={webinar.id} webinar={webinar} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Fade Gradient Overlay */}
|
||||
<div
|
||||
className="absolute top-0 right-0 bottom-0 w-16 pointer-events-none"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, transparent, #FFFFFF)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user