import { useState } from "react"; import conferencesData from "@/data/conferences.yml"; import { Conference } from "@/types/conference"; import { Calendar as CalendarIcon, Tag } from "lucide-react"; 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 = { "computer-vision": "bg-orange-500", "machine-learning": "bg-purple-500", "natural-language-processing": "bg-blue-500", "robotics": "bg-green-500", "data-mining": "bg-pink-500", "signal-processing": "bg-cyan-500", "human-computer-interaction": "bg-indigo-500", "web-search": "bg-yellow-500", }; const categoryNames: Record = { "computer-vision": "Computer Vision", "machine-learning": "Machine Learning", "natural-language-processing": "NLP", "robotics": "Robotics", "data-mining": "Data Mining", "signal-processing": "Signal Processing", "human-computer-interaction": "HCI", "web-search": "Web Search", }; const CalendarPage = () => { const [selectedDate, setSelectedDate] = useState(new Date()); const [isYearView, setIsYearView] = useState(true); 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()); const safeParseISO = (dateString: string | undefined | number): Date | null => { if (!dateString) return null; if (dateString === 'TBD') return null; // If it's already a Date object, return it if (dateString instanceof Date) 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) => { const matchesSearch = searchQuery === "" || conf.title.toLowerCase().includes(searchQuery.toLowerCase()) || (conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase())); // Add category filter const matchesCategory = selectedCategories.size === 0 || (Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag))); if (!matchesSearch || !matchesCategory) return false; const deadlineDate = safeParseISO(conf.deadline); const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); const dateMatches = isYearView ? isSameYear : isSameMonth; const deadlineInPeriod = deadlineDate && dateMatches(deadlineDate, date); let conferenceInPeriod = false; if (startDate && endDate) { let currentDate = new Date(startDate); while (currentDate <= endDate) { if (dateMatches(currentDate, date)) { conferenceInPeriod = true; break; } currentDate.setDate(currentDate.getDate() + 1); } } else if (startDate) { conferenceInPeriod = dateMatches(startDate, date); } return deadlineInPeriod || conferenceInPeriod; }); }; const getDayEvents = (date: Date) => { const deadlines = conferencesData.filter(conf => { const deadlineDate = safeParseISO(conf.deadline); const matchesCategory = selectedCategories.size === 0 || (Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag))); return deadlineDate && isSameDay(deadlineDate, date) && matchesCategory; }); const conferences = conferencesData.filter(conf => { const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); const matchesCategory = selectedCategories.size === 0 || (Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag))); return startDate && endDate && date >= startDate && date <= endDate && matchesCategory; }); return { deadlines, conferences }; }; 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}
))}
)}
); }; // Add these helper functions at the top of the file const isEndOfWeek = (date: Date) => date.getDay() === 6; // Saturday const isStartOfWeek = (date: Date) => date.getDay() === 0; // Sunday // Update the getConferenceLineStyle function const getConferenceLineStyle = (date: Date) => { return conferencesData .filter(conf => { const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); 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"; // Add specific styles for start, middle, and end days if (isSameDay(date, startDate)) { style += " rounded-l-sm"; } if (isSameDay(date, endDate)) { style += " rounded-r-sm"; } // Get the color based on the first tag const color = conf.tags && conf.tags[0] ? categoryColors[conf.tags[0]] : "bg-gray-500"; return { style, color }; }) .filter(Boolean); }; // Update the renderDayContent function const renderDayContent = (date: Date) => { const dayEvents = getDayEvents(date); const hasEvents = dayEvents.deadlines.length > 0 || dayEvents.conferences.length > 0; // Get conference line styles first const conferenceStyles = getConferenceLineStyle(date); // Get deadline style const hasDeadline = 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 (
{/* Day number at the top with moderate space */}
{format(date, 'd')}
{/* Event indicator lines closer to the day number */}
{/* Conference lines at the bottom (rendered first) */} {conferenceStyles.map((style, index) => (
))} {/* Deadline lines on top */} {hasDeadline && (
)}
{/* Tooltip trigger */} {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 = Object.entries(categoryColors).filter(([category]) => conferencesData.some(conf => conf.tags?.includes(category)) ); const renderLegend = () => { const categories = Object.entries(categoryColors); return (
Submission Deadlines
{categories.map(([category, color]) => { const isSelected = selectedCategories.has(category); return ( ) })} {selectedCategories.size > 0 && ( )}
); }; return (
{/* Add a search results section when there's a search query */} {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.

)}
)}

Calendar Overview

setIsYearView(false)} variant="outline" > Month View setIsYearView(true)} variant="outline" > Year View
{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: "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;