nielsr HF staff commited on
Commit
2906af3
·
1 Parent(s): 6fe328e

Improve calendar tab

Browse files
src/components/ConferenceCalendar.tsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { Calendar } from "@/components/ui/calendar";
3
+ import { Conference } from "@/types/conference";
4
+ import { parseISO, format, parse, startOfMonth } from "date-fns";
5
+
6
+ interface ConferenceCalendarProps {
7
+ conferences: Conference[];
8
+ }
9
+
10
+ const ConferenceCalendar = ({ conferences }: ConferenceCalendarProps) => {
11
+ const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
12
+ const [currentMonth, setCurrentMonth] = useState<Date>(new Date());
13
+
14
+ // Handle month change
15
+ const handleMonthChange = (month: Date) => {
16
+ setCurrentMonth(month);
17
+ setSelectedDate(undefined); // Clear selected date when changing months
18
+ };
19
+
20
+ // Convert conference dates to calendar events
21
+ const conferenceEvents = conferences.map(conf => {
22
+ let startDate: Date | null = null;
23
+
24
+ try {
25
+ // First try to use the start field if it exists
26
+ if (conf.start) {
27
+ startDate = parseISO(conf.start);
28
+ }
29
+ // If no start field or it failed, try to parse the date field
30
+ else if (conf.date) {
31
+ // Handle various date formats
32
+ const dateStr = conf.date.split('–')[0].split('-')[0].trim(); // Get first date in range
33
+
34
+ // Try different date formats
35
+ try {
36
+ // Try "MMM d, yyyy" format (e.g., "Feb 28, 2025")
37
+ startDate = parse(dateStr, 'MMM d, yyyy', new Date());
38
+ } catch {
39
+ try {
40
+ // Try "MMMM d, yyyy" format (e.g., "February 28, 2025")
41
+ startDate = parse(dateStr, 'MMMM d, yyyy', new Date());
42
+ } catch {
43
+ // If all else fails, try ISO format
44
+ startDate = parseISO(dateStr);
45
+ }
46
+ }
47
+ }
48
+
49
+ // Only return event if we successfully parsed a date
50
+ if (startDate && isValidDate(startDate)) {
51
+ return {
52
+ date: startDate,
53
+ title: conf.title,
54
+ conference: conf
55
+ };
56
+ }
57
+ return null;
58
+ } catch (error) {
59
+ console.warn(`Failed to parse date for conference ${conf.title}:`, error);
60
+ return null;
61
+ }
62
+ }).filter(event => event !== null); // Remove any null events
63
+
64
+ // Helper function to check if date is valid
65
+ function isValidDate(date: Date) {
66
+ return date instanceof Date && !isNaN(date.getTime());
67
+ }
68
+
69
+ // Get events for the selected date
70
+ const getEventsForDate = (date: Date) => {
71
+ if (!date || !isValidDate(date)) return [];
72
+ return conferenceEvents.filter(event =>
73
+ event && event.date &&
74
+ format(event.date, 'yyyy-MM-dd') === format(date, 'yyyy-MM-dd')
75
+ );
76
+ };
77
+
78
+ // Get events for the current month
79
+ const getEventsForMonth = (date: Date) => {
80
+ return conferenceEvents.filter(event =>
81
+ event && event.date &&
82
+ format(event.date, 'yyyy-MM') === format(date, 'yyyy-MM')
83
+ );
84
+ };
85
+
86
+ // Create footer content
87
+ const footer = (
88
+ <div className="mt-3">
89
+ <h3 className="font-medium">
90
+ Events in {format(currentMonth, 'MMMM yyyy')}:
91
+ </h3>
92
+ {getEventsForMonth(currentMonth).length > 0 ? (
93
+ <ul className="mt-2 space-y-1">
94
+ {getEventsForMonth(currentMonth).map((event, index) => (
95
+ <li key={index} className="text-sm">
96
+ {event.title} ({format(event.date, 'MMM d')}) - {event.conference.place}
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ ) : (
101
+ <p className="text-sm text-muted-foreground">No events this month</p>
102
+ )}
103
+ {selectedDate && (
104
+ <div className="mt-4">
105
+ <h3 className="font-medium">
106
+ Events on {format(selectedDate, 'MMMM d, yyyy')}:
107
+ </h3>
108
+ {getEventsForDate(selectedDate).length > 0 ? (
109
+ <ul className="mt-2 space-y-1">
110
+ {getEventsForDate(selectedDate).map((event, index) => (
111
+ <li key={index} className="text-sm">
112
+ {event.title} - {event.conference.place}
113
+ </li>
114
+ ))}
115
+ </ul>
116
+ ) : (
117
+ <p className="text-sm text-muted-foreground">No events on this date</p>
118
+ )}
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+
124
+ return (
125
+ <div className="flex flex-col items-center space-y-4 p-4">
126
+ <Calendar
127
+ mode="single"
128
+ selected={selectedDate}
129
+ onSelect={setSelectedDate}
130
+ footer={footer}
131
+ month={currentMonth}
132
+ onMonthChange={handleMonthChange}
133
+ modifiers={{
134
+ event: (date) => getEventsForDate(date).length > 0
135
+ }}
136
+ modifiersStyles={{
137
+ event: { fontWeight: 'bold', textDecoration: 'underline' }
138
+ }}
139
+ />
140
+ </div>
141
+ );
142
+ };
143
+
144
+ export default ConferenceCalendar;
src/components/ui/calendar.tsx CHANGED
@@ -16,9 +16,9 @@ function Calendar({
16
  return (
17
  <DayPicker
18
  showOutsideDays={showOutsideDays}
19
- className={cn("p-3", className)}
20
  classNames={{
21
- months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
22
  month: "space-y-4",
23
  caption: "flex justify-center pt-1 relative items-center",
24
  caption_label: "text-sm font-medium",
 
16
  return (
17
  <DayPicker
18
  showOutsideDays={showOutsideDays}
19
+ className={cn("p-3 mx-auto", className)}
20
  classNames={{
21
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0 justify-center",
22
  month: "space-y-4",
23
  caption: "flex justify-center pt-1 relative items-center",
24
  caption_label: "text-sm font-medium",
src/pages/Calendar.tsx CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import { useState } from "react";
3
  import conferencesData from "@/data/conferences.yml";
4
  import { Conference } from "@/types/conference";
@@ -9,6 +8,11 @@ import { parseISO, format, isValid, isSameMonth } from "date-fns";
9
  const CalendarPage = () => {
10
  const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
11
 
 
 
 
 
 
12
  // Helper function to safely parse dates
13
  const safeParseISO = (dateString: string | undefined | number): Date | null => {
14
  if (!dateString || dateString === 'TBD') return null;
@@ -16,10 +20,20 @@ const CalendarPage = () => {
16
  const dateStr = typeof dateString === 'number' ? dateString.toString() : dateString;
17
 
18
  try {
19
- const normalizedDate = dateStr.replace(/(\d{4})-(\d{1})-(\d{1,2})/, '$1-0$2-$3')
20
- .replace(/(\d{4})-(\d{2})-(\d{1})/, '$1-$2-0$3');
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- const parsedDate = parseISO(normalizedDate);
23
  return isValid(parsedDate) ? parsedDate : null;
24
  } catch (error) {
25
  console.error("Error parsing date:", dateString);
@@ -94,92 +108,104 @@ const CalendarPage = () => {
94
  const datesWithEvents = getDatesWithEvents();
95
 
96
  return (
97
- <div className="min-h-screen bg-neutral-light p-6">
98
- <div className="max-w-7xl mx-auto">
99
- <h1 className="text-3xl font-bold mb-8 text-center">Calendar Overview</h1>
100
-
101
- <div className="flex justify-center gap-6 mb-6">
102
- <div className="flex items-center gap-2">
103
- <CircleDot className="h-4 w-4 text-purple-600" />
104
- <span>Conference Dates</span>
105
- </div>
106
- <div className="flex items-center gap-2">
107
- <CircleDot className="h-4 w-4 text-red-500" />
108
- <span>Submission Deadlines</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </div>
110
  </div>
111
 
112
- <div className="grid grid-cols-1 gap-8">
113
- <div className="mx-auto w-full max-w-3xl">
114
- <Calendar
115
- mode="single"
116
- selected={selectedDate}
117
- onSelect={setSelectedDate}
118
- className="bg-white rounded-lg p-6 shadow-sm mx-auto w-full"
119
- classNames={{
120
- months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
121
- month: "space-y-4 w-full",
122
- caption: "flex justify-center pt-1 relative items-center mb-4",
123
- caption_label: "text-lg font-semibold",
124
- table: "w-full border-collapse space-y-1",
125
- head_row: "flex",
126
- head_cell: "text-muted-foreground rounded-md w-14 font-normal text-[0.8rem]",
127
- row: "flex w-full mt-2",
128
- cell: "h-14 w-14 text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
129
- day: "h-14 w-14 p-0 font-normal aria-selected:opacity-100 hover:bg-neutral-100 rounded-lg transition-colors",
130
- day_today: "bg-neutral-100 text-primary font-semibold",
131
- nav: "space-x-1 flex items-center",
132
- nav_button: "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
133
- nav_button_previous: "absolute left-1",
134
- nav_button_next: "absolute right-1",
135
- }}
136
- modifiers={{
137
- conference: datesWithEvents.conferences,
138
- deadline: datesWithEvents.deadlines
139
- }}
140
- modifiersStyles={{
141
- conference: {
142
- backgroundColor: '#DDD6FE',
143
- color: '#7C3AED',
144
- fontWeight: 'bold'
145
- },
146
- deadline: {
147
- backgroundColor: '#FEE2E2',
148
- color: '#EF4444',
149
- fontWeight: 'bold'
150
- }
151
- }}
152
- />
153
- </div>
154
 
155
- {selectedDate && (
156
- <div className="mx-auto w-full max-w-3xl space-y-4">
157
- <h2 className="text-xl font-semibold flex items-center gap-2">
158
- <CalendarIcon className="h-5 w-5" />
159
- Events in {format(selectedDate, 'MMMM yyyy')}
160
- </h2>
161
- {monthEvents.length === 0 ? (
162
- <p className="text-neutral-600">No events this month.</p>
163
- ) : (
164
- <div className="space-y-4">
165
- {monthEvents.map((conf: Conference) => (
166
  <div key={conf.id} className="bg-white p-4 rounded-lg shadow-sm">
167
  <h3 className="font-semibold text-lg">{conf.title}</h3>
168
  <div className="space-y-1">
169
- {conf.deadline && safeParseISO(conf.deadline) && isSameMonth(safeParseISO(conf.deadline)!, selectedDate) && (
170
  <p className="text-red-500">
171
- Submission Deadline: {format(safeParseISO(conf.deadline)!, 'MMMM d, yyyy')}
172
  </p>
173
  )}
174
- {conf.start && (
175
  <p className="text-purple-600">
176
- Conference Date: {format(safeParseISO(conf.start)!, 'MMMM d')}
177
- {conf.end ? ` - ${format(safeParseISO(conf.end)!, 'MMMM d, yyyy')}` : `, ${format(safeParseISO(conf.start)!, 'yyyy')}`}
 
 
 
178
  </p>
179
  )}
180
  </div>
181
  <div className="mt-2 flex flex-wrap gap-2">
182
- {conf.tags.map((tag) => (
183
  <span key={tag} className="tag text-sm">
184
  <Tag className="h-3 w-3 mr-1" />
185
  {tag}
@@ -187,12 +213,12 @@ const CalendarPage = () => {
187
  ))}
188
  </div>
189
  </div>
190
- ))}
191
- </div>
192
- )}
193
- </div>
194
- )}
195
- </div>
196
  </div>
197
  </div>
198
  );
 
 
1
  import { useState } from "react";
2
  import conferencesData from "@/data/conferences.yml";
3
  import { Conference } from "@/types/conference";
 
8
  const CalendarPage = () => {
9
  const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
10
 
11
+ // Handle month change
12
+ const handleMonthChange = (date: Date) => {
13
+ setSelectedDate(date);
14
+ };
15
+
16
  // Helper function to safely parse dates
17
  const safeParseISO = (dateString: string | undefined | number): Date | null => {
18
  if (!dateString || dateString === 'TBD') return null;
 
20
  const dateStr = typeof dateString === 'number' ? dateString.toString() : dateString;
21
 
22
  try {
23
+ // First try to parse the date directly
24
+ let parsedDate = parseISO(dateStr);
25
+
26
+ // If that fails, try to parse various date formats
27
+ if (!isValid(parsedDate)) {
28
+ // Try to parse formats like "July 11-19, 2025" or "Sept 9-12, 2025"
29
+ const match = dateStr.match(/([A-Za-z]+)\s+(\d+)(?:-\d+)?,\s*(\d{4})/);
30
+ if (match) {
31
+ const [_, month, day, year] = match;
32
+ const normalizedDate = `${year}-${month}-${day.padStart(2, '0')}`;
33
+ parsedDate = parseISO(normalizedDate);
34
+ }
35
+ }
36
 
 
37
  return isValid(parsedDate) ? parsedDate : null;
38
  } catch (error) {
39
  console.error("Error parsing date:", dateString);
 
108
  const datesWithEvents = getDatesWithEvents();
109
 
110
  return (
111
+ <div className="container mx-auto px-4 py-8">
112
+ <div className="flex flex-col lg:flex-row gap-8">
113
+ <div className="w-full lg:w-auto">
114
+ <Calendar
115
+ mode="single"
116
+ selected={selectedDate}
117
+ onSelect={setSelectedDate}
118
+ onMonthChange={handleMonthChange}
119
+ modifiers={{
120
+ hasConference: (date) => datesWithEvents.conferences.some(d =>
121
+ format(d, 'yyyy-MM-dd') === format(date, 'yyyy-MM-dd')
122
+ ),
123
+ hasDeadline: (date) => datesWithEvents.deadlines.some(d =>
124
+ format(d, 'yyyy-MM-dd') === format(date, 'yyyy-MM-dd')
125
+ )
126
+ }}
127
+ className="bg-white rounded-lg p-6 shadow-sm mx-auto w-full"
128
+ classNames={{
129
+ months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
130
+ month: "space-y-4 w-full",
131
+ caption: "flex justify-center pt-1 relative items-center mb-4",
132
+ caption_label: "text-lg font-semibold",
133
+ table: "w-full border-collapse space-y-1",
134
+ head_row: "flex",
135
+ head_cell: "text-muted-foreground rounded-md w-14 font-normal text-[0.8rem]",
136
+ row: "flex w-full mt-2",
137
+ cell: "h-14 w-14 text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
138
+ day: "h-14 w-14 p-0 font-normal aria-selected:opacity-100 hover:bg-neutral-100 rounded-lg transition-colors",
139
+ day_today: "bg-neutral-100 text-primary font-semibold",
140
+ nav: "space-x-1 flex items-center",
141
+ nav_button: "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
142
+ nav_button_previous: "absolute left-1",
143
+ nav_button_next: "absolute right-1",
144
+ }}
145
+ modifiersStyles={{
146
+ conference: {
147
+ backgroundColor: '#DDD6FE',
148
+ color: '#7C3AED',
149
+ fontWeight: 'bold'
150
+ },
151
+ deadline: {
152
+ backgroundColor: '#FEE2E2',
153
+ color: '#EF4444',
154
+ fontWeight: 'bold'
155
+ }
156
+ }}
157
+ />
158
+
159
+ <div className="mt-4 space-y-2">
160
+ <div className="flex justify-center gap-6 mb-6">
161
+ <div className="flex items-center gap-2">
162
+ <CircleDot className="h-4 w-4 text-purple-600" />
163
+ <span>Conference Dates</span>
164
+ </div>
165
+ <div className="flex items-center gap-2">
166
+ <CircleDot className="h-4 w-4 text-red-500" />
167
+ <span>Submission Deadlines</span>
168
+ </div>
169
+ </div>
170
  </div>
171
  </div>
172
 
173
+ {selectedDate && (
174
+ <div className="mx-auto w-full max-w-3xl space-y-4">
175
+ <h2 className="text-xl font-semibold flex items-center gap-2">
176
+ <CalendarIcon className="h-5 w-5" />
177
+ Events in {format(selectedDate, 'MMMM yyyy')}
178
+ </h2>
179
+ {monthEvents.length === 0 ? (
180
+ <p className="text-neutral-600">No events this month.</p>
181
+ ) : (
182
+ <div className="space-y-4">
183
+ {monthEvents.map((conf: Conference) => {
184
+ const startDate = safeParseISO(conf.start);
185
+ const endDate = safeParseISO(conf.end);
186
+ const deadlineDate = safeParseISO(conf.deadline);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ return (
 
 
 
 
 
 
 
 
 
 
189
  <div key={conf.id} className="bg-white p-4 rounded-lg shadow-sm">
190
  <h3 className="font-semibold text-lg">{conf.title}</h3>
191
  <div className="space-y-1">
192
+ {deadlineDate && isSameMonth(deadlineDate, selectedDate) && (
193
  <p className="text-red-500">
194
+ Submission Deadline: {format(deadlineDate, 'MMMM d, yyyy')}
195
  </p>
196
  )}
197
+ {startDate && (
198
  <p className="text-purple-600">
199
+ Conference Date: {format(startDate, 'MMMM d')}
200
+ {endDate ?
201
+ ` - ${format(endDate, 'MMMM d, yyyy')}` :
202
+ `, ${format(startDate, 'yyyy')}`
203
+ }
204
  </p>
205
  )}
206
  </div>
207
  <div className="mt-2 flex flex-wrap gap-2">
208
+ {Array.isArray(conf.tags) && conf.tags.map((tag) => (
209
  <span key={tag} className="tag text-sm">
210
  <Tag className="h-3 w-3 mr-1" />
211
  {tag}
 
213
  ))}
214
  </div>
215
  </div>
216
+ );
217
+ })}
218
+ </div>
219
+ )}
220
+ </div>
221
+ )}
222
  </div>
223
  </div>
224
  );
src/pages/Index.tsx CHANGED
@@ -6,6 +6,7 @@ import { Conference } from "@/types/conference";
6
  import { useState, useMemo, useEffect } from "react";
7
  import { Switch } from "@/components/ui/switch"
8
  import { parseISO, isValid, isPast } from "date-fns";
 
9
 
10
  const Index = () => {
11
  const [selectedTag, setSelectedTag] = useState("All");
@@ -78,6 +79,7 @@ const Index = () => {
78
  <ConferenceCard key={conference.id} {...conference} />
79
  ))}
80
  </div>
 
81
  </main>
82
  </div>
83
  );
 
6
  import { useState, useMemo, useEffect } from "react";
7
  import { Switch } from "@/components/ui/switch"
8
  import { parseISO, isValid, isPast } from "date-fns";
9
+ import ConferenceCalendar from "@/components/ConferenceCalendar";
10
 
11
  const Index = () => {
12
  const [selectedTag, setSelectedTag] = useState("All");
 
79
  <ConferenceCard key={conference.id} {...conference} />
80
  ))}
81
  </div>
82
+ <ConferenceCalendar conferences={filteredConferences} />
83
  </main>
84
  </div>
85
  );