src/pages/Index.tsx (254 lines of code) (raw):

import Header from "@/components/Header"; import FilterBar from "@/components/FilterBar"; import ConferenceCard from "@/components/ConferenceCard"; import conferencesData from "@/data/conferences.yml"; import { Conference } from "@/types/conference"; import { useState, useMemo, useEffect } from "react"; import { Switch } from "@/components/ui/switch" import { parseISO, isValid, isPast } from "date-fns"; import { extractCountry } from "@/utils/countryExtractor"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { X, ChevronRight, Filter, Globe } from "lucide-react"; import { getAllCountries } from "@/utils/countryExtractor"; import { getDeadlineInLocalTime } from "@/utils/dateUtils"; import { sortConferencesByDeadline } from "@/utils/conferenceUtils"; const Index = () => { const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set()); const [selectedCountries, setSelectedCountries] = useState<Set<string>>(new Set()); const [searchQuery, setSearchQuery] = useState(""); const [showPastConferences, setShowPastConferences] = useState(false); // Category buttons configuration const categoryButtons = [ { id: "machine-learning", label: "Machine Learning" }, { id: "lifelong-learning", label: "Lifelong Learning" }, { id: "robotics", label: "Robotics" }, { id: "computer-vision", label: "Computer Vision" }, { id: "web-search", label: "Web Search" }, { id: "data-mining", label: "Data Mining" }, { id: "natural-language-processing", label: "Natural Language Processing" }, { id: "signal-processing", label: "Signal Processing" }, { id: "human-computer-interaction", label: "Human Computer Interaction" }, { id: "computer-graphics", label: "Computer Graphics" }, { id: "mathematics", label: "Mathematics" }, { id: "reinforcement-learning", label: "Reinforcement Learning" }, ]; const filteredConferences = useMemo(() => { if (!Array.isArray(conferencesData)) { console.error("Conferences data is not an array:", conferencesData); return []; } return conferencesData .filter((conf: Conference) => { // Filter by deadline (past/future) const deadlineDate = conf.deadline && conf.deadline !== 'TBD' ? parseISO(conf.deadline) : null; const isUpcoming = !deadlineDate || !isValid(deadlineDate) || !isPast(deadlineDate); if (!showPastConferences && !isUpcoming) return false; // Filter by tags const matchesTags = selectedTags.size === 0 || (Array.isArray(conf.tags) && conf.tags.some(tag => selectedTags.has(tag))); // Filter by countries const matchesCountry = selectedCountries.size === 0 || (conf.country && selectedCountries.has(conf.country)); // Filter by search query const matchesSearch = searchQuery === "" || conf.title.toLowerCase().includes(searchQuery.toLowerCase()) || (conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase())); return matchesTags && matchesCountry && matchesSearch; }) .sort((a: Conference, b: Conference) => { const aDeadline = getDeadlineInLocalTime(a.deadline, a.timezone); const bDeadline = getDeadlineInLocalTime(b.deadline, b.timezone); if (aDeadline && bDeadline) { return aDeadline.getTime() - bDeadline.getTime(); } // Handle cases where one or both deadlines are invalid if (!aDeadline && !bDeadline) return 0; if (!aDeadline) return 1; if (!bDeadline) return -1; return 0; }); }, [selectedTags, selectedCountries, searchQuery, showPastConferences]); // Update handleTagsChange to handle multiple tags const handleTagsChange = (newTags: Set<string>) => { setSelectedTags(newTags); const searchParams = new URLSearchParams(window.location.search); if (newTags.size > 0) { searchParams.set('tags', Array.from(newTags).join(',')); } else { searchParams.delete('tags'); } window.history.replaceState({}, '', `${window.location.pathname}?${searchParams}`); }; const handleCountriesChange = (newCountries: Set<string>) => { setSelectedCountries(newCountries); const searchParams = new URLSearchParams(window.location.search); if (newCountries.size > 0) { searchParams.set('countries', Array.from(newCountries).join(',')); } else { searchParams.delete('countries'); } window.history.replaceState({}, '', `${window.location.pathname}?${searchParams}`); }; // Toggle a single tag const toggleTag = (tag: string) => { const newTags = new Set(selectedTags); if (newTags.has(tag)) { newTags.delete(tag); } else { newTags.add(tag); } handleTagsChange(newTags); }; // Load filters from URL on initial render useEffect(() => { const searchParams = new URLSearchParams(window.location.search); const tagsParam = searchParams.get('tags'); const countriesParam = searchParams.get('countries'); if (tagsParam) { const tags = tagsParam.split(','); setSelectedTags(new Set(tags)); } if (countriesParam) { const countries = countriesParam.split(','); setSelectedCountries(new Set(countries)); } }, []); if (!Array.isArray(conferencesData)) { return <div>Loading conferences...</div>; } return ( <div className="min-h-screen bg-neutral-light"> <Header onSearch={setSearchQuery} showEmptyMessage={false} /> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="space-y-4 py-4"> {/* Category filter buttons */} <div className="bg-white shadow rounded-lg p-4"> <div className="flex flex-wrap gap-2"> {categoryButtons.map(category => ( <button key={category.id} className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${ selectedTags.has(category.id) ? 'bg-blue-100 text-blue-800 hover:bg-blue-200' : 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200' }`} onClick={() => { const newTags = new Set(selectedTags); if (newTags.has(category.id)) { newTags.delete(category.id); } else { newTags.add(category.id); } handleTagsChange(newTags); }} > {category.label} </button> ))} </div> </div> {/* Controls row with past conferences toggle and country filter */} <div className="flex flex-wrap items-center gap-4"> <div className="flex items-center gap-2 bg-white p-2 rounded-md shadow-sm"> <label htmlFor="show-past" className="text-sm text-neutral-600"> Show past conferences </label> <Switch id="show-past" checked={showPastConferences} onCheckedChange={setShowPastConferences} /> </div> <div className="flex items-center gap-2 bg-white p-2 rounded-md shadow-sm"> <Popover> <PopoverTrigger asChild> <Button variant="outline" size="sm" className="h-8 gap-1"> <Globe className="h-4 w-4" /> Filter by Country </Button> </PopoverTrigger> <PopoverContent className="w-64 p-4 bg-white" align="start"> <div className="space-y-4"> <div> <div className="mb-4"> <h4 className="text-sm font-medium text-gray-800">Country</h4> </div> <div className="max-h-60 overflow-y-auto space-y-2 bg-white overscroll-contain touch-pan-y" style={{ WebkitOverflowScrolling: "touch" }} > {getAllCountries(conferencesData as Conference[]).map(country => ( <div key={country} className="flex items-center space-x-2 hover:bg-gray-50 p-1 rounded"> <Checkbox id={`country-${country}`} checked={selectedCountries.has(country)} onCheckedChange={() => { const newCountries = new Set(selectedCountries); if (newCountries.has(country)) { newCountries.delete(country); } else { newCountries.add(country); } handleCountriesChange(newCountries); }} /> <label htmlFor={`country-${country}`} className="text-sm font-medium text-gray-700 cursor-pointer w-full py-1" > {country} </label> </div> ))} </div> </div> </div> </PopoverContent> </Popover> {/* Display selected countries */} {Array.from(selectedCountries).map(country => ( <button key={country} className="inline-flex items-center px-3 py-1 rounded-full text-sm bg-blue-100 text-blue-800 hover:bg-blue-200 font-medium" onClick={() => { const newCountries = new Set(selectedCountries); newCountries.delete(country); handleCountriesChange(newCountries); }} > {country} <X className="ml-1 h-3 w-3" /> </button> ))} {/* Clear all filters button */} {(selectedTags.size > 0 || selectedCountries.size > 0) && ( <Button variant="ghost" size="sm" onClick={() => { handleTagsChange(new Set()); handleCountriesChange(new Set()); }} className="text-neutral-500 hover:text-neutral-700" > Clear all filters </Button> )} </div> </div> </div> </div> <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> {filteredConferences.length === 0 && ( <div className="bg-amber-50 border border-amber-200 text-amber-800 rounded-md p-4 mb-6"> <p className="text-center"> There are no upcoming conferences for the selected categories - enable "Show past conferences" to see previous ones </p> </div> )} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {filteredConferences.map((conference: Conference) => ( <ConferenceCard key={conference.id} {...conference} /> ))} </div> </main> </div> ); }; export default Index;