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 } from "date-fns"; import { Toggle } from "@/components/ui/toggle"; import Header from "@/components/Header"; import FilterBar from "@/components/FilterBar"; 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 [selectedTag, setSelectedTag] = useState("All"); const [selectedDayEvents, setSelectedDayEvents] = useState<{ date: Date | null, events: { deadlines: Conference[], conferences: Conference[] } }>({ date: null, events: { deadlines: [], conferences: [] } }); const safeParseISO = (dateString: string | undefined | number): Date | null => { if (!dateString) return null; if (dateString === 'TBD') return null; 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())); const matchesTag = selectedTag === "All" || (Array.isArray(conf.tags) && conf.tags.includes(selectedTag)); if (!matchesSearch || !matchesTag) 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) => { return conferencesData.reduce((acc, conf) => { const matchesSearch = searchQuery === "" || conf.title.toLowerCase().includes(searchQuery.toLowerCase()) || (conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase())); const matchesTag = selectedTag === "All" || (Array.isArray(conf.tags) && conf.tags.includes(selectedTag)); if (!matchesSearch || !matchesTag) { return acc; } const deadlineDate = safeParseISO(conf.deadline); const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); if (deadlineDate && isSameDay(deadlineDate, date)) { acc.deadlines.push(conf); } if (startDate && endDate) { if (date >= startDate && date <= endDate) { acc.conferences.push(conf); } } else if (startDate && isSameDay(startDate, date)) { acc.conferences.push(conf); } return acc; }, { deadlines: [], conferences: [] } as { deadlines: Conference[], conferences: Conference[] }); }; 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 const isSameWeek = (date1: Date, date2: Date) => { const diff = Math.abs(date1.getTime() - date2.getTime()); const diffDays = Math.floor(diff / (1000 * 60 * 60 * 24)); return diffDays <= 6 && Math.floor(date1.getTime() / (1000 * 60 * 60 * 24 * 7)) === Math.floor(date2.getTime() / (1000 * 60 * 60 * 24 * 7)); }; // Update the getConferenceLineStyle function const getConferenceLineStyle = (date: Date) => { const styles = []; for (const conf of conferencesData) { const startDate = safeParseISO(conf.start); const endDate = safeParseISO(conf.end); if (startDate && endDate && date >= startDate && date <= endDate) { const isFirst = isSameDay(date, startDate); const isLast = isSameDay(date, endDate); // Check if previous and next days are part of the same conference and week const prevDate = new Date(date); prevDate.setDate(date.getDate() - 1); const nextDate = new Date(date); nextDate.setDate(date.getDate() + 1); const hasPrevDay = prevDate >= startDate && isSameWeek(date, prevDate); const hasNextDay = nextDate <= endDate && isSameWeek(date, nextDate); let lineStyle = "h-1 absolute bottom-0"; if (hasPrevDay && hasNextDay) { // Middle of a sequence lineStyle += " w-[calc(100%+1rem)] -left-2"; } else if (hasPrevDay) { // End of a sequence lineStyle += " w-[calc(100%+0.5rem)] left-0"; } else if (hasNextDay) { // Start of a sequence lineStyle += " w-[calc(100%+0.5rem)] right-0"; } else { // Single day lineStyle += " w-full"; } // Get the color based on the first tag or default to purple const color = conf.tags?.[0] ? categoryColors[conf.tags[0]] : "bg-purple-500"; styles.push({ style: lineStyle, color: color }); } } return styles; }; // 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 deadlineStyle = hasDeadline ? "h-1 w-full bg-red-500" : ""; return (
{/* Day number at the top */}
{format(date, 'd')}
{/* Event indicator lines at the bottom */}
{/* Conference lines */} {conferenceStyles.map((style, index) => (
))} {/* Deadline line */} {deadlineStyle &&
}
{/* Tooltip trigger covering the whole cell */} {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}

)}
{deadlineDate && (

Submission Deadline: {format(deadlineDate, 'MMMM d, yyyy')}

)} {startDate && (

Conference Date: {format(startDate, 'MMMM d')} {endDate ? ` - ${format(endDate, 'MMMM d, yyyy')}` : `, ${format(startDate, 'yyyy')}`}

)}
{Array.isArray(conf.tags) && conf.tags.map((tag) => ( {tag} ))}
); }; const categories = Object.entries(categoryColors).filter(([category]) => conferencesData.some(conf => conf.tags?.includes(category)) ); return (

Calendar Overview

setIsYearView(false)} variant="outline" > Month setIsYearView(true)} variant="outline" > Year
Submission Deadlines
{categories.map(([category, color]) => (
{categoryNames[category]}
))}
{ const isOutsideDay = date.getMonth() !== props.displayMonth.getMonth(); if (isOutsideDay) { return null; } return ( ); }, }} 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-10 w-10 text-center text-sm p-0 relative focus-within:relative focus-within:z-20 hover:bg-neutral-50", day: "h-10 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') : ''}
{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;