Files
KLC-Website-Frontend/src/components/Webinars.tsx
2026-03-27 12:43:34 +05:30

710 lines
29 KiB
TypeScript

import {
ArrowRight,
ChevronLeft,
ChevronRight,
Clock,
Eye,
Filter,
Grid,
List,
Play,
Search,
Star,
Users,
X
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useWebinarListQuery, type WebinarItem } from '../redux/services/webinarApi';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { FullScreenLoader } from './FullScreenLoader';
import { navigateTo } from './Router';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Slider } from './ui/slider';
import { WebcastCTABanner } from './WebcastCTABanner';
// Status options with proper mapping to API values
const statusOptions = [
{ value: 'scheduled', label: '📅 Scheduled', color: 'bg-blue-100 text-blue-800 border-blue-200' },
{ value: 'live', label: '🔴 Live', color: 'bg-red-100 text-red-800 border-red-200' },
{ value: 'ended', label: '✅ Ended', color: 'bg-gray-100 text-gray-800 border-gray-200' },
{ value: 'cancelled', label: '❌ Cancelled', color: 'bg-red-50 text-red-600 border-red-200' }
];
const sortOptions = [
{ value: 'most_popular', label: 'Most Popular' },
{ value: 'newest', label: 'Newest First' },
{ value: 'oldest', label: 'Oldest First' },
{ value: 'title', label: 'Title A-Z' },
{ value: 'duration', label: 'Duration' }
];
// Static tags for all webinars
const staticTags = ['Leadership', 'Executive Development', 'Strategy', 'Innovation', 'Change Management', 'Business Growth', 'Team Building', 'Digital Transformation'];
export function Webinars() {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All Categories');
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
const [durationRange, setDurationRange] = useState([0, 120]);
const [attendeeRange, setAttendeeRange] = useState([0, 5000]);
const [sortBy, setSortBy] = useState('most_popular');
const [viewType, setViewType] = useState<'grid' | 'list'>('grid');
const [currentPage, setCurrentPage] = useState(1);
const webinarsPerPage = 6;
const containerRef = useRef<HTMLDivElement>(null);
// Fetch webinars from API
const {
data: webinarResponse,
isLoading,
isError,
} = useWebinarListQuery({
limit: 100,
offset: 0,
search: searchTerm || undefined,
status: selectedStatuses.length > 0 ? selectedStatuses : undefined,
minDuration: durationRange[0] > 0 ? durationRange[0] : undefined,
maxDuration: durationRange[1] < 120 ? durationRange[1] : undefined,
minAttendees: attendeeRange[0] > 0 ? attendeeRange[0] : undefined,
maxAttendees: attendeeRange[1] < 5000 ? attendeeRange[1] : undefined,
sortBy: sortBy as any,
});
const webinars = webinarResponse?.data?.items || [];
// Get random tags for each webinar (3 random tags from staticTags)
const getRandomTags = (seed: string) => {
// Use the webinar ID as seed to get consistent tags for each webinar
const hash = seed.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const shuffled = [...staticTags];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled.slice(0, 3);
};
// Get unique categories from API data
const categories = [
'All Categories',
...Array.from(new Set(webinars.map(webinar => webinar.session_title?.split(' ')[0] || 'General')))
];
// Helper functions
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const formatDuration = (minutes: number) => {
if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
return `${minutes}min`;
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'live':
return <Badge className="bg-red-600 text-white animate-pulse">LIVE NOW</Badge>;
case 'scheduled':
return <Badge className="bg-blue-600 text-white">SCHEDULED</Badge>;
case 'ended':
return <Badge className="bg-gray-600 text-white">ENDED</Badge>;
case 'cancelled':
return <Badge className="bg-red-400 text-white">CANCELLED</Badge>;
default:
return null;
}
};
const getActionText = (status: string) => {
switch (status) {
case 'live':
return 'Join Now';
case 'scheduled':
return 'Register';
case 'ended':
return 'Watch Recording';
case 'cancelled':
return 'Cancelled';
default:
return 'Learn More';
}
};
// Statistics
const stats = {
total: webinars.length,
scheduled: webinars.filter(w => w.webinar_status === 'scheduled').length,
live: webinars.filter(w => w.webinar_status === 'live').length,
ended: webinars.filter(w => w.webinar_status === 'ended').length,
cancelled: webinars.filter(w => w.webinar_status === 'cancelled').length,
categories: categories.length - 1
};
// Filter webinars
const filteredWebinars = webinars.filter(webinar => {
const matchesCategory = selectedCategory === 'All Categories' ||
(webinar.session_title && webinar.session_title.toLowerCase().includes(selectedCategory.toLowerCase()));
return matchesCategory;
});
// Paginate results
const totalPages = Math.ceil(filteredWebinars.length / webinarsPerPage);
const currentWebinars = filteredWebinars.slice((currentPage - 1) * webinarsPerPage, currentPage * webinarsPerPage);
const clearAllFilters = () => {
setSearchTerm('');
setSelectedCategory('All Categories');
setSelectedStatuses([]);
setDurationRange([0, 120]);
setAttendeeRange([0, 5000]);
setSortBy('most_popular');
};
const hasActiveFilters = searchTerm ||
selectedCategory !== 'All Categories' ||
selectedStatuses.length > 0 ||
durationRange[0] !== 0 || durationRange[1] !== 120 ||
attendeeRange[0] !== 0 || attendeeRange[1] !== 5000;
const toggleStatus = (status: string) => {
setSelectedStatuses(prev =>
prev.includes(status)
? prev.filter(s => s !== status)
: [...prev, status]
);
};
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, selectedCategory, selectedStatuses, durationRange, attendeeRange, sortBy]);
const WebinarCard = ({ webinar }: { webinar: WebinarItem }) => {
const handleCardClick = () => {
if (webinar.webinar_status !== 'cancelled') {
navigateTo(`/webinar/${webinar.id}`);
}
};
const isCancelled = webinar.webinar_status === 'cancelled';
const webinarTags = getRandomTags(webinar.id);
if (viewType === 'list') {
return (
<Card
className={`mb-4 cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-1 ${isCancelled ? 'opacity-75' : ''}`}
onClick={handleCardClick}
style={isCancelled ? { cursor: 'not-allowed' } : {}}
>
<CardContent className="p-6">
<div className="flex gap-6">
{/* Thumbnail */}
<div className="flex-shrink-0 w-32 h-24 rounded-lg overflow-hidden bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
<Play className="w-8 h-8 text-gray-400" />
</div>
{/* Content */}
<div className="flex-1">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
{getStatusBadge(webinar.webinar_status)}
</div>
<span className="text-small text-gray-500">{formatDate(webinar.session_datetime)}</span>
</div>
<h3 className="text-h4 mb-2 line-clamp-2">{webinar.session_title}</h3>
<p className="text-body text-gray-600 mb-3 line-clamp-2">
{webinar.description || 'No description available'}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-3">
{webinarTags.map((tag, idx) => (
<Badge key={idx} variant="outline" className="text-xs bg-gray-50">
{tag}
</Badge>
))}
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 text-small text-gray-500">
<span className="flex items-center gap-1">
<Users className="w-4 h-4" />
{webinar.owner || 'Kautilya Leadership'}
</span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{formatDuration(webinar.duration_minutes)}
</span>
<span className="flex items-center gap-1">
<Eye className="w-4 h-4" />
Max {webinar.max_attendee.toLocaleString()}
</span>
</div>
{!isCancelled && (
<div className="flex items-center gap-2 font-medium" style={{ color: '#04045b' }}>
<span className="text-small">{getActionText(webinar.webinar_status)}</span>
<ArrowRight className="w-4 h-4" />
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
}
// Grid View
return (
<Card
className={`cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-2 group overflow-hidden ${isCancelled ? 'opacity-75' : ''}`}
onClick={handleCardClick}
style={isCancelled ? { cursor: 'not-allowed' } : {}}
>
{/* Image */}
<div className="aspect-video relative overflow-hidden bg-gradient-to-br from-gray-100 to-gray-200">
<div className="w-full h-full flex items-center justify-center">
<Play className="w-12 h-12 text-gray-400" />
</div>
{/* Status Badge */}
<div className="absolute top-4 left-4">
{getStatusBadge(webinar.webinar_status)}
</div>
{/* Play Icon Overlay */}
{!isCancelled && (
<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="bg-white bg-opacity-90 rounded-full p-3">
<Play className="w-6 h-6 text-gray-800" />
</div>
</div>
)}
</div>
{/* Content */}
<CardContent className="p-6">
<div className="flex items-center justify-between mb-2">
<Badge variant="secondary" className="text-xs">
{webinar.recurring_webinar ? 'Recurring' : 'One-time'}
</Badge>
<span className="text-small text-gray-500">{formatDate(webinar.session_datetime)}</span>
</div>
<h3 className="text-h4 mb-3 line-clamp-2 group-hover:text-primary transition-colors">
{webinar.session_title}
</h3>
<p className="text-body text-gray-600 mb-4 line-clamp-2">
{webinar.description || 'No description available'}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-2 mb-4">
{webinarTags.slice(0, 2).map((tag, idx) => (
<Badge key={idx} variant="outline" className="text-xs bg-gray-50">
{tag}
</Badge>
))}
</div>
<div className="space-y-3">
<div className="flex items-center gap-2 text-small text-gray-500">
<Users className="w-4 h-4" />
<span>{webinar.owner || 'Kautilya Leadership'}</span>
</div>
<div className="flex items-center justify-between text-small text-gray-500">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{formatDuration(webinar.duration_minutes)}</span>
</div>
<div className="flex items-center gap-1">
<Eye className="w-4 h-4" />
<span>Max {webinar.max_attendee.toLocaleString()}</span>
</div>
</div>
</div>
{!isCancelled && (
<div className="flex items-center justify-between mt-4 pt-4 border-t">
<div className="flex items-center gap-1 text-xs text-gray-500">
{webinar.require_registration && (
<>
<Star className="w-3 h-3 text-yellow-500" />
<span>Registration Required</span>
</>
)}
</div>
<div className="flex items-center gap-2 font-medium group-hover:translate-x-1 transition-transform" style={{ color: '#04045b' }}>
<span className="text-small">{getActionText(webinar.webinar_status)}</span>
<ArrowRight className="w-4 h-4" />
</div>
</div>
)}
</CardContent>
</Card>
);
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<FullScreenLoader text="Loading webinars..." />
</div>
);
}
if (isError) {
return (
<div className="min-h-screen flex items-center justify-center bg-white">
<div className="text-center">
<p className="text-red-600 mb-4">Error loading webinars. Please try again later.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
</div>
</div>
);
}
return (
<div style={{ backgroundColor: '#FFFFFF' }}>
{/* Hero Section */}
<section className="relative h-[400px] overflow-hidden">
<div className="absolute inset-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1652265540589-46f91535337b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxidXNpbmVzcyUyMHByZXNlbnRhdGlvbiUyMHdlYmluYXIlMjBjb25mZXJlbmNlfGVufDF8fHx8MTc1NTg1NDI3MHww&ixlib=rb-4.1.0&q=80&w=1080"
alt="Professional webinar and conference presentation"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/60" />
</div>
<div className="relative h-full flex flex-col justify-center section-margin-x">
<div className="text-center">
<h1 className="text-h1-white mb-6">
Leadership Webcasts &<br />
Expert Insights
</h1>
<p className="text-body-lg-white max-w-3xl mx-auto">
Explore our comprehensive collection of expert insights, research, and practical guidance
to elevate your leadership journey and drive organizational excellence.
</p>
</div>
</div>
<div className="absolute bottom-0 left-0 right-0">
<div className="bg-black/80 backdrop-blur-sm px-8 py-6">
<div className="section-margin-x">
<div className="grid grid-cols-3 gap-8 text-center">
<div>
<div className="text-h2-white mb-2">{stats.total}+</div>
<div className="text-small-white">Expert Webcasts</div>
</div>
<div>
<div className="text-h2-white mb-2">{stats.categories}</div>
<div className="text-small-white">Categories</div>
</div>
<div>
<div className="text-h2-white mb-2">15,200</div>
<div className="text-small-white">Total Views</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Search and Controls Section */}
<section className="py-8" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div className="relative max-w-md flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
type="text"
placeholder="Search webinars..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-3 text-body rounded-lg border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 w-full bg-gray-50"
style={{
fontSize: 'var(--font-body)',
fontFamily: 'var(--font-family-base)',
height: '48px'
}}
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center border border-gray-300 rounded-lg overflow-hidden">
<button
onClick={() => setViewType('grid')}
className={`p-2 transition-colors ${viewType === 'grid'
? 'text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewType === 'grid' ? '#04045b' : undefined
}}
aria-label="Grid view"
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewType('list')}
className={`p-2 transition-colors ${viewType === 'list'
? 'text-white'
: 'bg-white text-gray-600 hover:bg-gray-50'
}`}
style={{
backgroundColor: viewType === 'list' ? '#04045b' : undefined
}}
aria-label="List view"
>
<List className="w-4 h-4" />
</button>
</div>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-40 text-body">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
{sortOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</section>
{/* Main Content Section */}
<section className="pb-16" style={{ backgroundColor: '#FFFFFF' }}>
<div className="section-margin-x">
<div className="grid grid-cols-12 gap-8">
{/* Left Sidebar Filters */}
<div className="col-span-12 lg:col-span-3">
<div className="sticky top-4">
<Card className="bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md" style={{ backgroundColor: 'rgba(4, 4, 91, 0.1)' }}>
<Filter className="w-3.5 h-3.5" style={{ color: '#04045b' }} />
</div>
<h3 className="text-body font-semibold text-gray-800">Filters</h3>
</div>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="text-xs px-2 py-1 rounded-md transition-colors"
>
<X className="w-3 h-3 mr-1" />
Clear
</Button>
)}
</div>
</div>
<div className="p-4">
<div className="space-y-6">
{/* Status Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Status
</label>
<div className="flex flex-wrap gap-2">
{statusOptions.map((status) => (
<button
key={status.value}
onClick={() => toggleStatus(status.value)}
className={`
px-3 py-1.5 rounded-full text-xs font-medium border transition-all duration-200
${selectedStatuses.includes(status.value)
? `${status.color} ring-2 ring-blue-200 shadow-sm`
: 'bg-gray-50 text-gray-600 border-gray-200 hover:bg-gray-100 hover:border-gray-300'
}
`}
>
{status.label}
</button>
))}
</div>
{selectedStatuses.length > 0 && (
<div className="mt-2 text-xs text-gray-500">
{selectedStatuses.length} selected
</div>
)}
</div>
{/* Duration Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Duration (minutes)
</label>
<div className="px-2">
<Slider
value={durationRange}
onValueChange={setDurationRange}
max={120}
min={0}
step={5}
className="w-full"
/>
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>{durationRange[0]} min</span>
<span>{durationRange[1]} min</span>
</div>
</div>
</div>
{/* Attendee Filter */}
<div className="filter-section">
<label className="block text-small mb-3 font-medium text-gray-700">
Max Attendees
</label>
<div className="px-2">
<Slider
value={attendeeRange}
onValueChange={setAttendeeRange}
max={5000}
min={0}
step={100}
className="w-full"
/>
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>{attendeeRange[0].toLocaleString()}</span>
<span>{attendeeRange[1].toLocaleString()}+</span>
</div>
</div>
</div>
</div>
</div>
</Card>
</div>
</div>
{/* Right Main Content */}
<div className="col-span-12 lg:col-span-9">
<div className="flex items-center justify-between mb-6">
<div className="text-body text-gray-600">
Showing {currentWebinars.length} of {filteredWebinars.length} webinars
</div>
<div className="text-small text-gray-500">
Page {currentPage} of {totalPages}
</div>
</div>
<div ref={containerRef}>
{currentWebinars.length === 0 ? (
<div className="text-center py-12">
<div className="text-gray-400 mb-4">
<Search className="w-12 h-12 mx-auto mb-4" />
</div>
<h3 className="text-h4 mb-2">No webinars found</h3>
<p className="text-body text-gray-600 mb-4">
Try adjusting your filters or search terms
</p>
{hasActiveFilters && (
<Button onClick={clearAllFilters}>
Clear all filters
</Button>
)}
</div>
) : (
<>
{viewType === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
{currentWebinars.map((webinar) => (
<WebinarCard key={webinar.id} webinar={webinar} />
))}
</div>
) : (
<div className="space-y-4 mb-8">
{currentWebinars.map((webinar) => (
<WebinarCard key={webinar.id} webinar={webinar} />
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4" />
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
const page = i + 1;
return (
<Button
key={page}
variant={currentPage === page ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(page)}
className="min-w-10"
style={currentPage === page ? { backgroundColor: '#04045b' } : {}}
>
{page}
</Button>
);
})}
{totalPages > 5 && <span className="px-2">...</span>}
{totalPages > 5 && (
<Button
variant={currentPage === totalPages ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(totalPages)}
className="min-w-10"
style={currentPage === totalPages ? { backgroundColor: '#04045b' } : {}}
>
{totalPages}
</Button>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
>
Next
<ChevronRight className="w-4 h-4" />
</Button>
</div>
)}
</>
)}
</div>
</div>
</div>
</div>
</section>
<WebcastCTABanner />
</div>
);
}