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 |
+
}
|