import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { CalendarDays, Globe, Tag, Clock, AlarmClock, CalendarPlus } from "lucide-react"; import { Conference } from "@/types/conference"; import { formatDistanceToNow, parseISO, isValid, format, parse, addDays } from "date-fns"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useState, useEffect } from "react"; interface ConferenceDialogProps { conference: Conference; open: boolean; onOpenChange: (open: boolean) => void; } const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => { const deadlineDate = conference.deadline && conference.deadline !== 'TBD' ? parseISO(conference.deadline) : null; const [countdown, setCountdown] = useState<string>(''); useEffect(() => { const calculateTimeLeft = () => { if (!deadlineDate || !isValid(deadlineDate)) { setCountdown('TBD'); return; } const now = new Date().getTime(); const difference = deadlineDate.getTime() - now; if (difference <= 0) { setCountdown('Deadline passed'); return; } const days = Math.floor(difference / (1000 * 60 * 60 * 24)); const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((difference % (1000 * 60)) / 1000); setCountdown(`${days}d ${hours}h ${minutes}m ${seconds}s`); }; // Calculate immediately calculateTimeLeft(); // Update every second const timer = setInterval(calculateTimeLeft, 1000); // Cleanup interval on component unmount return () => clearInterval(timer); }, [deadlineDate]); const getCountdownColor = () => { if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600"; const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)); if (daysRemaining <= 7) return "text-red-600"; if (daysRemaining <= 30) return "text-orange-600"; return "text-green-600"; }; const parseDateFromString = (dateStr: string) => { try { // Handle formats like "October 19-25, 2025" or "Sept 9-12, 2025" const [monthDay, year] = dateStr.split(", "); const [month, dayRange] = monthDay.split(" "); const [startDay] = dayRange.split("-"); // Construct a date string in a format that can be parsed const dateString = `${month} ${startDay} ${year}`; const date = parse(dateString, 'MMMM d yyyy', new Date()); if (!isValid(date)) { // Try alternative format for abbreviated months return parse(dateString, 'MMM d yyyy', new Date()); } return date; } catch (error) { console.error("Error parsing date:", error); return new Date(); } }; const createCalendarEvent = (type: 'google' | 'apple') => { try { if (!conference.deadline || conference.deadline === 'TBD') { throw new Error('No valid deadline found'); } // Parse the deadline date const deadlineDate = parseISO(conference.deadline); if (!isValid(deadlineDate)) { throw new Error('Invalid deadline date'); } // Create an end date 1 hour after the deadline const endDate = new Date(deadlineDate.getTime() + (60 * 60 * 1000)); const formatDateForGoogle = (date: Date) => format(date, "yyyyMMdd'T'HHmmss'Z'"); const formatDateForApple = (date: Date) => format(date, "yyyyMMdd'T'HHmmss'Z'"); const title = encodeURIComponent(`${conference.title} deadline`); const location = encodeURIComponent(conference.place); const description = encodeURIComponent( `Paper Submission Deadline for ${conference.full_name || conference.title}\n` + (conference.abstract_deadline ? `Abstract Deadline: ${conference.abstract_deadline}\n` : '') + `Dates: ${conference.date}\n` + `Location: ${conference.place}\n` + (conference.link ? `Website: ${conference.link}` : '') ); if (type === 'google') { const url = `https://calendar.google.com/calendar/render?action=TEMPLATE` + `&text=${title}` + `&dates=${formatDateForGoogle(deadlineDate)}/${formatDateForGoogle(endDate)}` + `&details=${description}` + `&location=${location}` + `&sprop=website:${encodeURIComponent(conference.link || '')}`; window.open(url, '_blank'); } else { const url = `data:text/calendar;charset=utf8,BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT URL:${conference.link || ''} DTSTART:${formatDateForApple(deadlineDate)} DTEND:${formatDateForApple(endDate)} SUMMARY:${title} DESCRIPTION:${description} LOCATION:${location} END:VEVENT END:VCALENDAR`; const link = document.createElement('a'); link.href = url; link.download = `${conference.title.toLowerCase().replace(/\s+/g, '-')}-deadline.ics`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } catch (error) { console.error("Error creating calendar event:", error); alert("Sorry, there was an error creating the calendar event. Please try again."); } }; const generateGoogleMapsUrl = (venue: string | undefined, place: string): string => { return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(venue || place)}`; }; return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-xl"> <DialogHeader> <DialogTitle>{conference.title} {conference.year}</DialogTitle> <DialogDescription> {conference.full_name} </DialogDescription> </DialogHeader> <div className="space-y-4"> <div className="flex items-start gap-2"> <CalendarDays className="h-5 w-5 mt-0.5 text-gray-500" /> <div> <p className="font-medium">Dates</p> <p className="text-sm text-gray-500">{conference.date}</p> </div> </div> <div className="flex items-start gap-2"> <Clock className="h-5 w-5 mt-0.5 text-gray-500" /> <div className="space-y-2 flex-1"> <p className="font-medium">Important Deadlines</p> <div className="text-sm text-gray-500 space-y-2"> {conference.abstract_deadline && ( <div className="bg-gray-100 rounded-md p-2"> <p>Abstract: {parseISO(conference.abstract_deadline) && isValid(parseISO(conference.abstract_deadline)) ? format(parseISO(conference.abstract_deadline), "MMMM d, yyyy") : conference.abstract_deadline} </p> </div> )} <div className="bg-gray-100 rounded-md p-2"> <p>Submission: {conference.deadline && conference.deadline !== 'TBD' && isValid(parseISO(conference.deadline)) ? format(parseISO(conference.deadline), "MMMM d, yyyy") : conference.deadline} </p> </div> {conference.commitment_deadline && ( <div className="bg-gray-100 rounded-md p-2"> <p>Commitment: {isValid(parseISO(conference.commitment_deadline)) ? format(parseISO(conference.commitment_deadline), "MMMM d, yyyy") : conference.commitment_deadline} </p> </div> )} {conference.review_release_date && ( <div className="bg-gray-100 rounded-md p-2"> <p>Reviews Released: {isValid(parseISO(conference.review_release_date)) ? format(parseISO(conference.review_release_date), "MMMM d, yyyy") : conference.review_release_date} </p> </div> )} {(conference.rebuttal_period_start || conference.rebuttal_period_end) && ( <div className="bg-gray-100 rounded-md p-2"> <p>Rebuttal Period: {conference.rebuttal_period_start && isValid(parseISO(conference.rebuttal_period_start)) ? format(parseISO(conference.rebuttal_period_start), "MMMM d, yyyy") : conference.rebuttal_period_start} - {conference.rebuttal_period_end && isValid(parseISO(conference.rebuttal_period_end)) ? format(parseISO(conference.rebuttal_period_end), "MMMM d, yyyy") : conference.rebuttal_period_end} </p> </div> )} {conference.final_decision_date && ( <div className="bg-gray-100 rounded-md p-2"> <p>Final Decision: {isValid(parseISO(conference.final_decision_date)) ? format(parseISO(conference.final_decision_date), "MMMM d, yyyy") : conference.final_decision_date} </p> </div> )} </div> </div> </div> <div className="flex items-start gap-2"> <Globe className="h-5 w-5 mt-0.5 text-gray-500" /> <div> <p className="font-medium">Location</p> <p className="text-sm text-gray-500">{conference.place}</p> {conference.venue && ( <p className="text-sm text-gray-500">{conference.venue}</p> )} </div> </div> <div className="flex items-center"> <AlarmClock className={`h-5 w-5 mr-3 flex-shrink-0 ${getCountdownColor()}`} /> <div> <span className={`font-medium ${getCountdownColor()}`}> {countdown} </span> {deadlineDate && isValid(deadlineDate) && ( <div className="text-sm text-neutral-500"> {format(deadlineDate, "MMMM d, yyyy 'at' HH:mm:ss")} {conference.timezone} </div> )} </div> </div> {Array.isArray(conference.tags) && conference.tags.length > 0 && ( <div className="flex flex-wrap gap-2"> {conference.tags.map((tag) => ( <span key={tag} className="tag"> <Tag className="h-3 w-3 mr-1" /> {tag} </span> ))} </div> )} {conference.note && ( <div className="text-sm text-neutral-600 mt-2 p-3 bg-neutral-50 rounded-lg" dangerouslySetInnerHTML={{ __html: conference.note }} /> )} <div className="flex items-center justify-between pt-2"> {conference.link && ( <Button variant="ghost" size="sm" className="text-base text-primary hover:underline p-0" asChild > <a href={conference.link} target="_blank" rel="noopener noreferrer" > Visit website </a> </Button> )} <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="sm" className="text-sm focus-visible:ring-0 focus:outline-none" > <CalendarPlus className="h-4 w-4 mr-2" /> Add to Calendar </Button> </DropdownMenuTrigger> <DropdownMenuContent className="bg-white" align="end"> <DropdownMenuItem className="text-neutral-800 hover:bg-neutral-100" onClick={() => createCalendarEvent('google')} > Add to Google Calendar </DropdownMenuItem> <DropdownMenuItem className="text-neutral-800 hover:bg-neutral-100" onClick={() => createCalendarEvent('apple')} > Add to Apple Calendar </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> </div> </DialogContent> </Dialog> ); }; export default ConferenceDialog;