import { useState } from "react"; import conferencesData from "@/data/conferences.yml"; import { Conference } from "@/types/conference"; import { Calendar as CalendarIcon, Tag, X, Plus } from "lucide-react"; // Added X and Plus imports import { Calendar } from "@/components/ui/calendar"; import { parseISO, format, isValid, isSameMonth, isSameYear, isSameDay, isSameWeek } from "date-fns"; import { Toggle } from "@/components/ui/toggle"; import Header from "@/components/Header"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; const categoryColors: Record = { "machine-learning": "bg-purple-500", "computer-vision": "bg-orange-500", "natural-language-processing": "bg-blue-500", "robotics": "bg-green-500", "signal-processing": "bg-cyan-500", "data-mining": "bg-pink-500", "reinforcement-learning": "bg-yellow-500", "automated-planning": "bg-amber-500", "other": "bg-gray-500" }; const categoryNames: Record = { "machine-learning": "Machine Learning", "computer-vision": "Computer Vision", "natural-language-processing": "NLP", "robotics": "Robotics", "signal-processing": "Speech/Signal Processing", "data-mining": "Data Mining", "reinforcement-learning": "Reinforcement Learning", "automated-planning": "Automated Planning", "other": "Other" }; // Add this array to maintain the exact order we want const orderedCategories = [ "machine-learning", "computer-vision", "natural-language-processing", "robotics", "reinforcement-learning", "signal-processing", "data-mining", "automated-planning", "other" ] as const; const mapLegacyTag = (tag: string): string => { const legacyTagMapping: Record = { "web-search": "other", "human-computer-interaction": "other", "computer-graphics": "other", // reinforcement-learning is already a proper tag, so no mapping needed // Add any other legacy mappings here }; return legacyTagMapping[tag] || tag; }; const CalendarPage = () => { const [selectedDate, setSelectedDate] = useState(new Date()); const [isYearView, setIsYearView] = useState(true); const [currentMonth, setCurrentMonth] = useState(new Date()); const [searchQuery, setSearchQuery] = useState(""); const [selectedDayEvents, setSelectedDayEvents] = useState<{ date: Date | null, events: { deadlines: Conference[], conferences: Conference[] } }>({ date: null, events: { deadlines: [], conferences: [] } }); const [selectedCategories, setSelectedCategories] = useState>( new Set(orderedCategories) ); const [showDeadlines, setShowDeadlines] = useState(true); const [currentYear, setCurrentYear] = useState(new Date().getFullYear()); const safeParseISO = (dateString: string | undefined | number): Date | null => { if (!dateString) return null; if (dateString === 'TBD') return null; const isDate = (value: any): value is Date => { return value && Object.prototype.toString.call(value) === '[object Date]'; }; if (isDate(dateString)) return dateString; try { if (typeof dateString === 'object') { return null; } const dateStr = typeof dateString === 'number' ? dateString.toString() : dateString; let normalizedDate = dateStr; const parts = dateStr.split('-'); if (parts.length === 3) { normalizedDate = `${parts[0]}-${parts[1].padStart(2, '0')}-${parts[2].padStart(2, '0')}`; } const parsedDate = parseISO(normalizedDate); return isValid(parsedDate) ? parsedDate : null; } catch (error) { console.error("Error parsing date:", dateString); return null; } }; const getEvents = (date: Date) => { return conferencesData.filter((conf: Conference) => { // Map the conference tags to our new category system const mappedTags = conf.tags?.map(mapLegacyTag) || []; const matchesSearch = searchQuery === "" || conf.title.toLowerCase().includes(searchQuery.toLowerCase()) || (conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase())); // Use mapped tags for category matching const matchesCategory = mappedTags.some(tag => selectedCategories.has(tag)); const deadlineDate = safeParseISO(conf.deadline); const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); // Check if a date is in the current year const isInCurrentYear = (date: Date | null) => { return date && date.getFullYear() === currentYear; }; // If showing deadlines and no categories selected, only show deadlines if (showDeadlines && selectedCategories.size === 0) { return deadlineDate && isInCurrentYear(deadlineDate) && matchesSearch; } if (!matchesSearch || (!matchesCategory && selectedCategories.size > 0)) return false; // Check if either deadline or conference dates are in the current year const deadlineInYear = showDeadlines && deadlineDate && isInCurrentYear(deadlineDate); const conferenceInYear = (startDate && isInCurrentYear(startDate)) || (endDate && isInCurrentYear(endDate)) || (startDate && endDate && startDate.getFullYear() <= currentYear && endDate.getFullYear() >= currentYear); return deadlineInYear || (selectedCategories.size > 0 && conferenceInYear); }); }; const getDayEvents = (date: Date) => { const deadlines = showDeadlines ? conferencesData.filter(conf => { const deadlineDate = safeParseISO(conf.deadline); const matchesCategory = selectedCategories.size === 0 ? true : (Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag))); return deadlineDate && isSameDay(deadlineDate, date) && deadlineDate.getFullYear() === currentYear && matchesCategory; }) : []; const conferences = selectedCategories.size > 0 ? conferencesData.filter(conf => { const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); const matchesCategory = Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag)); if (!matchesCategory) return false; if (startDate && endDate) { return startDate.getFullYear() <= currentYear && endDate.getFullYear() >= currentYear && date >= startDate && date <= endDate; } else if (startDate) { return startDate.getFullYear() === currentYear && isSameDay(startDate, date); } return false; }) : []; return { deadlines: deadlines.filter(conf => searchQuery === "" || conf.title.toLowerCase().includes(searchQuery.toLowerCase()) || (conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase())) ), conferences: conferences.filter(conf => searchQuery === "" || conf.title.toLowerCase().includes(searchQuery.toLowerCase()) || (conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase())) ) }; }; const renderEventPreview = (events: { deadlines: Conference[], conferences: Conference[] }) => { if (events.deadlines.length === 0 && events.conferences.length === 0) return null; return (
{events.deadlines.length > 0 && (

Deadlines:

{events.deadlines.map(conf => (
{conf.title}
))}
)} {events.conferences.length > 0 && (

Conferences:

{events.conferences.map(conf => (
{conf.title}
))}
)}
); }; const isEndOfWeek = (date: Date) => date.getDay() === 6; // Saturday const isStartOfWeek = (date: Date) => date.getDay() === 0; // Sunday const getConferenceLineStyle = (date: Date) => { // If only showing deadlines and no categories are selected, don't show any conference lines if (selectedCategories.size === 0 && showDeadlines) { return []; } return conferencesData .filter(conf => { const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); // Only show conference dates if categories are selected const matchesCategory = selectedCategories.size > 0 && (Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag))); return startDate && endDate && date >= startDate && date <= endDate && matchesCategory; }) .map(conf => { const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); if (!startDate || !endDate) return null; let style = "w-[calc(100%+1rem)] -left-2 relative"; if (isSameDay(date, startDate)) { style += " rounded-l-sm"; } if (isSameDay(date, endDate)) { style += " rounded-r-sm"; } const color = conf.tags && conf.tags[0] ? categoryColors[conf.tags[0]] : "bg-gray-500"; return { style, color }; }); }; const renderDayContent = (date: Date) => { const dayEvents = getDayEvents(date); const hasEvents = dayEvents.deadlines.length > 0 || dayEvents.conferences.length > 0; const conferenceStyles = getConferenceLineStyle(date); const hasDeadline = showDeadlines && dayEvents.deadlines.length > 0; const handleDayClick = (e: React.MouseEvent) => { e.preventDefault(); // Prevent default calendar behavior e.stopPropagation(); // Stop event propagation setSelectedDayEvents({ date, events: dayEvents }); }; return (
{format(date, 'd')}
{conferenceStyles.map((style, index) => (
))} {hasDeadline && (
)}
{hasEvents && ( {renderEventPreview(dayEvents)} )}
); }; const renderEventDetails = (conf: Conference) => { const deadlineDate = safeParseISO(conf.deadline); const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); return (

{conf.title}

{conf.full_name && (

{conf.full_name}

)}
{conf.link && ( Website )}
{deadlineDate && (
Deadline:
{format(deadlineDate, 'MMMM d, yyyy')}
{conf.timezone && (
Timezone: {conf.timezone}
)}
)} {startDate && (
Date:
{format(startDate, 'MMMM d')} {endDate ? ` - ${format(endDate, 'MMMM d, yyyy')}` : `, ${format(startDate, 'yyyy')}`}
)} {conf.place && (
Location: {conf.place}
)} {conf.note && (
Note:
)}
{Array.isArray(conf.tags) && conf.tags.map((tag) => ( {categoryNames[tag] || tag} ))}
); }; const categories = orderedCategories .filter(category => conferencesData.some(conf => conf.tags?.includes(category)) ) .map(category => [category, categoryColors[category]]); const renderLegend = () => { return (

Click to toggle submission deadlines

{/* Divider */} {categories.map(([tag, color]) => (

Click to toggle {categoryNames[tag] || tag}

))} {selectedCategories.size < Object.keys(categoryColors).length && ( )} {selectedCategories.size > 0 && ( )}
); }; const renderViewToggle = () => { return (
{isYearView && (
{currentYear}
)}
); }; const handleMonthChange = (month: Date) => { setCurrentMonth(month); setSelectedDate(month); }; return (
{searchQuery && (

Search Results for "{searchQuery}"

{getEvents(new Date()).map((conf: Conference) => (
{ const deadlineDate = safeParseISO(conf.deadline); const startDate = safeParseISO(conf.start); if (deadlineDate) { setSelectedDate(deadlineDate); setSelectedDayEvents({ date: deadlineDate, events: getDayEvents(deadlineDate) }); } else if (startDate) { setSelectedDate(startDate); setSelectedDayEvents({ date: startDate, events: getDayEvents(startDate) }); } }} >

{conf.title}

{conf.full_name && (

{conf.full_name}

)}
{conf.deadline && conf.deadline !== 'TBD' && ( Deadline: {format(safeParseISO(conf.deadline)!, 'MMM d, yyyy')} )}
{Array.isArray(conf.tags) && conf.tags.length > 0 && (
{conf.tags.map(tag => ( {categoryNames[tag] || tag} ))}
)}
))} {getEvents(new Date()).length === 0 && (

No conferences found matching your search.

)}
)}
{renderViewToggle()} {renderLegend()}
{ const isOutsideDay = date.getMonth() !== displayMonth.getMonth(); if (isOutsideDay) { return null; } return (
{renderDayContent(date)}
); }, }} classNames={{ months: `grid ${isYearView ? 'grid-cols-3 gap-4' : ''} justify-center`, month: "space-y-4", caption: "flex justify-center pt-1 relative items-center mb-4", caption_label: "text-lg font-semibold", head_row: "flex", head_cell: "text-muted-foreground rounded-md w-10 font-normal text-[0.8rem]", row: "flex w-full mt-2", cell: "h-16 w-10 text-center text-sm p-0 relative focus-within:relative focus-within:z-20 hover:bg-neutral-50", day: "h-16 w-10 p-0 font-normal hover:bg-neutral-100 rounded-lg transition-colors", day_today: "bg-neutral-100 text-primary font-semibold", day_outside: "hidden", nav: "space-x-1 flex items-center", nav_button: isYearView ? "hidden" : "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", nav_button_previous: "absolute left-1", nav_button_next: "absolute right-1" }} />
setSelectedDayEvents({ date: null, events: { deadlines: [], conferences: [] } })} > Events for {selectedDayEvents.date ? format(selectedDayEvents.date, 'MMMM d, yyyy') : ''}
View conference details and deadlines for this date.
{selectedDayEvents.events.deadlines.length > 0 && (

Submission Deadlines

{selectedDayEvents.events.deadlines.map(conf => (
{renderEventDetails(conf)}
))}
)} {selectedDayEvents.events.conferences.length > 0 && (

Conferences

{selectedDayEvents.events.conferences.map(conf => (
{renderEventDetails(conf)}
))}
)}
); }; export default CalendarPage;