diff --git a/components/Dropdown.tsx b/components/Dropdown.tsx
deleted file mode 100644
index 827bcd1..0000000
--- a/components/Dropdown.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-import { ChevronDown } from '@styled-icons/fa-solid/ChevronDown';
-
-const findOptionFromValue = (options, value) => {
- return options.find(option => {
- if (!value) {
- return option.value === '';
- }
-
- return option.value === value;
- });
-};
-
-export default function DropdownSelector({
- options,
- fieldLabel,
- value,
- onChange,
- currentCategoryColor,
- ariaLabel,
-}: {
- options: any;
- fieldLabel: any;
- value: any;
- currentCategoryColor: string;
- ariaLabel: string;
- onChange: (any) => void;
- onOpen?: () => void;
-}) {
- const selectedOption = findOptionFromValue(options, value);
- const breakIndex = options.findIndex(option => option.break);
- const lastVisibleIndex = breakIndex > 0 ? breakIndex : options.length;
- const filterApplied = selectedOption.value !== options[0].value;
-
- return (
-
-
-
-
- );
-}
diff --git a/components/FilterArea.tsx b/components/FilterArea.tsx
index cd9e87e..a9db768 100644
--- a/components/FilterArea.tsx
+++ b/components/FilterArea.tsx
@@ -2,8 +2,9 @@ import React, { Fragment } from 'react';
import AnimateHeight from 'react-animate-height';
import CategoryFilter from './CategorySelect';
-import Dropdown from './Dropdown';
-import { CloseIcon, DateIcon, FilterIcon, LocationPin } from './Icons';
+import { CloseIcon, FilterIcon } from './Icons';
+import { LocationFilter } from './LocationFilter';
+import { TimeFilter } from './TimeFilter';
export const Filters = ({
filter,
@@ -16,7 +17,6 @@ export const Filters = ({
collectivesInView,
}) => {
const [expanded, setExpanded] = React.useState(!mobile);
-
return (
{mobile && (
@@ -49,43 +49,23 @@ export const Filters = ({
-
-
- Location
-
- }
- options={locationOptions.map(option => ({
- ...option,
- value: JSON.stringify({ type: option.type, value: option.value }),
- }))}
- value={JSON.stringify(filter.location)}
- onOpen={() => {
- mobile && setExpanded(false);
- }}
- onChange={option => {
- setFilter({ location: JSON.parse(option.value) });
+ locationOptions={locationOptions}
+ setLocationFilter={locationFilter => {
+ setFilter({ location: locationFilter });
}}
/>
-
-
- Date range
-
- }
options={[
{ value: 'ALL', label: 'All time' },
{ value: 'PAST_YEAR', label: 'Past 12 months' },
{ value: 'PAST_QUARTER', label: 'Past 3 months' },
]}
- value={filter.timePeriod}
- onChange={({ value }) => {
+ setTimeFilter={value => {
setFilter({ timePeriod: value });
}}
/>
diff --git a/components/FilterButton.tsx b/components/FilterButton.tsx
new file mode 100644
index 0000000..60ee66d
--- /dev/null
+++ b/components/FilterButton.tsx
@@ -0,0 +1,39 @@
+import React, { forwardRef } from 'react';
+
+import { ChevronUpDown } from './Icons';
+
+interface Props {
+ currentCategoryColor: string;
+ selectedOption?: any;
+ icon: any;
+ label: string;
+ onClick?: any;
+}
+
+const FilterButton: React.ForwardRefRenderFunction = (
+ { currentCategoryColor, selectedOption, icon, label, onClick },
+ ref,
+) => {
+ return (
+
+ );
+};
+
+export default forwardRef(FilterButton);
diff --git a/components/LocationFilter.tsx b/components/LocationFilter.tsx
new file mode 100644
index 0000000..ea7da06
--- /dev/null
+++ b/components/LocationFilter.tsx
@@ -0,0 +1,257 @@
+import React, { Fragment, useState } from 'react';
+import { Dialog, Transition } from '@headlessui/react';
+import { ChevronDown } from '@styled-icons/fa-solid/ChevronDown';
+import { ChevronUp } from '@styled-icons/fa-solid/ChevronUp';
+
+import FilterButton from './FilterButton';
+import { CloseIcon, LocationPin } from './Icons';
+
+export const LocationFilter = ({ locationOptions, currentCategoryColor, filter, setLocationFilter }) => {
+ const [open, setOpen] = useState(false);
+ const selectedOption = locationOptions.find(
+ option => option.value === filter.location.value && option.type === filter.location.type,
+ );
+ return (
+
+ }
+ onClick={() => setOpen(true)}
+ currentCategoryColor={currentCategoryColor}
+ selectedOption={selectedOption}
+ />
+ {open && (
+ setOpen(false)}
+ options={locationOptions}
+ setLocationFilter={setLocationFilter}
+ filter={filter}
+ />
+ )}
+
+ );
+};
+
+const LocationFilterModal = ({ open, handleClose, options, filter, setLocationFilter }) => {
+ const [activeFilter, setActiveFilter] = useState<{ region?: string; country?: string; city?: string }>({
+ region: filter?.location?.type === 'region' ? filter?.location?.value : undefined,
+ country: filter?.location?.type === 'country' ? filter?.location?.value : undefined,
+ city: filter?.location?.type === 'city' ? filter?.location?.value : undefined,
+ });
+
+ const [query, setQuery] = useState('');
+ const filteredOptions = {
+ regions: options
+ .filter(option => option.type === 'region')
+ .filter(option =>
+ option.label.toLowerCase().replace(/\s+/g, '').includes(query.toLowerCase().replace(/\s+/g, '')),
+ ),
+ countries: options
+ .filter(option => option.type === 'country')
+ .filter(option => !activeFilter.region || activeFilter.region === option.region)
+ .filter(option =>
+ option.label.toLowerCase().replace(/\s+/g, '').includes(query.toLowerCase().replace(/\s+/g, '')),
+ ),
+ cities: options
+ .filter(option => option.type === 'city')
+ .filter(option => !activeFilter.region || activeFilter.region === option.region)
+ .filter(option => !activeFilter.country || activeFilter.country === option.country)
+ .filter(option =>
+ option.label.toLowerCase().replace(/\s+/g, '').includes(query.toLowerCase().replace(/\s+/g, '')),
+ ),
+ };
+
+ const applyFilter = () => {
+ const locationFilter = {
+ type: activeFilter.city ? 'city' : activeFilter.country ? 'country' : activeFilter.region ? 'region' : null,
+ value: activeFilter.city || activeFilter.country || activeFilter.region || null,
+ };
+
+ setLocationFilter(locationFilter);
+ handleClose();
+ };
+
+ return (
+
+
+
+ );
+};
+
+const LocationSection = ({ label, options, onSelect, activeFilter, grow, filter, field }) => {
+ const [expanded, setExpanded] = useState(
+ !filter.location.value || filter.location.type === field || filter.location.type === 'region',
+ );
+
+ return (
+
+
+ {expanded && (
+
+
+ {options.map(option => {
+ return (
+
+ );
+ })}
+
+
+ )}
+
+ );
+};
+
+const LocationOption = ({ option, onSelect, activeFilter }) => {
+ const ref = React.useRef(null);
+
+ // scroll active option into view when opening the modal
+ React.useEffect(() => {
+ if (active) {
+ setTimeout(() => {
+ ref.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }, 50);
+ }
+ }, []);
+
+ const active = activeFilter && activeFilter[option.type] === option.value;
+
+ let filtersToKeep = {};
+ if (option.type === 'region') {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { city, country, region, ...rest } = activeFilter;
+ filtersToKeep = rest;
+ } else if (option.type === 'country') {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { city, country, ...rest } = activeFilter;
+ filtersToKeep = rest;
+ } else if (option.type === 'city') {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { city, ...rest } = activeFilter;
+ filtersToKeep = rest;
+ }
+
+ return (
+
+ );
+};
diff --git a/components/TimeFilter.tsx b/components/TimeFilter.tsx
new file mode 100644
index 0000000..059f32a
--- /dev/null
+++ b/components/TimeFilter.tsx
@@ -0,0 +1,104 @@
+import React, { Fragment, useEffect, useRef, useState } from 'react';
+import { Menu, Transition } from '@headlessui/react';
+
+import FilterButton from './FilterButton';
+import { DateIcon } from './Icons';
+
+const findOptionFromValue = (options, value) => {
+ return options.find(option => {
+ return option.value === value;
+ });
+};
+
+export function TimeFilter({
+ filter,
+ options,
+ currentCategoryColor,
+ setTimeFilter,
+}: {
+ options: any;
+ filter: any;
+ currentCategoryColor: string;
+ setTimeFilter: (any) => void;
+}) {
+ const selectedOption = findOptionFromValue(options, filter.timePeriod);
+ const buttonRef = useRef(null);
+ return (
+
+
+
+ );
+}
+
+const MenuContainer = ({ options, setTimeFilter, open, buttonRef }) => {
+ const [dropdownPlacement, setDropdownPlacement] = useState('bottom');
+
+ // put dropdown above button if it's too close to the bottom of the screen
+ useEffect(() => {
+ if (!buttonRef.current) {
+ return;
+ }
+ const { top } = buttonRef.current.getBoundingClientRect();
+ const windowHeight = window.innerHeight;
+ const distance = windowHeight - top;
+ if (distance < 175) {
+ setDropdownPlacement('top');
+ } else {
+ setDropdownPlacement('bottom');
+ }
+ }, [open]);
+
+ return (
+
+
+
+ {options.map(option => {
+ return (
+
+ {({ active }) => (
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/utils/location/data/countries.json b/utils/location/data/countries.json
index c68aa00..291da1e 100644
--- a/utils/location/data/countries.json
+++ b/utils/location/data/countries.json
@@ -678,7 +678,7 @@
{ "name": "Ukraine", "code": "UA", "code3chars": "UKR", "region": "Europe", "subRegion": "Eastern Europe" },
{ "name": "United Arab Emirates", "code": "AE", "code3chars": "ARE", "region": "Asia", "subRegion": "Western Asia" },
{
- "name": "United Kingdom of Great Britain and Northern Ireland",
+ "name": "United Kingdom",
"code": "GB",
"code3chars": "GBR",
"region": "Europe",
diff --git a/utils/location/get-filter-options.ts b/utils/location/get-filter-options.ts
index ea4ddf0..552e8a9 100644
--- a/utils/location/get-filter-options.ts
+++ b/utils/location/get-filter-options.ts
@@ -5,6 +5,8 @@ type LocationOption = {
value: string;
label?: string;
count: number;
+ country?: string;
+ region?: string;
};
export default function getFilterOptions(collectives) {
@@ -33,6 +35,7 @@ export default function getFilterOptions(collectives) {
foundLocations.countries[c.location.countryCode] = {
type: 'country',
value: c.location.countryCode,
+ region: c.location.region,
count: (foundLocations.countries[c.location.countryCode]?.count || 0) + 1,
};
}
@@ -49,50 +52,29 @@ export default function getFilterOptions(collectives) {
value: c.location.city,
label: c.location.city,
count: (foundLocations.cities[c.location.city]?.count || 0) + 1,
+ country: c.location.countryCode,
+ region: c.location.region,
};
}
}
});
- const regions = Object.values(foundLocations.regions).sort((a, b) => a.label.localeCompare(b.label));
+ const regions = Object.values(foundLocations.regions).sort((a, b) => b.count - a.count);
- const countries = Object.values(foundLocations.countries).map(c => {
- const country = countriesData.find(c2 => c2.code === c.value);
- return { ...c, label: country.name, region: country.region };
- });
+ const countries = Object.values(foundLocations.countries)
+ .map(c => {
+ const country = countriesData.find(c2 => c2.code === c.value);
+ return { ...c, label: country.name, region: country.region };
+ })
+ .sort((a, b) => b.count - a.count);
- const states = Object.values(foundLocations.states);
const cities = Object.values(foundLocations.cities).sort((a, b) => b.count - a.count);
- const topCities = cities.slice(0, 5);
- const restCities = cities.slice(5);
-
- // add top cities with hr below
- const regionsAndCountriesNested = [];
- // for each region
- regions.forEach(region => {
- // add the region to the options
- regionsAndCountriesNested.push(region);
- // add the countries in that region to the options
- countries
- .filter(country => {
- return country.region === region.value;
- })
- .sort((a, b) => a.label.localeCompare(b.label))
- .forEach(country => {
- regionsAndCountriesNested.push(country);
- });
- });
-
return [
- { type: null, value: null, label: 'All locations', count: collectives.length },
- ...topCities,
- { hr: true },
- ...regionsAndCountriesNested,
- { break: true },
- ...restCities,
- ...states,
- { type: 'other', value: 'online', label: 'Online', count: collectives.filter(c => c.location?.isOnline).length },
- { type: 'other', value: 'global', label: 'Global', count: collectives.filter(c => c.location?.isGlobal).length },
+ ...regions,
+ ...countries,
+ ...cities,
+ { type: 'other', value: 'online', label: 'Online' },
+ { type: 'other', value: 'global', label: 'Global' },
];
}