import React, { useEffect, useState } from "react"; import { Button } from "../../ui/atoms/button"; import { Search } from "../../ui/atoms/search"; import { Submenu } from "../../ui/molecules/submenu"; import "./index.scss"; import { DropdownMenu, DropdownMenuWrapper } from "../../ui/molecules/dropdown"; import { useSearchParams } from "react-router-dom"; import { camelUnwrap } from "../../utils"; export type AnyFilter = SelectFilter; type SelectFilter = { type: "select"; key: string; label: string; multiple?: { encode: (...values: string[]) => string; decode: (value: string) => string[]; }; options: { label: string; value: string; isDefault?: true; }[]; }; export type AnySort = { label: string; param: string; isDefault?: true }; enum Params { PAGE = "page", QUERY = "q", } export default function SearchFilter({ isDisabled = false, filters = [], sorts = [], onChange = (key, newValue, oldValue) => {}, }: { isDisabled?: boolean; filters?: AnyFilter[]; sorts?: AnySort[]; onChange?: ( key: string, newValue: string | null, oldValue: string | null ) => unknown; }) { const [searchParams, setSearchParams] = useSearchParams(); const [openFilter, setOpenFilter] = useState(null); const [isSortingOpen, setIsSortingOpen] = useState(false); const [terms, setTerms] = useState(""); useEffect( () => setTerms(searchParams.get(Params.QUERY) ?? ""), [searchParams] ); const sortedParams = (params: URLSearchParams): URLSearchParams => new URLSearchParams( [...params.entries()].sort(([a], [b]) => a.localeCompare(b)) ); const replaceSearchParam = (key: string, value: string | null) => { setSearchParams((params) => { const oldValue = params.get(key); if (value) { params.set(key, value); } else { params.delete(key); } onChange(key, value, oldValue); params.delete(Params.PAGE); return sortedParams(params); }); }; const isDefaultFilter = (key: string, value: string) => { const filter = filters.find((filter) => filter.key === key) as AnyFilter; const option = filter.options.find((option) => option.value === value); return option?.isDefault ?? false; }; const isCurrentFilter = (key: string, value: string) => { const currentValue = searchParams.get(key) ?? null; const filter = filters.find((filter) => filter.key === key) as AnyFilter; if (filter.multiple) { const values = typeof currentValue === "string" ? filter.multiple.decode(currentValue) : []; return values.includes(value); } else { return currentValue ? currentValue === value : isDefaultFilter(key, value); } }; const isDefaultSort = (param: string) => { const sort = sorts.find((sort) => sort.param === param); return sort?.isDefault ?? false; }; const isCurrentSort = (param: string) => { const [key, value] = param.split("=", 2); const currentValue = searchParams.get(key) ?? null; return currentValue ? currentValue === value : isDefaultSort(param); }; const toggleSelectedFilter = (key: string, value: string) => { const currentValue = searchParams.get(key) ?? null; const filter = filters.find((filter) => filter.key === key) as AnyFilter; let newValue: string | null; if (filter.multiple) { let values = typeof currentValue === "string" ? filter.multiple.decode(currentValue) : []; if (values.includes(value)) { values = values.filter((v) => v !== value); } else { values = [...values, value].sort(); } if (values.length) { newValue = filter.multiple.encode(...values); } else { newValue = null; } } else if (isDefaultFilter(key, value)) { newValue = null; } else { newValue = currentValue !== value ? value : null; } replaceSearchParam(key, newValue); }; const toggleSelectedSort = (param: string) => { const [key, value] = param.split("=", 2); const newValue = isDefaultSort(param) ? "" : value; replaceSearchParam(key, newValue); }; const setSelectedTerms = (newValue: string) => { replaceSearchParam("q", newValue); }; const filterMenus = filters.map((filter) => ({ key: filter.key, label: filter.label, id: `filters-menu-${filter.key}`, items: filter.options.map((option) => ({ component: () => ( ), })), })); const sortMenu = { label: "Sort", id: "sort-menu", items: sorts.map((sort) => ({ component: () => ( ), })), }; return (
{ event.preventDefault(); setSelectedTerms(terms); }} > {filterMenus.map((filterMenu) => ( setOpenFilter(isOpen ? filterMenu.key : null) } > ))} // The updates event list uses `camelWrap()` to insert // zero-width spaces, so we `camelUnwrap()` them here. // Otherwise we would not find a copy-pasted feature. setSelectedTerms(camelUnwrap(terms)) } onChangeHandler={(e) => setTerms(e.target.value)} onResetHandler={() => { setTerms(""); setSelectedTerms(""); }} /> {sorts.length ? ( ) : null} ); }