802 lines
34 KiB
TypeScript
802 lines
34 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { Button } from './ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import { Badge } from './ui/badge';
|
|
import { Input } from './ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
|
import { Slider } from './ui/slider';
|
|
import { ImageWithFallback } from './figma/ImageWithFallback';
|
|
import { PrimaryCTAButton } from './PrimaryCTAButton';
|
|
import { navigateTo } from './Router';
|
|
import { sharedWebinarsData, type WebinarData } from '../data/webinarsData';
|
|
import { WebcastCTABanner } from './WebcastCTABanner';
|
|
import {
|
|
Search,
|
|
Calendar,
|
|
Clock,
|
|
Users,
|
|
Play,
|
|
ArrowRight,
|
|
Filter,
|
|
Grid,
|
|
List,
|
|
SortAsc,
|
|
Eye,
|
|
Star,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
X
|
|
} from 'lucide-react';
|
|
|
|
export function Webinars() {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState('All Categories');
|
|
const [selectedFormat, setSelectedFormat] = useState('All Formats');
|
|
const [selectedLevel, setSelectedLevel] = useState('All Levels');
|
|
|
|
// Updated state for multi-select status pills
|
|
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
|
|
|
|
// Updated state for duration slider (min, max in minutes)
|
|
const [durationRange, setDurationRange] = useState([0, 120]);
|
|
|
|
// Attendee range slider state
|
|
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);
|
|
|
|
// Use shared webinars data instead of local mock data
|
|
const webinars = sharedWebinarsData;
|
|
|
|
// Get unique values for filters from shared data
|
|
const categories = ['All Categories', ...Array.from(new Set(webinars.map(webinar => webinar.category)))];
|
|
const formats = ['All Formats', ...Array.from(new Set(webinars.map(webinar => webinar.format)))];
|
|
const levels = ['All Levels', ...Array.from(new Set(webinars.map(webinar => webinar.level)))];
|
|
|
|
// Status options for pills - updated to match shared data structure
|
|
const statusOptions = [
|
|
{ value: 'upcoming', label: '📅 Upcoming', 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: 'recorded', label: '▶️ Recorded', color: 'bg-green-100 text-green-800 border-green-200' },
|
|
{ value: 'featured', label: '⭐ Featured', color: 'bg-yellow-100 text-yellow-800 border-yellow-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' }
|
|
];
|
|
|
|
// Helper function to convert attendees string to number
|
|
const parseAttendees = (attendeesStr: string): number => {
|
|
const numStr = attendeesStr.replace(/[^\d]/g, '');
|
|
return parseInt(numStr) || 0;
|
|
};
|
|
|
|
// Helper function to convert duration string to minutes
|
|
const parseDuration = (durationStr: string): number => {
|
|
const numStr = durationStr.replace(/[^\d]/g, '');
|
|
return parseInt(numStr) || 0;
|
|
};
|
|
|
|
// Filter and sort webinars
|
|
const filteredWebinars = webinars.filter(webinar => {
|
|
const matchesSearch = webinar.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
webinar.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
webinar.presenter.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
webinar.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
const matchesCategory = selectedCategory === 'All Categories' || webinar.category === selectedCategory;
|
|
const matchesFormat = selectedFormat === 'All Formats' || webinar.format === selectedFormat;
|
|
const matchesLevel = selectedLevel === 'All Levels' || webinar.level === selectedLevel;
|
|
|
|
const matchesStatus = selectedStatuses.length === 0 ||
|
|
selectedStatuses.some(status => {
|
|
if (status === 'featured') return webinar.featured;
|
|
return webinar.status === status;
|
|
});
|
|
|
|
const durationMinutes = parseDuration(webinar.duration);
|
|
const matchesDuration = durationMinutes >= durationRange[0] && durationMinutes <= durationRange[1];
|
|
|
|
const attendeeCount = parseAttendees(webinar.attendees);
|
|
const matchesAttendees = attendeeCount >= attendeeRange[0] && attendeeCount <= attendeeRange[1];
|
|
|
|
return matchesSearch && matchesCategory && matchesFormat && matchesLevel && matchesStatus && matchesDuration && matchesAttendees;
|
|
}).sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'Most Popular':
|
|
// Add logic for "Most Popular" - you might want to use views, attendees, or featured status
|
|
return (b.featured ? 1 : 0) - (a.featured ? 1 : 0) ||
|
|
parseAttendees(b.attendees) - parseAttendees(a.attendees);
|
|
case 'newest':
|
|
return new Date(b.date).getTime() - new Date(a.date).getTime();
|
|
case 'oldest':
|
|
return new Date(a.date).getTime() - new Date(b.date).getTime();
|
|
case 'title':
|
|
return a.title.localeCompare(b.title);
|
|
case 'duration':
|
|
return parseDuration(b.duration) - parseDuration(a.duration);
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// Statistics
|
|
const stats = {
|
|
total: webinars.length,
|
|
upcoming: webinars.filter(w => w.status === 'upcoming').length,
|
|
live: webinars.filter(w => w.status === 'live').length,
|
|
recorded: webinars.filter(w => w.status === 'recorded').length,
|
|
featured: webinars.filter(w => w.featured).length,
|
|
categories: new Set(webinars.map(w => w.category)).size
|
|
};
|
|
|
|
// Paginate results
|
|
const totalPages = Math.ceil(filteredWebinars.length / webinarsPerPage);
|
|
const currentWebinars = filteredWebinars.slice((currentPage - 1) * webinarsPerPage, currentPage * webinarsPerPage);
|
|
|
|
console.log('Filtered webinars:', filteredWebinars.length);
|
|
console.log('Total pages:', totalPages);
|
|
console.log('Current page:', currentPage);
|
|
console.log('Current webinars count:', currentWebinars.length);
|
|
|
|
const formatDate = (dateString: string) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
};
|
|
|
|
const clearAllFilters = () => {
|
|
setSearchTerm('');
|
|
setSelectedCategory('All Categories');
|
|
setSelectedFormat('All Formats');
|
|
setSelectedLevel('All Levels');
|
|
setSelectedStatuses([]);
|
|
setDurationRange([0, 120]);
|
|
setAttendeeRange([0, 5000]);
|
|
setSortBy('Most Popular');
|
|
};
|
|
|
|
const hasActiveFilters = searchTerm ||
|
|
selectedCategory !== 'All Categories' ||
|
|
selectedFormat !== 'All Formats' ||
|
|
selectedLevel !== 'All Levels' ||
|
|
selectedStatuses.length > 0 ||
|
|
durationRange[0] !== 0 || durationRange[1] !== 120 ||
|
|
attendeeRange[0] !== 0 || attendeeRange[1] !== 5000;
|
|
|
|
// Status pill toggle function
|
|
const toggleStatus = (status: string) => {
|
|
setSelectedStatuses(prev =>
|
|
prev.includes(status)
|
|
? prev.filter(s => s !== status)
|
|
: [...prev, status]
|
|
);
|
|
};
|
|
|
|
// Reset to page 1 when filters change
|
|
useEffect(() => {
|
|
setCurrentPage(1);
|
|
}, [searchTerm, selectedCategory, selectedFormat, selectedLevel, selectedStatuses, durationRange, attendeeRange, sortBy]);
|
|
|
|
// Updated WebinarCard component that navigates to consistent route
|
|
const WebinarCard = ({ webinar }: { webinar: WebinarData }) => {
|
|
const handleCardClick = () => {
|
|
// Navigate to consistent webinar detail route
|
|
navigateTo(`/webinar/${webinar.slug}`);
|
|
};
|
|
|
|
const getStatusBadge = () => {
|
|
switch (webinar.status) {
|
|
case 'live':
|
|
return <Badge className="bg-red-600 text-white animate-pulse">LIVE</Badge>;
|
|
case 'upcoming':
|
|
return <Badge className="bg-blue-600 text-white">UPCOMING</Badge>;
|
|
case 'recorded':
|
|
return <Badge className="bg-green-600 text-white">RECORDED</Badge>;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getActionText = () => {
|
|
switch (webinar.status) {
|
|
case 'live':
|
|
return 'Join Now';
|
|
case 'upcoming':
|
|
return 'Register';
|
|
case 'recorded':
|
|
return 'Watch Recording';
|
|
default:
|
|
return 'Learn More';
|
|
}
|
|
};
|
|
|
|
if (viewType === 'list') {
|
|
return (
|
|
<Card
|
|
className="mb-4 cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-1"
|
|
onClick={handleCardClick}
|
|
>
|
|
<CardContent className="p-6">
|
|
<div className="flex gap-6">
|
|
{/* Thumbnail */}
|
|
<div className="flex-shrink-0 w-32 h-24 rounded-lg overflow-hidden">
|
|
<ImageWithFallback
|
|
src={webinar.thumbnail}
|
|
alt={webinar.title}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1">
|
|
<div className="flex justify-between items-start mb-2">
|
|
<div className="flex items-center gap-2">
|
|
{getStatusBadge()}
|
|
{webinar.featured && (
|
|
<Badge className="bg-yellow-100 text-yellow-800">Featured</Badge>
|
|
)}
|
|
</div>
|
|
<span className="text-small text-gray-500">{formatDate(webinar.date)}</span>
|
|
</div>
|
|
|
|
<h3 className="text-h4 mb-2 line-clamp-2">{webinar.title}</h3>
|
|
<p className="text-body text-gray-600 mb-3 line-clamp-2">{webinar.description}</p>
|
|
|
|
<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.presenter}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="w-4 h-4" />
|
|
{webinar.duration}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Eye className="w-4 h-4" />
|
|
{webinar.attendees}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-primary font-medium">
|
|
<span className="text-small">{getActionText()}</span>
|
|
<ArrowRight className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
className="cursor-pointer transition-all duration-300 hover:shadow-lg hover:transform hover:-translate-y-2 group overflow-hidden"
|
|
onClick={handleCardClick}
|
|
>
|
|
{/* Image */}
|
|
<div className="aspect-video relative overflow-hidden">
|
|
<ImageWithFallback
|
|
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">
|
|
<Badge className="bg-yellow-100 text-yellow-800 border border-yellow-200">
|
|
<Star className="w-3 h-3 mr-1" />
|
|
Featured
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
{/* Play Icon 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="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.category}
|
|
</Badge>
|
|
<span className="text-small text-gray-500">{formatDate(webinar.date)}</span>
|
|
</div>
|
|
|
|
<h3 className="text-h4 mb-3 line-clamp-2 group-hover:text-primary transition-colors">
|
|
{webinar.title}
|
|
</h3>
|
|
|
|
<p className="text-body text-gray-600 mb-4 line-clamp-2">
|
|
{webinar.description}
|
|
</p>
|
|
|
|
<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.presenter}</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>{webinar.duration}</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Eye className="w-4 h-4" />
|
|
<span>{webinar.attendees}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
|
<div className="flex items-center gap-1">
|
|
{webinar.tags.slice(0, 2).map((tag, index) => (
|
|
<Badge key={index} variant="outline" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-2 text-primary font-medium group-hover:translate-x-1 transition-transform">
|
|
<span className="text-small">{getActionText()}</span>
|
|
<ArrowRight className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{ backgroundColor: '#FFFFFF' }}>
|
|
{/* Hero Section with Background Image */}
|
|
<section className="relative h-[400px] overflow-hidden">
|
|
{/* Background Image */}
|
|
<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>
|
|
|
|
{/* Hero Content */}
|
|
<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>
|
|
|
|
{/* Statistics Strip at Bottom */}
|
|
<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">
|
|
{/* Search Bar */}
|
|
<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 webcasts..."
|
|
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>
|
|
|
|
{/* View Toggle and Sort */}
|
|
<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' ? 'var(--color-primary)' : 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' ? 'var(--color-primary)' : 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 with Sidebar */}
|
|
<section className="pb-16" style={{ backgroundColor: '#FFFFFF' }}>
|
|
<div className="section-margin-x">
|
|
<div className="grid grid-cols-12 gap-8">
|
|
{/* Left Sidebar - Sticky 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">
|
|
{/* Filter Header */}
|
|
<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: 'var(--color-primary)' }} />
|
|
</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 filter-clear-btn"
|
|
>
|
|
<X className="w-3 h-3 mr-1" />
|
|
Clear
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Content */}
|
|
<div className="p-4">
|
|
<div className="space-y-6">
|
|
{/* Category Filter */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Category
|
|
</label>
|
|
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Categories" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{categories.map((category) => (
|
|
<SelectItem key={category} value={category} className="text-small">
|
|
{category}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Format Filter */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Format
|
|
</label>
|
|
<Select value={selectedFormat} onValueChange={setSelectedFormat}>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Formats" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{formats.map((format) => (
|
|
<SelectItem key={format} value={format} className="text-small">
|
|
{format}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Level Filter */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-2 font-medium text-gray-700">
|
|
Level
|
|
</label>
|
|
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
|
|
<SelectTrigger className="w-full text-small h-9 border-gray-300 hover:border-gray-400 transition-colors">
|
|
<SelectValue placeholder="All Levels" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{levels.map((level) => (
|
|
<SelectItem key={level} value={level} className="text-small">
|
|
{level}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* Status Filter - Multi-select Pills */}
|
|
<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 - Slider */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-3 font-medium text-gray-700">
|
|
Duration
|
|
</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 className="mt-1 text-center text-xs text-gray-400">
|
|
{durationRange[0] === 0 && durationRange[1] === 120
|
|
? 'All durations'
|
|
: `${durationRange[0]}-${durationRange[1]} minutes`
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Attendee Count Filter - Slider */}
|
|
<div className="filter-section">
|
|
<label className="block text-small mb-3 font-medium text-gray-700">
|
|
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 className="mt-1 text-center text-xs text-gray-400">
|
|
{attendeeRange[0] === 0 && attendeeRange[1] === 5000
|
|
? 'Any size'
|
|
: `${attendeeRange[0].toLocaleString()}-${attendeeRange[1].toLocaleString()}+`
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Main Content */}
|
|
<div className="col-span-12 lg:col-span-9">
|
|
{/* Results Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="text-body text-gray-600">
|
|
Showing {currentWebinars.length} of {filteredWebinars.length} webcasts
|
|
</div>
|
|
<div className="text-small text-gray-500">
|
|
Page {currentPage} of {totalPages}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<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 webcasts 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>
|
|
) : (
|
|
<>
|
|
{/* Grid View */}
|
|
{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>
|
|
) : (
|
|
/* List View */
|
|
<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));
|
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}}
|
|
disabled={currentPage === 1}
|
|
className="flex items-center gap-1 border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
Previous
|
|
</Button>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{Array.from({ length: totalPages }, (_, i) => {
|
|
const page = i + 1;
|
|
// Show limited pages for better UX
|
|
if (totalPages > 7) {
|
|
const showPage =
|
|
page === 1 ||
|
|
page === totalPages ||
|
|
(page >= currentPage - 1 && page <= currentPage + 1);
|
|
|
|
if (!showPage) {
|
|
if (page === currentPage - 2 || page === currentPage + 2) {
|
|
return <span key={page} className="px-2">...</span>;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
key={page}
|
|
variant={currentPage === page ? "default" : "outline"}
|
|
size="sm"
|
|
onClick={() => {
|
|
setCurrentPage(page);
|
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}}
|
|
className={`min-w-10 ${currentPage === page
|
|
? 'bg-blue-600 text-white hover:bg-blue-700'
|
|
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{page}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
setCurrentPage(prev => Math.min(totalPages, prev + 1));
|
|
containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}}
|
|
disabled={currentPage === totalPages}
|
|
className="flex items-center gap-1 border-gray-300 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Next
|
|
<ChevronRight className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Webcast CTA Banner */}
|
|
<WebcastCTABanner />
|
|
</div>
|
|
);
|
|
} |