Spaces:
Running
Running
Add ability to select multiple categories
Browse files- src/components/FilterBar.tsx +17 -10
- src/pages/Index.tsx +7 -6
src/components/FilterBar.tsx
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
-
|
| 2 |
import { useMemo } from "react";
|
| 3 |
import conferencesData from "@/data/conferences.yml";
|
| 4 |
import { X } from "lucide-react";
|
| 5 |
|
| 6 |
interface FilterBarProps {
|
| 7 |
-
|
| 8 |
-
onTagSelect: (
|
| 9 |
}
|
| 10 |
|
| 11 |
-
const FilterBar = ({
|
| 12 |
const uniqueTags = useMemo(() => {
|
| 13 |
const tags = new Set<string>();
|
| 14 |
if (Array.isArray(conferencesData)) {
|
|
@@ -18,12 +17,12 @@ const FilterBar = ({ selectedTag, onTagSelect }: FilterBarProps) => {
|
|
| 18 |
}
|
| 19 |
});
|
| 20 |
}
|
| 21 |
-
return
|
| 22 |
id: tag,
|
| 23 |
label: tag.split("-").map(word =>
|
| 24 |
word.charAt(0).toUpperCase() + word.slice(1)
|
| 25 |
).join(" "),
|
| 26 |
-
description:
|
| 27 |
}));
|
| 28 |
}, []);
|
| 29 |
|
|
@@ -35,11 +34,19 @@ const FilterBar = ({ selectedTag, onTagSelect }: FilterBarProps) => {
|
|
| 35 |
<button
|
| 36 |
key={filter.id}
|
| 37 |
title={filter.description}
|
| 38 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
className={`
|
| 40 |
px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200
|
| 41 |
filter-tag
|
| 42 |
-
${
|
| 43 |
? "bg-primary text-white shadow-sm filter-tag-active"
|
| 44 |
: "bg-neutral-50 text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900"
|
| 45 |
}
|
|
@@ -49,9 +56,9 @@ const FilterBar = ({ selectedTag, onTagSelect }: FilterBarProps) => {
|
|
| 49 |
</button>
|
| 50 |
))}
|
| 51 |
|
| 52 |
-
{
|
| 53 |
<button
|
| 54 |
-
onClick={() => onTagSelect(
|
| 55 |
className="px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200
|
| 56 |
bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700
|
| 57 |
flex items-center gap-2"
|
|
|
|
|
|
|
| 1 |
import { useMemo } from "react";
|
| 2 |
import conferencesData from "@/data/conferences.yml";
|
| 3 |
import { X } from "lucide-react";
|
| 4 |
|
| 5 |
interface FilterBarProps {
|
| 6 |
+
selectedTags: Set<string>;
|
| 7 |
+
onTagSelect: (tags: Set<string>) => void;
|
| 8 |
}
|
| 9 |
|
| 10 |
+
const FilterBar = ({ selectedTags, onTagSelect }: FilterBarProps) => {
|
| 11 |
const uniqueTags = useMemo(() => {
|
| 12 |
const tags = new Set<string>();
|
| 13 |
if (Array.isArray(conferencesData)) {
|
|
|
|
| 17 |
}
|
| 18 |
});
|
| 19 |
}
|
| 20 |
+
return Array.from(tags).map(tag => ({
|
| 21 |
id: tag,
|
| 22 |
label: tag.split("-").map(word =>
|
| 23 |
word.charAt(0).toUpperCase() + word.slice(1)
|
| 24 |
).join(" "),
|
| 25 |
+
description: `${tag} Conferences`
|
| 26 |
}));
|
| 27 |
}, []);
|
| 28 |
|
|
|
|
| 34 |
<button
|
| 35 |
key={filter.id}
|
| 36 |
title={filter.description}
|
| 37 |
+
onClick={() => {
|
| 38 |
+
const newTags = new Set(selectedTags);
|
| 39 |
+
if (newTags.has(filter.id)) {
|
| 40 |
+
newTags.delete(filter.id);
|
| 41 |
+
} else {
|
| 42 |
+
newTags.add(filter.id);
|
| 43 |
+
}
|
| 44 |
+
onTagSelect(newTags);
|
| 45 |
+
}}
|
| 46 |
className={`
|
| 47 |
px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200
|
| 48 |
filter-tag
|
| 49 |
+
${selectedTags.has(filter.id)
|
| 50 |
? "bg-primary text-white shadow-sm filter-tag-active"
|
| 51 |
: "bg-neutral-50 text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900"
|
| 52 |
}
|
|
|
|
| 56 |
</button>
|
| 57 |
))}
|
| 58 |
|
| 59 |
+
{selectedTags.size > 0 && (
|
| 60 |
<button
|
| 61 |
+
onClick={() => onTagSelect(new Set())}
|
| 62 |
className="px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200
|
| 63 |
bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700
|
| 64 |
flex items-center gap-2"
|
src/pages/Index.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { Switch } from "@/components/ui/switch"
|
|
| 8 |
import { parseISO, isValid, isPast } from "date-fns";
|
| 9 |
|
| 10 |
const Index = () => {
|
| 11 |
-
const [
|
| 12 |
const [searchQuery, setSearchQuery] = useState("");
|
| 13 |
const [showPastConferences, setShowPastConferences] = useState(false);
|
| 14 |
|
|
@@ -25,20 +25,21 @@ const Index = () => {
|
|
| 25 |
const isUpcoming = !deadlineDate || !isValid(deadlineDate) || !isPast(deadlineDate);
|
| 26 |
if (!showPastConferences && !isUpcoming) return false;
|
| 27 |
|
| 28 |
-
// Filter by
|
| 29 |
-
const
|
|
|
|
| 30 |
const matchesSearch = searchQuery === "" ||
|
| 31 |
conf.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 32 |
(conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase()));
|
| 33 |
|
| 34 |
-
return
|
| 35 |
})
|
| 36 |
.sort((a: Conference, b: Conference) => {
|
| 37 |
const dateA = a.deadline && a.deadline !== 'TBD' ? parseISO(a.deadline).getTime() : Infinity;
|
| 38 |
const dateB = b.deadline && b.deadline !== 'TBD' ? parseISO(b.deadline).getTime() : Infinity;
|
| 39 |
return dateA - dateB;
|
| 40 |
});
|
| 41 |
-
}, [
|
| 42 |
|
| 43 |
if (!Array.isArray(conferencesData)) {
|
| 44 |
return <div>Loading conferences...</div>;
|
|
@@ -49,7 +50,7 @@ const Index = () => {
|
|
| 49 |
<Header onSearch={setSearchQuery} />
|
| 50 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 51 |
<div className="space-y-4 py-4">
|
| 52 |
-
<FilterBar
|
| 53 |
<div className="flex items-center gap-2">
|
| 54 |
<label htmlFor="show-past" className="text-sm text-neutral-600">
|
| 55 |
Show past conferences
|
|
|
|
| 8 |
import { parseISO, isValid, isPast } from "date-fns";
|
| 9 |
|
| 10 |
const Index = () => {
|
| 11 |
+
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
| 12 |
const [searchQuery, setSearchQuery] = useState("");
|
| 13 |
const [showPastConferences, setShowPastConferences] = useState(false);
|
| 14 |
|
|
|
|
| 25 |
const isUpcoming = !deadlineDate || !isValid(deadlineDate) || !isPast(deadlineDate);
|
| 26 |
if (!showPastConferences && !isUpcoming) return false;
|
| 27 |
|
| 28 |
+
// Filter by tags and search query
|
| 29 |
+
const matchesTags = selectedTags.size === 0 ||
|
| 30 |
+
(Array.isArray(conf.tags) && conf.tags.some(tag => selectedTags.has(tag)));
|
| 31 |
const matchesSearch = searchQuery === "" ||
|
| 32 |
conf.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 33 |
(conf.full_name && conf.full_name.toLowerCase().includes(searchQuery.toLowerCase()));
|
| 34 |
|
| 35 |
+
return matchesTags && matchesSearch;
|
| 36 |
})
|
| 37 |
.sort((a: Conference, b: Conference) => {
|
| 38 |
const dateA = a.deadline && a.deadline !== 'TBD' ? parseISO(a.deadline).getTime() : Infinity;
|
| 39 |
const dateB = b.deadline && b.deadline !== 'TBD' ? parseISO(b.deadline).getTime() : Infinity;
|
| 40 |
return dateA - dateB;
|
| 41 |
});
|
| 42 |
+
}, [selectedTags, searchQuery, showPastConferences]);
|
| 43 |
|
| 44 |
if (!Array.isArray(conferencesData)) {
|
| 45 |
return <div>Loading conferences...</div>;
|
|
|
|
| 50 |
<Header onSearch={setSearchQuery} />
|
| 51 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 52 |
<div className="space-y-4 py-4">
|
| 53 |
+
<FilterBar selectedTags={selectedTags} onTagSelect={setSelectedTags} />
|
| 54 |
<div className="flex items-center gap-2">
|
| 55 |
<label htmlFor="show-past" className="text-sm text-neutral-600">
|
| 56 |
Show past conferences
|