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
         | 

