From 99c59ee988f05a8d47615d5f1397b96ff0c72af3 Mon Sep 17 00:00:00 2001 From: TetraTsunami <78718829+TetraTsunami@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:40:09 -0600 Subject: [PATCH] [feat] Add category filter Close Category and type filter DSSD-Madison/Red-CORAL#71 --- src/components/filters/AddFilter.tsx | 3 +- src/components/filters/CategoryFilter.tsx | 137 ++++++++++++++++++++++ src/components/filters/CountryFilter.tsx | 26 ++++ 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/components/filters/CategoryFilter.tsx diff --git a/src/components/filters/AddFilter.tsx b/src/components/filters/AddFilter.tsx index b7d76f3..4d8ceaa 100644 --- a/src/components/filters/AddFilter.tsx +++ b/src/components/filters/AddFilter.tsx @@ -17,12 +17,13 @@ import { arrow, } from '@floating-ui/react' import CountryFilter from './CountryFilter' +import CategoryFilter from './CategoryFilter' const possibleFilters = [ { name: 'Actividades', icon: LucideTags, - component: DateFilter, // TODO: Change this to the actual component + component: CategoryFilter, }, { name: 'Fecha', diff --git a/src/components/filters/CategoryFilter.tsx b/src/components/filters/CategoryFilter.tsx new file mode 100644 index 0000000..4e8cf5b --- /dev/null +++ b/src/components/filters/CategoryFilter.tsx @@ -0,0 +1,137 @@ +import { filterProps } from '@/pages/StatsDashboard' +import BaseFilter from './BaseFilter' +import { LucideTags, LucideTrash2 } from 'lucide-react' +import { useMemo, useState } from 'react' +import { Incident } from '@/types' + +const CategoryFilter = ({ id, data, dispatch }: filterProps) => { + const [hiddenCategories, setHiddenCategories] = useState([]) + const [hiddenTypes, setHiddenTypes] = useState([]) + + const typesByCategory = useMemo(() => { + return Object.entries(data.Types).reduce( + (acc, [typeID, type]) => { + if (!acc[type.categoryID]) { + acc[type.categoryID] = [] + } + acc[type.categoryID].push({ typeID, name: type.name }) + return acc + }, + {} as { [key: string]: { typeID: string; name: string }[] } + ) + }, [data.Types]) + + const handleCategoryChange = (categoryID: string, makeVisible: boolean) => { + // Clobber the state of its descendants + setHiddenTypes((prev) => prev.filter((t) => !typesByCategory[categoryID]?.some(({ typeID }) => typeID === t))) + // Set the state of the category + setHiddenCategories((prev) => (makeVisible ? prev.filter((c) => c !== categoryID) : [...prev, categoryID])) + } + + const handleTypeChange = (categoryID: string, typeID: string, makeVisible: boolean) => { + // If trying to show a type, ensure its category is visible, but hide all other types in it + if (makeVisible && hiddenCategories.includes(categoryID)) { + handleCategoryChange(categoryID, true) + setHiddenTypes((prev) => [...prev, ...typesByCategory[categoryID].map((t) => t.typeID)]) + } + // If all types are hidden, hide the category instead + const typesInCategory = typesByCategory[categoryID]?.map((t) => t.typeID) || [] + const hiddenTypesInCategory = hiddenTypes.filter((t) => typesInCategory.includes(t)) + if (!makeVisible && hiddenTypesInCategory.length + 1 === typesInCategory.length) { + handleCategoryChange(categoryID, false) + } else { + setHiddenTypes((prev) => (makeVisible ? prev.filter((t) => t !== typeID) : [...prev, typeID])) + } + } + + const selectAllCategories = (selectAll: boolean) => { + if (selectAll) { + setHiddenCategories([]) + setHiddenTypes([]) + } else { + setHiddenCategories(Object.keys(data.Categories)) + setHiddenTypes(Object.keys(data.Types)) + } + } + + const applyFilters = () => { + const incidentNotHidden = (incident: Incident) => + !hiddenCategories.includes(data.Types[incident.typeID as string].categoryID as string) && !hiddenTypes.includes(incident.typeID as string) + + dispatch({ + type: 'UPDATE_FILTER', + payload: { + id: id, + operation: incidentNotHidden, + }, + }) + } + + const removeThisFilter = () => { + dispatch({ type: 'REMOVE_FILTER', payload: { id: id } }) + } + + const filterString = [] + if (hiddenCategories.length === 1) { + filterString.push(`1 categoría oculta`) + } else if (hiddenCategories.length > 1) { + filterString.push(`${hiddenCategories.length} categorías ocultas`) + } + if (hiddenTypes.length === 1) { + filterString.push(`1 tipo oculto`) + } else if (hiddenTypes.length > 1) { + filterString.push(`${hiddenTypes.length} tipos ocultos`) + } + if (filterString.length === 0) { + filterString.push('ningún filtro aplicado') + } + + return ( + } text={'Categorías: ' + filterString.join(', ')}> + +
+ + + {Object.entries(data.Categories).map(([categoryID, category]) => ( +
+ + handleCategoryChange(categoryID, e.target.checked)} + className="mr-2" + /> + {category.name} + +
+
    + {typesByCategory[categoryID]?.map(({ typeID, name: typeName }) => ( +
  • + handleTypeChange(categoryID, typeID, e.target.checked)} + className="mr-2" + /> + {typeName} +
  • + ))} +
+
+
+ ))} + +
+
+ ) +} + +export default CategoryFilter diff --git a/src/components/filters/CountryFilter.tsx b/src/components/filters/CountryFilter.tsx index 7def17a..4c49e6c 100644 --- a/src/components/filters/CountryFilter.tsx +++ b/src/components/filters/CountryFilter.tsx @@ -68,6 +68,26 @@ const CountryFilter = ({ id, data, dispatch }: filterProps) => { } } + const selectAllCountries = (selectAll: boolean) => { + if (selectAll) { + setHiddenCountries([]) + setHiddenDepartments([]) + setHiddenMunicipalities([]) + } else { + setHiddenCountries(Object.keys(data.filterBounds.locations)) + setHiddenDepartments( + Object.entries(data.filterBounds.locations).flatMap(([country, departments]) => + Object.keys(departments).map((dept) => `${country} - ${dept}`) + ) + ) + setHiddenMunicipalities( + Object.entries(data.filterBounds.locations).flatMap(([country, departments]) => + Object.entries(departments).flatMap(([dept, municipalities]) => municipalities.map((muni) => `${country} - ${dept} - ${muni}`)) + ) + ) + } + } + const applyFilters = () => { const incidentNotHidden = (incident: Incident) => !hiddenCountries.includes(incident.country) && @@ -113,6 +133,12 @@ const CountryFilter = ({ id, data, dispatch }: filterProps) => {
+ + {Object.entries(data.filterBounds.locations).map(([country, departments]) => (