ai-deadlines-copy / src /components /ConferenceDialog.tsx
nielsr's picture
nielsr HF staff
Update conference data
ade630a
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;