CoolFace
Apppublic

huggingface/ai-deadlines

sourceHugging Faceupdated 3d agoView on Hugging Face
781likes
ConferenceDialog.tsx356 linesDownload Raw Back to components
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