Spaces:
Running
Running
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;
|