huggingface/ai-deadlines
781
1import {2 Dialog,3 DialogContent,4 DialogHeader,5 DialogTitle,6 DialogDescription,7} from "@/components/ui/dialog";8import { CalendarDays, Globe, Tag, Clock, AlarmClock, CalendarPlus } from "lucide-react";9import { Conference } from "@/types/conference";10import { isValid, format } from "date-fns";11import { Button } from "@/components/ui/button";12import {13 DropdownMenu,14 DropdownMenuContent,15 DropdownMenuItem,16 DropdownMenuTrigger,17} from "@/components/ui/dropdown-menu";18import { useState, useEffect } from "react";19import { getDeadlineInLocalTime } from '@/utils/dateUtils';20import { getNextUpcomingDeadline, getUpcomingDeadlines, getDaysRemaining, getCountdownColorClass } from '@/utils/deadlineUtils';21 22interface ConferenceDialogProps {23 conference: Conference;24 open: boolean;25 onOpenChange: (open: boolean) => void;26}27 28const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => {29 console.log('Conference object:', conference);30 31 // Get upcoming deadlines and the next upcoming one32 const upcomingDeadlines = getUpcomingDeadlines(conference);33 const nextDeadline = getNextUpcomingDeadline(conference);34 const deadlineDate = nextDeadline ? getDeadlineInLocalTime(nextDeadline.date, nextDeadline.timezone || conference.timezone) : null;35 36 const [countdown, setCountdown] = useState<string>('');37 38 // Replace the current location string creation with this more verbose version39 const getLocationString = () => {40 console.log('Venue:', conference.venue);41 console.log('City:', conference.city);42 console.log('Country:', conference.country);43 44 if (conference.venue) {45 return conference.venue;46 }47 48 const cityCountryArray = [conference.city, conference.country].filter(Boolean);49 console.log('City/Country array after filter:', cityCountryArray);50 51 const cityCountryString = cityCountryArray.join(", ");52 console.log('Final location string:', cityCountryString);53 54 return cityCountryString || "Location TBD"; // Fallback if everything is empty55 };56 57 // Use the function result58 const location = getLocationString();59 60 useEffect(() => {61 const calculateTimeLeft = () => {62 if (!deadlineDate || !isValid(deadlineDate)) {63 setCountdown('TBD');64 return;65 }66 67 const now = new Date();68 const difference = deadlineDate.getTime() - now.getTime();69 70 if (difference <= 0) {71 setCountdown('Deadline passed');72 return;73 }74 75 const days = Math.floor(difference / (1000 * 60 * 60 * 24));76 const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));77 const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));78 const seconds = Math.floor((difference % (1000 * 60)) / 1000);79 80 setCountdown(`${days}d ${hours}h ${minutes}m ${seconds}s`);81 };82 83 calculateTimeLeft();84 const timer = setInterval(calculateTimeLeft, 1000);85 return () => clearInterval(timer);86 }, [deadlineDate]);87 88 const getCountdownColor = () => {89 if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600";90 const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));91 if (daysRemaining <= 7) return "text-red-600";92 if (daysRemaining <= 30) return "text-orange-600";93 return "text-green-600";94 };95 96 const createCalendarEvent = (type: 'google' | 'apple') => {97 try {98 if (!nextDeadline || !deadlineDate || !isValid(deadlineDate)) {99 throw new Error('No valid upcoming deadline found');100 }101 102 // Create an end date 1 hour after the deadline103 const endDate = new Date(deadlineDate.getTime() + (60 * 60 * 1000));104 105 const formatCalendarDate = (date: Date) => format(date, "yyyyMMdd'T'HHmmss");106 const escapeICSText = (value: string) =>107 value108 .replace(/\\/g, '\\\\')109 .replace(/\n/g, '\\n')110 .replace(/,/g, '\\,')111 .replace(/;/g, '\\;');112 113 const eventTitle = `${conference.title} ${nextDeadline.label}`;114 const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;115 const descriptionText =116 `${nextDeadline.label} for ${conference.full_name || conference.title}\n` +117 `Deadline: ${format(deadlineDate, "MMMM d, yyyy 'at' HH:mm:ss")} (${localTimezone})\n` +118 `Dates: ${conference.date}\n` +119 `Location: ${location}\n` +120 (conference.link ? `Website: ${conference.link}` : '');121 122 if (type === 'google') {123 const params = new URLSearchParams({124 action: 'TEMPLATE',125 text: eventTitle,126 dates: `${formatCalendarDate(deadlineDate)}/${formatCalendarDate(endDate)}`,127 details: descriptionText,128 location,129 ctz: localTimezone,130 sprop: `website:${conference.link || ''}`,131 });132 const url = `https://calendar.google.com/calendar/render?${params.toString()}`;133 window.open(url, '_blank');134 } else {135 const calendarContent = `BEGIN:VCALENDAR136VERSION:2.0137PRODID:-//AI Conference Deadlines//EN138BEGIN:VEVENT139URL:${escapeICSText(conference.link || '')}140DTSTART:${formatCalendarDate(deadlineDate)}141DTEND:${formatCalendarDate(endDate)}142SUMMARY:${escapeICSText(eventTitle)}143DESCRIPTION:${escapeICSText(descriptionText)}144LOCATION:${escapeICSText(location)}145END:VEVENT146END:VCALENDAR`;147 148 const link = document.createElement('a');149 link.href = `data:text/calendar;charset=utf8,${encodeURIComponent(calendarContent)}`;150 link.download = `${conference.title.toLowerCase().replace(/\s+/g, '-')}-deadline.ics`;151 document.body.appendChild(link);152 link.click();153 document.body.removeChild(link);154 }155 } catch (error) {156 console.error("Error creating calendar event:", error);157 alert("Sorry, there was an error creating the calendar event. Please try again.");158 }159 };160 161 const generateGoogleMapsUrl = (venue: string | undefined, place: string): string => {162 return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(venue || place)}`;163 };164 165 const formatDeadlineDisplay = () => {166 if (!deadlineDate || !isValid(deadlineDate)) return null;167 168 const localTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;169 return (170 <div className="text-sm text-neutral-500">171 <div>{format(deadlineDate, "MMMM d, yyyy 'at' HH:mm:ss")} ({localTZ})</div>172 {conference.timezone && conference.timezone !== localTZ && (173 <div className="text-xs">174 Conference timezone: {conference.timezone}175 </div>176 )}177 </div>178 );179 };180 181 // Add these new functions to handle consistent date conversion182 const getLocalDeadline = (dateString: string | undefined) => {183 if (!dateString || dateString === 'TBD') return null;184 return getDeadlineInLocalTime(dateString, conference.timezone);185 };186 187 // Format any deadline date consistently188 const formatDeadlineDate = (dateString: string | undefined) => {189 if (!dateString || dateString === 'TBD') return dateString || 'TBD';190 191 const localDate = getLocalDeadline(dateString);192 if (!localDate || !isValid(localDate)) return dateString;193 194 const localTZ = Intl.DateTimeFormat().resolvedOptions().timeZone;195 return `${format(localDate, "MMMM d, yyyy")} (${localTZ})`;196 };197 198 return (199 <Dialog open={open} onOpenChange={onOpenChange}>200 <DialogContent 201 className="max-w-lg w-full"202 >203 <DialogHeader>204 <DialogTitle className="text-2xl font-bold text-blue-600">205 {conference.title} {conference.year}206 </DialogTitle>207 <DialogDescription className="text-base text-gray-700">208 {conference.full_name}209 </DialogDescription>210 </DialogHeader>211 212 <div className="space-y-6 mt-4">213 <div className="space-y-4">214 <div className="flex items-start gap-2">215 <CalendarDays className="h-5 w-5 mt-0.5 text-gray-500" />216 <div>217 <p className="font-medium">Dates</p>218 <p className="text-sm text-gray-500">{conference.date}</p>219 </div>220 </div>221 222 <div className="flex items-start gap-2">223 <Globe className="h-5 w-5 mt-0.5 text-gray-500" />224 <div>225 <p className="font-medium">Venue</p>226 <p className="text-sm text-gray-500">227 {conference.venue || [conference.city, conference.country].filter(Boolean).join(", ")}228 </p>229 </div>230 </div>231 232 <div className="flex items-start gap-2">233 <Clock className="h-5 w-5 mt-0.5 text-gray-500" />234 <div className="space-y-2 flex-1">235 <p className="font-medium">Important Deadlines</p>236 <div className="text-sm text-gray-500 space-y-2">237 {upcomingDeadlines.length > 0 ? (238 upcomingDeadlines.map((deadline, index) => {239 const isNext = nextDeadline && deadline.date === nextDeadline.date && deadline.type === nextDeadline.type;240 const daysRemaining = getDaysRemaining(deadline, conference.timezone);241 const daysColorClass = getCountdownColorClass(daysRemaining);242 return (243 <div 244 key={`${deadline.type}-${index}`} 245 className={`rounded-md p-2 ${isNext ? 'bg-blue-100 border border-blue-200' : 'bg-gray-100'}`}246 >247 <div className="flex items-center justify-between gap-2">248 <p className={`flex-1 ${isNext ? 'font-medium text-blue-800' : ''}`}>249 {deadline.label}: {formatDeadlineDate(deadline.date)}250 {isNext && <span className="ml-2 text-xs">(Next)</span>}251 </p>252 {daysRemaining !== null && daysRemaining > 0 && (253 <span className={`text-xs font-medium whitespace-nowrap ${daysColorClass}`}>254 {daysRemaining} {daysRemaining === 1 ? 'day' : 'days'}255 </span>256 )}257 </div>258 </div>259 );260 })261 ) : (262 <div className="bg-gray-100 rounded-md p-2">263 <p>No upcoming deadlines</p>264 </div>265 )}266 </div>267 </div>268 </div>269 </div>270 271 <div className="flex items-center">272 <AlarmClock className={`h-5 w-5 mr-3 flex-shrink-0 ${getCountdownColor()}`} />273 <div>274 <span className={`font-medium ${getCountdownColor()}`}>275 {countdown}276 </span>277 {formatDeadlineDisplay()}278 </div>279 </div>280 281 {Array.isArray(conference.tags) && conference.tags.length > 0 && (282 <div className="flex flex-wrap gap-2">283 {conference.tags.map((tag) => (284 <span key={tag} className="tag">285 <Tag className="h-3 w-3 mr-1" />286 {tag}287 </span>288 ))}289 </div>290 )}291 292 {conference.note && (293 <div 294 className="text-sm text-neutral-600 mt-2 p-3 bg-neutral-50 rounded-lg"295 dangerouslySetInnerHTML={{ 296 __html: conference.note.replace(297 /<a(.*?)>/g, 298 '<a$1 style="color: #3b82f6; font-weight: 500; text-decoration: underline; text-underline-offset: 2px;">'299 ) 300 }}301 />302 )}303 304 <div className="flex items-center justify-between pt-2">305 {conference.link && (306 <Button 307 variant="ghost" 308 size="sm" 309 className="text-base text-primary hover:underline p-0"310 asChild311 >312 <a313 href={conference.link}314 target="_blank"315 rel="noopener noreferrer"316 >317 Visit website318 </a>319 </Button>320 )}321 322 <DropdownMenu>323 <DropdownMenuTrigger asChild>324 <Button 325 variant="ghost" 326 size="sm" 327 className="text-sm focus-visible:ring-0 focus:outline-none"328 >329 <CalendarPlus className="h-4 w-4 mr-2" />330 Add to Calendar331 </Button>332 </DropdownMenuTrigger>333 <DropdownMenuContent className="bg-white" align="end">334 <DropdownMenuItem 335 className="text-neutral-800 hover:bg-neutral-100"336 onClick={() => createCalendarEvent('google')}337 >338 Add to Google Calendar339 </DropdownMenuItem>340 <DropdownMenuItem 341 className="text-neutral-800 hover:bg-neutral-100"342 onClick={() => createCalendarEvent('apple')}343 >344 Add to Apple Calendar345 </DropdownMenuItem>346 </DropdownMenuContent>347 </DropdownMenu>348 </div>349 </div>350 </DialogContent>351 </Dialog>352 );353};354 355export default ConferenceDialog;356 