|
import { Globe, Tag, Clock } from "lucide-react"; |
|
import { Conference } from "@/types/conference"; |
|
import { isValid, isPast } from "date-fns"; |
|
import ConferenceDialog from "./ConferenceDialog"; |
|
import CountdownTimer from "./CountdownTimer"; |
|
import { useState } from "react"; |
|
import { getDeadlineInLocalTime } from '@/utils/dateUtils'; |
|
|
|
const ConferenceCard = ({ |
|
title, |
|
full_name, |
|
year, |
|
date, |
|
deadline, |
|
timezone, |
|
tags = [], |
|
link, |
|
note, |
|
abstract_deadline, |
|
city, |
|
country, |
|
venue, |
|
...conferenceProps |
|
}: Conference) => { |
|
const [dialogOpen, setDialogOpen] = useState(false); |
|
const deadlineDate = getDeadlineInLocalTime(deadline, timezone); |
|
|
|
|
|
const location = [city, country].filter(Boolean).join(", "); |
|
|
|
const handleCardClick = (e: React.MouseEvent) => { |
|
if (!(e.target as HTMLElement).closest('a') && |
|
!(e.target as HTMLElement).closest('.tag-button')) { |
|
setDialogOpen(true); |
|
} |
|
}; |
|
|
|
const handleTagClick = (e: React.MouseEvent, tag: string) => { |
|
e.stopPropagation(); |
|
const searchParams = new URLSearchParams(window.location.search); |
|
const currentTags = searchParams.get('tags')?.split(',') || []; |
|
|
|
let newTags; |
|
if (currentTags.includes(tag)) { |
|
newTags = currentTags.filter(t => t !== tag); |
|
} else { |
|
newTags = [...currentTags, tag]; |
|
} |
|
|
|
if (newTags.length > 0) { |
|
searchParams.set('tags', newTags.join(',')); |
|
} else { |
|
searchParams.delete('tags'); |
|
} |
|
|
|
const newUrl = `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`; |
|
window.history.pushState({}, '', newUrl); |
|
window.dispatchEvent(new CustomEvent('urlchange', { detail: { tag } })); |
|
}; |
|
|
|
return ( |
|
<> |
|
<div |
|
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow p-4 flex flex-col cursor-pointer" |
|
onClick={handleCardClick} |
|
> |
|
<div className="flex justify-between items-start mb-2"> |
|
<div className="flex-1"> |
|
<h3 className="text-lg font-semibold text-primary"> |
|
{title} {year} |
|
{link && ( |
|
<a |
|
href={link} |
|
target="_blank" |
|
rel="noopener noreferrer" |
|
className="ml-2 hover:underline" |
|
onClick={(e) => e.stopPropagation()} |
|
> |
|
<Globe className="h-4 w-4 inline flex-shrink-0" /> |
|
</a> |
|
)} |
|
</h3> |
|
<CountdownTimer |
|
deadline={deadlineDate} |
|
className="mt-1" |
|
/> |
|
{full_name && ( |
|
<p className="text-sm text-gray-600 mt-1"> |
|
{full_name} |
|
</p> |
|
)} |
|
<div className="mt-2 text-sm text-gray-600"> |
|
{date}. {location} |
|
</div> |
|
{note && ( |
|
<div className="text-xs text-gray-500 mt-1"> |
|
Note: {note.replace(/<[^>]*>/g, '')} |
|
</div> |
|
)} |
|
<div className="flex items-center text-gray-600 mt-2"> |
|
<Clock className="h-4 w-4 mr-2 flex-shrink-0" /> |
|
<span className="text-sm"> |
|
Deadline: {deadline === 'TBD' ? 'TBD' : deadlineDate?.toLocaleString('en-US', { |
|
weekday: 'short', |
|
year: 'numeric', |
|
month: 'short', |
|
day: 'numeric', |
|
hour: '2-digit', |
|
minute: '2-digit', |
|
timeZoneName: 'short' |
|
})} |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div className="flex flex-col gap-2 mb-3"> |
|
|
|
</div> |
|
|
|
{Array.isArray(tags) && tags.length > 0 && ( |
|
<div className="flex flex-wrap gap-2"> |
|
{tags.map((tag) => ( |
|
<button |
|
key={tag} |
|
className="tag tag-button" |
|
onClick={(e) => handleTagClick(e, tag)} |
|
> |
|
<Tag className="h-3 w-3 mr-1" /> |
|
{tag} |
|
</button> |
|
))} |
|
</div> |
|
)} |
|
</div> |
|
|
|
<ConferenceDialog |
|
conference={{ |
|
title, |
|
full_name, |
|
year, |
|
date, |
|
deadline, |
|
timezone, |
|
tags, |
|
link, |
|
note, |
|
abstract_deadline, |
|
city, |
|
country, |
|
venue, |
|
...conferenceProps |
|
}} |
|
open={dialogOpen} |
|
onOpenChange={setDialogOpen} |
|
/> |
|
</> |
|
); |
|
}; |
|
|
|
export default ConferenceCard; |
|
|