gpt-engineer-app[bot] commited on
Commit
a2085ce
·
1 Parent(s): f1ac2c9

Add conference details modal

Browse files

Adds a modal that displays detailed conference information when a user clicks on a conference tile.

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
- <div className="conference-card">
32
- <div className="mb-3">
33
- {link ? (
34
- <a
35
- href={link}
36
- target="_blank"
37
- rel="noopener noreferrer"
38
- className="hover:underline"
39
- >
40
- <h3 className="text-lg font-semibold text-primary">{title}</h3>
41
- </a>
42
- ) : (
43
- <h3 className="text-lg font-semibold">{title}</h3>
44
- )}
45
- {full_name && <p className="text-xs text-neutral-600 truncate">{full_name}</p>}
46
- </div>
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
- <div className="flex items-center text-neutral">
54
- <Globe className="h-4 w-4 mr-2 flex-shrink-0" />
55
- <span className="text-sm truncate">{place}</span>
56
- </div>
57
- <div className="flex items-center text-neutral">
58
- <Clock className="h-4 w-4 mr-2 flex-shrink-0" />
59
- <span className="text-sm truncate">
60
- {deadline === 'TBD' ? 'TBD' : deadline}
61
- </span>
62
- </div>
63
- <div className="flex items-center">
64
- <AlarmClock className={`h-4 w-4 mr-2 flex-shrink-0 ${getCountdownColor()}`} />
65
- <span className={`text-sm font-medium truncate ${getCountdownColor()}`}>
66
- {daysLeft}
67
- </span>
68
- </div>
69
- </div>
70
-
71
- {Array.isArray(tags) && tags.length > 0 && (
72
- <div className="flex flex-wrap gap-1 mt-auto">
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
- {note && (
83
- <div
84
- className="text-xs text-neutral-600 mt-2"
85
- dangerouslySetInnerHTML={{ __html: note }}
86
- />
87
- )}
88
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;