Spaces:
Sleeping
Sleeping
gpt-engineer-app[bot]
commited on
Commit
·
a2085ce
1
Parent(s):
f1ac2c9
Add conference details modal
Browse filesAdds a modal that displays detailed conference information when a user clicks on a conference tile.
- src/components/ConferenceCard.tsx +74 -54
- src/components/ConferenceDialog.tsx +111 -0
src/components/ConferenceCard.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
import { CalendarDays, Globe, Tag, Clock, AlarmClock } from "lucide-react";
|
| 3 |
import { Conference } from "@/types/conference";
|
| 4 |
import { formatDistanceToNow, parseISO, isValid } from "date-fns";
|
|
|
|
|
|
|
| 5 |
|
| 6 |
const ConferenceCard = ({
|
| 7 |
title,
|
|
@@ -14,7 +16,9 @@ const ConferenceCard = ({
|
|
| 14 |
link,
|
| 15 |
note,
|
| 16 |
abstract_deadline,
|
|
|
|
| 17 |
}: Conference) => {
|
|
|
|
| 18 |
const deadlineDate = deadline && deadline !== 'TBD' ? parseISO(deadline) : null;
|
| 19 |
const daysLeft = deadlineDate && isValid(deadlineDate) ? formatDistanceToNow(deadlineDate, { addSuffix: true }) : 'TBD';
|
| 20 |
|
|
@@ -27,65 +31,81 @@ const ConferenceCard = ({
|
|
| 27 |
return "text-green-600";
|
| 28 |
};
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
return (
|
| 31 |
-
|
| 32 |
-
<div className="
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
<div className="flex flex-col gap-2 mb-3">
|
| 49 |
-
<div className="flex items-center text-neutral">
|
| 50 |
-
<CalendarDays className="h-4 w-4 mr-2 flex-shrink-0" />
|
| 51 |
-
<span className="text-sm truncate">{date}</span>
|
| 52 |
</div>
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
<
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
<
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
{tags.map((tag) => (
|
| 74 |
-
<span key={tag} className="tag text-xs py-0.5">
|
| 75 |
-
<Tag className="h-3 w-3 mr-1" />
|
| 76 |
-
{tag}
|
| 77 |
</span>
|
| 78 |
-
|
| 79 |
</div>
|
| 80 |
-
)}
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
);
|
| 90 |
};
|
| 91 |
|
|
|
|
| 2 |
import { CalendarDays, Globe, Tag, Clock, AlarmClock } from "lucide-react";
|
| 3 |
import { Conference } from "@/types/conference";
|
| 4 |
import { formatDistanceToNow, parseISO, isValid } from "date-fns";
|
| 5 |
+
import ConferenceDialog from "./ConferenceDialog";
|
| 6 |
+
import { useState } from "react";
|
| 7 |
|
| 8 |
const ConferenceCard = ({
|
| 9 |
title,
|
|
|
|
| 16 |
link,
|
| 17 |
note,
|
| 18 |
abstract_deadline,
|
| 19 |
+
...conferenceProps
|
| 20 |
}: Conference) => {
|
| 21 |
+
const [dialogOpen, setDialogOpen] = useState(false);
|
| 22 |
const deadlineDate = deadline && deadline !== 'TBD' ? parseISO(deadline) : null;
|
| 23 |
const daysLeft = deadlineDate && isValid(deadlineDate) ? formatDistanceToNow(deadlineDate, { addSuffix: true }) : 'TBD';
|
| 24 |
|
|
|
|
| 31 |
return "text-green-600";
|
| 32 |
};
|
| 33 |
|
| 34 |
+
const handleCardClick = (e: React.MouseEvent) => {
|
| 35 |
+
// Only open dialog if the click wasn't on a link or interactive element
|
| 36 |
+
if (!(e.target as HTMLElement).closest('a')) {
|
| 37 |
+
setDialogOpen(true);
|
| 38 |
+
}
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
return (
|
| 42 |
+
<>
|
| 43 |
+
<div className="conference-card cursor-pointer" onClick={handleCardClick}>
|
| 44 |
+
<div className="mb-3">
|
| 45 |
+
{link ? (
|
| 46 |
+
<a
|
| 47 |
+
href={link}
|
| 48 |
+
target="_blank"
|
| 49 |
+
rel="noopener noreferrer"
|
| 50 |
+
className="hover:underline"
|
| 51 |
+
onClick={(e) => e.stopPropagation()}
|
| 52 |
+
>
|
| 53 |
+
<h3 className="text-lg font-semibold text-primary">{title}</h3>
|
| 54 |
+
</a>
|
| 55 |
+
) : (
|
| 56 |
+
<h3 className="text-lg font-semibold">{title}</h3>
|
| 57 |
+
)}
|
| 58 |
+
{full_name && <p className="text-xs text-neutral-600 truncate">{full_name}</p>}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
</div>
|
| 60 |
+
|
| 61 |
+
<div className="flex flex-col gap-2 mb-3">
|
| 62 |
+
<div className="flex items-center text-neutral">
|
| 63 |
+
<CalendarDays className="h-4 w-4 mr-2 flex-shrink-0" />
|
| 64 |
+
<span className="text-sm truncate">{date}</span>
|
| 65 |
+
</div>
|
| 66 |
+
<div className="flex items-center text-neutral">
|
| 67 |
+
<Globe className="h-4 w-4 mr-2 flex-shrink-0" />
|
| 68 |
+
<span className="text-sm truncate">{place}</span>
|
| 69 |
+
</div>
|
| 70 |
+
<div className="flex items-center text-neutral">
|
| 71 |
+
<Clock className="h-4 w-4 mr-2 flex-shrink-0" />
|
| 72 |
+
<span className="text-sm truncate">
|
| 73 |
+
{deadline === 'TBD' ? 'TBD' : deadline}
|
| 74 |
+
</span>
|
| 75 |
+
</div>
|
| 76 |
+
<div className="flex items-center">
|
| 77 |
+
<AlarmClock className={`h-4 w-4 mr-2 flex-shrink-0 ${getCountdownColor()}`} />
|
| 78 |
+
<span className={`text-sm font-medium truncate ${getCountdownColor()}`}>
|
| 79 |
+
{daysLeft}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
</span>
|
| 81 |
+
</div>
|
| 82 |
</div>
|
|
|
|
| 83 |
|
| 84 |
+
{Array.isArray(tags) && tags.length > 0 && (
|
| 85 |
+
<div className="flex flex-wrap gap-1 mt-auto">
|
| 86 |
+
{tags.map((tag) => (
|
| 87 |
+
<span key={tag} className="tag text-xs py-0.5">
|
| 88 |
+
<Tag className="h-3 w-3 mr-1" />
|
| 89 |
+
{tag}
|
| 90 |
+
</span>
|
| 91 |
+
))}
|
| 92 |
+
</div>
|
| 93 |
+
)}
|
| 94 |
+
|
| 95 |
+
{note && (
|
| 96 |
+
<div
|
| 97 |
+
className="text-xs text-neutral-600 mt-2"
|
| 98 |
+
dangerouslySetInnerHTML={{ __html: note }}
|
| 99 |
+
/>
|
| 100 |
+
)}
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<ConferenceDialog
|
| 104 |
+
conference={{ title, full_name, date, place, deadline, timezone, tags, link, note, abstract_deadline, ...conferenceProps }}
|
| 105 |
+
open={dialogOpen}
|
| 106 |
+
onOpenChange={setDialogOpen}
|
| 107 |
+
/>
|
| 108 |
+
</>
|
| 109 |
);
|
| 110 |
};
|
| 111 |
|
src/components/ConferenceDialog.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import {
|
| 3 |
+
Dialog,
|
| 4 |
+
DialogContent,
|
| 5 |
+
DialogHeader,
|
| 6 |
+
DialogTitle,
|
| 7 |
+
} from "@/components/ui/dialog";
|
| 8 |
+
import { CalendarDays, Globe, Tag, Clock, AlarmClock } from "lucide-react";
|
| 9 |
+
import { Conference } from "@/types/conference";
|
| 10 |
+
import { formatDistanceToNow, parseISO, isValid } from "date-fns";
|
| 11 |
+
|
| 12 |
+
interface ConferenceDialogProps {
|
| 13 |
+
conference: Conference;
|
| 14 |
+
open: boolean;
|
| 15 |
+
onOpenChange: (open: boolean) => void;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const ConferenceDialog = ({ conference, open, onOpenChange }: ConferenceDialogProps) => {
|
| 19 |
+
const deadlineDate = conference.deadline && conference.deadline !== 'TBD' ? parseISO(conference.deadline) : null;
|
| 20 |
+
const daysLeft = deadlineDate && isValid(deadlineDate) ? formatDistanceToNow(deadlineDate, { addSuffix: true }) : 'TBD';
|
| 21 |
+
|
| 22 |
+
const getCountdownColor = () => {
|
| 23 |
+
if (!deadlineDate || !isValid(deadlineDate)) return "text-neutral-600";
|
| 24 |
+
const daysRemaining = Math.ceil((deadlineDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24));
|
| 25 |
+
if (daysRemaining <= 7) return "text-red-600";
|
| 26 |
+
if (daysRemaining <= 30) return "text-orange-600";
|
| 27 |
+
return "text-green-600";
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
| 32 |
+
<DialogContent className="max-w-md">
|
| 33 |
+
<DialogHeader>
|
| 34 |
+
<DialogTitle className="text-xl font-bold">
|
| 35 |
+
{conference.title}
|
| 36 |
+
</DialogTitle>
|
| 37 |
+
{conference.full_name && (
|
| 38 |
+
<p className="text-sm text-neutral-600">{conference.full_name}</p>
|
| 39 |
+
)}
|
| 40 |
+
</DialogHeader>
|
| 41 |
+
|
| 42 |
+
<div className="space-y-4 py-4">
|
| 43 |
+
<div className="flex flex-col gap-3">
|
| 44 |
+
<div className="flex items-center text-neutral">
|
| 45 |
+
<CalendarDays className="h-5 w-5 mr-3 flex-shrink-0" />
|
| 46 |
+
<span>{conference.date}</span>
|
| 47 |
+
</div>
|
| 48 |
+
<div className="flex items-center text-neutral">
|
| 49 |
+
<Globe className="h-5 w-5 mr-3 flex-shrink-0" />
|
| 50 |
+
<span>{conference.place}</span>
|
| 51 |
+
</div>
|
| 52 |
+
<div className="flex items-center text-neutral">
|
| 53 |
+
<Clock className="h-5 w-5 mr-3 flex-shrink-0" />
|
| 54 |
+
<div className="flex flex-col">
|
| 55 |
+
<span>Deadline: {conference.deadline === 'TBD' ? 'TBD' : conference.deadline}</span>
|
| 56 |
+
{conference.timezone && (
|
| 57 |
+
<span className="text-sm text-neutral-500">Timezone: {conference.timezone}</span>
|
| 58 |
+
)}
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
<div className="flex items-center">
|
| 62 |
+
<AlarmClock className={`h-5 w-5 mr-3 flex-shrink-0 ${getCountdownColor()}`} />
|
| 63 |
+
<span className={`font-medium ${getCountdownColor()}`}>
|
| 64 |
+
{daysLeft}
|
| 65 |
+
</span>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
{conference.abstract_deadline && (
|
| 70 |
+
<div className="text-sm text-neutral-600">
|
| 71 |
+
Abstract Deadline: {conference.abstract_deadline}
|
| 72 |
+
</div>
|
| 73 |
+
)}
|
| 74 |
+
|
| 75 |
+
{Array.isArray(conference.tags) && conference.tags.length > 0 && (
|
| 76 |
+
<div className="flex flex-wrap gap-2">
|
| 77 |
+
{conference.tags.map((tag) => (
|
| 78 |
+
<span key={tag} className="tag">
|
| 79 |
+
<Tag className="h-3 w-3 mr-1" />
|
| 80 |
+
{tag}
|
| 81 |
+
</span>
|
| 82 |
+
))}
|
| 83 |
+
</div>
|
| 84 |
+
)}
|
| 85 |
+
|
| 86 |
+
{conference.note && (
|
| 87 |
+
<div
|
| 88 |
+
className="text-sm text-neutral-600 mt-2 p-3 bg-neutral-50 rounded-lg"
|
| 89 |
+
dangerouslySetInnerHTML={{ __html: conference.note }}
|
| 90 |
+
/>
|
| 91 |
+
)}
|
| 92 |
+
|
| 93 |
+
{conference.link && (
|
| 94 |
+
<div className="pt-2">
|
| 95 |
+
<a
|
| 96 |
+
href={conference.link}
|
| 97 |
+
target="_blank"
|
| 98 |
+
rel="noopener noreferrer"
|
| 99 |
+
className="text-primary hover:underline"
|
| 100 |
+
>
|
| 101 |
+
Visit Conference Website →
|
| 102 |
+
</a>
|
| 103 |
+
</div>
|
| 104 |
+
)}
|
| 105 |
+
</div>
|
| 106 |
+
</DialogContent>
|
| 107 |
+
</Dialog>
|
| 108 |
+
);
|
| 109 |
+
};
|
| 110 |
+
|
| 111 |
+
export default ConferenceDialog;
|