Spaces:
Running
Running
Improve calendar filters
Browse files- src/pages/Calendar.tsx +77 -42
- src/styles/globals.css +19 -0
src/pages/Calendar.tsx
CHANGED
@@ -49,7 +49,10 @@ const CalendarPage = () => {
|
|
49 |
date: null,
|
50 |
events: { deadlines: [], conferences: [] }
|
51 |
});
|
52 |
-
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(
|
|
|
|
|
|
|
53 |
|
54 |
const safeParseISO = (dateString: string | undefined | number): Date | null => {
|
55 |
if (!dateString) return null;
|
@@ -85,7 +88,6 @@ const CalendarPage = () => {
|
|
85 |
conf.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
86 |
(conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase()));
|
87 |
|
88 |
-
// Add category filter
|
89 |
const matchesCategory = selectedCategories.size === 0 ||
|
90 |
(Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag)));
|
91 |
|
@@ -97,7 +99,7 @@ const CalendarPage = () => {
|
|
97 |
|
98 |
const dateMatches = isYearView ? isSameYear : isSameMonth;
|
99 |
|
100 |
-
const deadlineInPeriod = deadlineDate && dateMatches(deadlineDate, date);
|
101 |
|
102 |
let conferenceInPeriod = false;
|
103 |
if (startDate && endDate) {
|
@@ -118,12 +120,12 @@ const CalendarPage = () => {
|
|
118 |
};
|
119 |
|
120 |
const getDayEvents = (date: Date) => {
|
121 |
-
const deadlines = conferencesData.filter(conf => {
|
122 |
const deadlineDate = safeParseISO(conf.deadline);
|
123 |
const matchesCategory = selectedCategories.size === 0 ||
|
124 |
(Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag)));
|
125 |
return deadlineDate && isSameDay(deadlineDate, date) && matchesCategory;
|
126 |
-
});
|
127 |
|
128 |
const conferences = conferencesData.filter(conf => {
|
129 |
const startDate = safeParseISO(conf.start);
|
@@ -214,7 +216,7 @@ const CalendarPage = () => {
|
|
214 |
const conferenceStyles = getConferenceLineStyle(date);
|
215 |
|
216 |
// Get deadline style
|
217 |
-
const hasDeadline = dayEvents.deadlines.length > 0;
|
218 |
|
219 |
const handleDayClick = (e: React.MouseEvent) => {
|
220 |
e.preventDefault(); // Prevent default calendar behavior
|
@@ -361,45 +363,78 @@ const CalendarPage = () => {
|
|
361 |
);
|
362 |
|
363 |
const renderLegend = () => {
|
364 |
-
const categories = Object.entries(categoryColors);
|
365 |
-
|
366 |
return (
|
367 |
-
<div className="flex flex-wrap gap-
|
368 |
-
<
|
369 |
-
<
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
398 |
<button
|
399 |
-
onClick={() =>
|
400 |
-
|
|
|
|
|
|
|
|
|
|
|
401 |
>
|
402 |
-
|
403 |
</button>
|
404 |
)}
|
405 |
</div>
|
|
|
49 |
date: null,
|
50 |
events: { deadlines: [], conferences: [] }
|
51 |
});
|
52 |
+
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(
|
53 |
+
new Set(Object.keys(categoryColors))
|
54 |
+
);
|
55 |
+
const [showDeadlines, setShowDeadlines] = useState(true);
|
56 |
|
57 |
const safeParseISO = (dateString: string | undefined | number): Date | null => {
|
58 |
if (!dateString) return null;
|
|
|
88 |
conf.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
89 |
(conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase()));
|
90 |
|
|
|
91 |
const matchesCategory = selectedCategories.size === 0 ||
|
92 |
(Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag)));
|
93 |
|
|
|
99 |
|
100 |
const dateMatches = isYearView ? isSameYear : isSameMonth;
|
101 |
|
102 |
+
const deadlineInPeriod = showDeadlines && deadlineDate && dateMatches(deadlineDate, date);
|
103 |
|
104 |
let conferenceInPeriod = false;
|
105 |
if (startDate && endDate) {
|
|
|
120 |
};
|
121 |
|
122 |
const getDayEvents = (date: Date) => {
|
123 |
+
const deadlines = showDeadlines ? conferencesData.filter(conf => {
|
124 |
const deadlineDate = safeParseISO(conf.deadline);
|
125 |
const matchesCategory = selectedCategories.size === 0 ||
|
126 |
(Array.isArray(conf.tags) && conf.tags.some(tag => selectedCategories.has(tag)));
|
127 |
return deadlineDate && isSameDay(deadlineDate, date) && matchesCategory;
|
128 |
+
}) : [];
|
129 |
|
130 |
const conferences = conferencesData.filter(conf => {
|
131 |
const startDate = safeParseISO(conf.start);
|
|
|
216 |
const conferenceStyles = getConferenceLineStyle(date);
|
217 |
|
218 |
// Get deadline style
|
219 |
+
const hasDeadline = showDeadlines && dayEvents.deadlines.length > 0;
|
220 |
|
221 |
const handleDayClick = (e: React.MouseEvent) => {
|
222 |
e.preventDefault(); // Prevent default calendar behavior
|
|
|
363 |
);
|
364 |
|
365 |
const renderLegend = () => {
|
|
|
|
|
366 |
return (
|
367 |
+
<div className="flex flex-wrap gap-3 justify-center items-center mb-4">
|
368 |
+
<TooltipProvider>
|
369 |
+
<Tooltip>
|
370 |
+
<TooltipTrigger asChild>
|
371 |
+
<button
|
372 |
+
onClick={() => setShowDeadlines(!showDeadlines)}
|
373 |
+
className={`
|
374 |
+
flex items-center gap-2 px-3 py-1.5
|
375 |
+
rounded-lg border border-red-200
|
376 |
+
bg-white hover:bg-red-50
|
377 |
+
transition-all duration-200
|
378 |
+
cursor-pointer
|
379 |
+
${showDeadlines ? 'ring-2 ring-primary ring-offset-2' : ''}
|
380 |
+
`}
|
381 |
+
>
|
382 |
+
<div className="w-3 h-3 bg-red-500 rounded-full" />
|
383 |
+
<span className="text-sm">Submission Deadlines</span>
|
384 |
+
</button>
|
385 |
+
</TooltipTrigger>
|
386 |
+
<TooltipContent>
|
387 |
+
<p>Click to toggle submission deadlines</p>
|
388 |
+
</TooltipContent>
|
389 |
+
</Tooltip>
|
390 |
+
</TooltipProvider>
|
391 |
+
|
392 |
+
{Object.entries(categoryColors).map(([tag, color]) => (
|
393 |
+
<TooltipProvider key={tag}>
|
394 |
+
<Tooltip>
|
395 |
+
<TooltipTrigger asChild>
|
396 |
+
<button
|
397 |
+
onClick={() => {
|
398 |
+
const newCategories = new Set(selectedCategories);
|
399 |
+
if (newCategories.has(tag)) {
|
400 |
+
newCategories.delete(tag);
|
401 |
+
} else {
|
402 |
+
newCategories.add(tag);
|
403 |
+
}
|
404 |
+
setSelectedCategories(newCategories);
|
405 |
+
}}
|
406 |
+
className={`
|
407 |
+
flex items-center gap-2 px-3 py-1.5
|
408 |
+
rounded-lg border border-neutral-200
|
409 |
+
bg-white hover:bg-neutral-50
|
410 |
+
transition-all duration-200
|
411 |
+
cursor-pointer
|
412 |
+
${selectedCategories.has(tag) ? 'ring-2 ring-primary ring-offset-2' : ''}
|
413 |
+
`}
|
414 |
+
>
|
415 |
+
<div className={`w-3 h-3 rounded-full ${color}`} />
|
416 |
+
<span className="text-sm">{categoryNames[tag] || tag}</span>
|
417 |
+
</button>
|
418 |
+
</TooltipTrigger>
|
419 |
+
<TooltipContent>
|
420 |
+
<p>Click to toggle {categoryNames[tag] || tag}</p>
|
421 |
+
</TooltipContent>
|
422 |
+
</Tooltip>
|
423 |
+
</TooltipProvider>
|
424 |
+
))}
|
425 |
+
|
426 |
+
{/* Only show Reset when some filters are deselected */}
|
427 |
+
{(selectedCategories.size < Object.keys(categoryColors).length || !showDeadlines) && (
|
428 |
<button
|
429 |
+
onClick={() => {
|
430 |
+
setSelectedCategories(new Set(Object.keys(categoryColors)));
|
431 |
+
setShowDeadlines(true);
|
432 |
+
}}
|
433 |
+
className="text-sm text-neutral-500 hover:text-neutral-700
|
434 |
+
px-3 py-1.5 rounded-lg border border-neutral-200
|
435 |
+
hover:bg-neutral-50 transition-colors"
|
436 |
>
|
437 |
+
Reset filters
|
438 |
</button>
|
439 |
)}
|
440 |
</div>
|
src/styles/globals.css
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Add these styles to make the filter tags more interactive */
|
2 |
+
.filter-tag {
|
3 |
+
@apply transition-all duration-200;
|
4 |
+
}
|
5 |
+
|
6 |
+
.filter-tag:hover {
|
7 |
+
@apply transform scale-105;
|
8 |
+
}
|
9 |
+
|
10 |
+
/* Add a subtle pulse animation for the active filters */
|
11 |
+
@keyframes subtle-pulse {
|
12 |
+
0% { transform: scale(1); }
|
13 |
+
50% { transform: scale(1.02); }
|
14 |
+
100% { transform: scale(1); }
|
15 |
+
}
|
16 |
+
|
17 |
+
.filter-tag-active {
|
18 |
+
animation: subtle-pulse 2s infinite;
|
19 |
+
}
|