From 91b7a8e6cb7084d1e9b1e35f6dc8fd7d218457ca Mon Sep 17 00:00:00 2001 From: Donovan Date: Mon, 20 Jan 2025 17:37:54 -0500 Subject: [PATCH] first commit --- src/components/FilterView.tsx | 13 ++- src/components/Filters/AutoComplete.tsx | 59 ++++++++++ .../Filters/AutoCompleteVariants.tsx | 43 ++++++++ src/components/Filters/DimensionFilter.tsx | 101 +++++++++++++----- src/components/Filters/RangedInputs.tsx | 71 ++++++++++++ src/components/Filters/filterOptions.ts | 9 ++ src/components/PropertyMap.tsx | 32 ++++-- src/context/FilterContext.tsx | 50 ++++++--- 8 files changed, 332 insertions(+), 46 deletions(-) create mode 100644 src/components/Filters/AutoComplete.tsx create mode 100644 src/components/Filters/AutoCompleteVariants.tsx create mode 100644 src/components/Filters/RangedInputs.tsx diff --git a/src/components/FilterView.tsx b/src/components/FilterView.tsx index 34e6ebfd..cca02486 100644 --- a/src/components/FilterView.tsx +++ b/src/components/FilterView.tsx @@ -4,7 +4,12 @@ import { FC } from 'react'; import { PiX } from 'react-icons/pi'; import { ThemeButton } from './ThemeButton'; import { BarClickOptions } from '@/app/find-properties/[[...opa_id]]/page'; -import { rcos, neighborhoods, zoning } from './Filters/filterOptions'; +import { + rcos, + neighborhoods, + zoning, + marketValues, +} from './Filters/filterOptions'; import DimensionFilter from './Filters/DimensionFilter'; const filters = [ @@ -14,6 +19,12 @@ const filters = [ options: ['Low', 'Medium', 'High'], type: 'buttonGroup', }, + { + property: 'market_value', + display: 'Market Value', + options: marketValues, + type: 'range', + }, { property: 'get_access', display: 'Get Access', diff --git a/src/components/Filters/AutoComplete.tsx b/src/components/Filters/AutoComplete.tsx new file mode 100644 index 00000000..00dbab2a --- /dev/null +++ b/src/components/Filters/AutoComplete.tsx @@ -0,0 +1,59 @@ +'use client'; + +import React, { FC } from 'react'; +import { + AutocompleteFilter, + AutocompleteFilterItem, +} from './AutoCompleteVariants'; + +type AutocompleteProps = { + display: string; + options: string[]; + limitType: string; + selectedRange: string | React.ChangeEvent; + setSelectedRange: (prev: any) => void; + aria_describedby_label?: string; + handleSelectionChange: (selection: string, limitType: string) => void; +}; + +export const Autocomplete: FC = ({ + options, + limitType, + selectedRange, + setSelectedRange, + aria_describedby_label, + handleSelectionChange, +}) => { + const selectedKey = options.indexOf(selectedRange as string).toString(); + + const onEnter = (key: string) => { + setSelectedRange((prev: any) => ({ + ...prev, + [limitType]: key, + })); + handleSelectionChange(key, limitType); + }; + return ( +
+ + key.key === 'Enter' && onEnter(key.currentTarget.value) + } + placeholder="Select options..." + > + {options.map((option: string, index: number) => ( + + {option} + + ))} + +
+ ); +}; + +export default Autocomplete; diff --git a/src/components/Filters/AutoCompleteVariants.tsx b/src/components/Filters/AutoCompleteVariants.tsx new file mode 100644 index 00000000..fcc075a0 --- /dev/null +++ b/src/components/Filters/AutoCompleteVariants.tsx @@ -0,0 +1,43 @@ +import { + Autocomplete, + AutocompleteItem, + extendVariants, +} from '@nextui-org/react'; + +export const AutocompleteFilterItem = extendVariants(AutocompleteItem, { + variants: { + color: { + gray: { + title: ['text-gray-900'], + }, + }, + size: { + md: { + wrapper: 'm-12', + }, + }, + }, + defaultVariants: { + color: 'gray', + size: 'md', + }, +}); + +export const AutocompleteFilter = extendVariants(Autocomplete, { + variants: { + color: { + gray: { + base: 'text-gray-900', + }, + }, + size: { + md: { + base: 'py-2', + }, + }, + }, + defaultVariants: { + color: 'gray', + size: 'md', + }, +}); diff --git a/src/components/Filters/DimensionFilter.tsx b/src/components/Filters/DimensionFilter.tsx index 335c08b5..8da6964c 100644 --- a/src/components/Filters/DimensionFilter.tsx +++ b/src/components/Filters/DimensionFilter.tsx @@ -5,6 +5,7 @@ import { useFilter } from '@/context/FilterContext'; import ButtonGroup from './ButtonGroup'; import MultiSelect from './MultiSelect'; import Panels from './Panels'; +import RangedInputs from './RangedInputs'; type DimensionFilterProps = { property: string; @@ -36,10 +37,17 @@ const DimensionFilter: FC = ({ const [selectedKeys, setSelectedKeys] = useState( appFilter[property]?.values || [] ); + const [selectedRanges, setSelectedRanges] = useState<{ + min: string | React.ChangeEvent; + max: string | React.ChangeEvent; + }>({ + min: appFilter[property]?.rangedValues?.min as string, + max: appFilter[property]?.rangedValues?.max as string, + }); const initialSelectedPanelKeys = () => { const panelKeyObj: { [key: string]: string[] } = {}; for (const key in appFilter) { - panelKeyObj[key] = appFilter[key].values; + panelKeyObj[key] = appFilter[key].values || []; } return panelKeyObj; }; @@ -87,9 +95,20 @@ const DimensionFilter: FC = ({ }; const handleSelectionChange = ( - selection: React.ChangeEvent | string + selection: React.ChangeEvent | string, + limitType?: string ) => { let newMultiSelect: string[] = []; + const newRangeValues = { ...selectedRanges }; + + if (limitType) { + if (limitType === 'min') { + newRangeValues.min = selection; + } else if (limitType === 'max') { + newRangeValues.max = selection; + } + } + if (typeof selection === 'string') { newMultiSelect = selectedKeys.includes(selection) ? selectedKeys.filter((key) => key !== selection) @@ -99,13 +118,23 @@ const DimensionFilter: FC = ({ newMultiSelect = selection.target.value.split(','); } } - setSelectedKeys(newMultiSelect); - dispatch({ - type: 'SET_DIMENSIONS', - property, - dimensions: newMultiSelect, - useIndexOfFilter, - }); + if (limitType) { + dispatch({ + type: 'SET_DIMENSIONS', + property, + limitType, + dimensions: newRangeValues, + useIndexOfFilter, + }); + } else { + setSelectedKeys(newMultiSelect); + dispatch({ + type: 'SET_DIMENSIONS', + property, + dimensions: newMultiSelect, + useIndexOfFilter, + }); + } }; const filter = useMemo(() => { @@ -130,6 +159,17 @@ const DimensionFilter: FC = ({ aria_describedby_label={filterLabelID} /> ); + } else if (type === 'range') { + return ( + + ); } else { return ( = ({ options={options} selectedKeys={selectedKeys} toggleDimension={toggleDimension} - handleSelectionChange={handleSelectionChange} + handleSelectionChange={handleSelectionChange as any} aria_describedby_label={filterLabelID} /> ); @@ -150,10 +190,15 @@ const DimensionFilter: FC = ({ desc: 'Find properties based on how much they can reduce gun violence considering the gun violence, cleanliness, and tree canopy nearby. ', linkFragment: 'priority-method', } - : { - desc: 'Find properties based on what we think is the easiest method to get legal access to them, based on the data available to us. ', - linkFragment: 'access-method', - }; + : property === 'market_value' + ? { + desc: 'Find properties based on their market value (USD). ', + linkFragment: 'access-method', + } + : { + desc: 'Find properties based on what we think is the easiest method to get legal access to them, based on the data available to us. ', + linkFragment: 'access-method', + }; // text-gray-500, 600 ? or #586266 (figma)? return ( @@ -162,20 +207,24 @@ const DimensionFilter: FC = ({

{display}

- {(property === 'get_access' || property === 'priority_level') && ( + {(property === 'get_access' || + property === 'priority_level' || + property === 'market_value') && (

{filterDescription.desc} - - Learn more{' '} - + {(property === 'get_access' || property === 'priority_level') && ( + + Learn more{' '} + + )}

)} diff --git a/src/components/Filters/RangedInputs.tsx b/src/components/Filters/RangedInputs.tsx new file mode 100644 index 00000000..26d002e5 --- /dev/null +++ b/src/components/Filters/RangedInputs.tsx @@ -0,0 +1,71 @@ +'use client'; + +import React, { FC } from 'react'; +import Autocomplete from './AutoComplete'; + +type RangedInputsProps = { + display: string; + options: string[]; + selectedRanges: + | { + min: string | React.ChangeEvent; + max: string | React.ChangeEvent; + } + | undefined; + setSelectedRanges: (prev: any) => void; + aria_describedby_label?: string; + handleSelectionChange: (selection: string) => void; +}; + +const RangedInputs: FC = ({ + display, + options, + selectedRanges, + setSelectedRanges, + aria_describedby_label, + handleSelectionChange, +}) => { + return ( +
+
+ + +
+
+
+ + +
+
+ ); +}; + +export default RangedInputs; diff --git a/src/components/Filters/filterOptions.ts b/src/components/Filters/filterOptions.ts index d387d10d..48570d4f 100644 --- a/src/components/Filters/filterOptions.ts +++ b/src/components/Filters/filterOptions.ts @@ -159,6 +159,15 @@ export const neighborhoods: string[] = [ 'Yorktown', ]; +export const marketValues: string[] = [ + '$10,000', + '$20,000', + '$30,000', + '$40,000', + '$50,000', + '$60,000', +]; + export const rcos: string[] = [ '10th Democratic Ward', '12th Ward Democratic Committee', diff --git a/src/components/PropertyMap.tsx b/src/components/PropertyMap.tsx index ea8c9eb5..ebb41e5b 100644 --- a/src/components/PropertyMap.tsx +++ b/src/components/PropertyMap.tsx @@ -181,6 +181,12 @@ const PropertyMap: FC = ({ const onMapClick = (e: MapMouseEvent) => { handleMapClick(e.lngLat); }; + const formatRangeValue = (value: string) => { + if (value === '') { + return null; + } + return parseFloat(value.replace('$', '').replace(',', '')); + }; const moveMap = (targetPoint: LngLatLike) => { if (map) { @@ -335,9 +341,8 @@ const PropertyMap: FC = ({ // update filters on both layers for ease of switching between layers const updateFilter = () => { if (!map) return; - const isAnyFilterEmpty = Object.values(appFilter).some((filterItem) => { - return filterItem.values.length === 0; + return filterItem.values?.length === 0; }); if (isAnyFilterEmpty) { @@ -349,8 +354,10 @@ const PropertyMap: FC = ({ const mapFilter = Object.entries(appFilter).reduce( (acc, [property, filterItem]) => { - if (filterItem.values.length) { - const thisFilterGroup: any = ['any']; + const thisFilterGroup: any = ['any']; + const { limitType } = filterItem; + + if (filterItem.values?.length) { filterItem.values.forEach((item) => { if (filterItem.useIndexOfFilter) { thisFilterGroup.push([ @@ -362,9 +369,22 @@ const PropertyMap: FC = ({ thisFilterGroup.push(['in', ['get', property], item]); } }); - - acc.push(thisFilterGroup); + } else if (filterItem.rangedValues) { + if (limitType === 'min' || limitType === 'max') { + if (filterItem.rangedValues[limitType] === null) { + thisFilterGroup.push(['>=', ['get', property]]); + } else { + thisFilterGroup.push([ + '>=', + ['get', property], + formatRangeValue( + filterItem?.rangedValues[limitType] as string + ), + ]); + } + } } + acc.push(thisFilterGroup); return acc; }, [] as any[] diff --git a/src/context/FilterContext.tsx b/src/context/FilterContext.tsx index ae6f7e4b..dce84daa 100644 --- a/src/context/FilterContext.tsx +++ b/src/context/FilterContext.tsx @@ -8,8 +8,13 @@ import React, { export interface DimensionFilter { type: 'dimension'; - values: string[]; + values?: string[]; + rangedValues?: { + max: string | React.ChangeEvent; + min: string | React.ChangeEvent; + }; useIndexOfFilter?: boolean; + limitType?: string | boolean; } interface FilterState { @@ -24,8 +29,14 @@ interface FilterContextProps { type FilterAction = { type: 'SET_DIMENSIONS' | 'CLEAR_DIMENSIONS'; property: string; - dimensions: string[]; + dimensions: + | string[] + | { + max: string | React.ChangeEvent; + min: string | React.ChangeEvent; + }; useIndexOfFilter?: boolean; + limitType?: string | boolean; }; const filterReducer = ( @@ -34,18 +45,31 @@ const filterReducer = ( ): FilterState => { switch (action.type) { case 'SET_DIMENSIONS': - if (action.dimensions.length === 0) { - const { [action.property]: _, ...rest } = state; - return rest; + if (Array.isArray(action.dimensions)) { + if (action.dimensions.length === 0) { + const { [action.property]: _, ...rest } = state; + return rest; + } + return { + ...state, + [action.property]: { + type: 'dimension', + values: action.dimensions, + useIndexOfFilter: action.useIndexOfFilter || false, + }, + }; + } else { + return { + ...state, + [action.property]: { + type: 'dimension', + rangedValues: action.dimensions, + useIndexOfFilter: action.useIndexOfFilter || false, + limitType: action.limitType, + }, + }; } - return { - ...state, - [action.property]: { - type: 'dimension', - values: action.dimensions, - useIndexOfFilter: action.useIndexOfFilter || false, - }, - }; + case 'CLEAR_DIMENSIONS': return {}; default: