diff --git a/hooks/use-debounce.ts b/hooks/use-debounce.ts new file mode 100644 index 0000000..bd131c1 --- /dev/null +++ b/hooks/use-debounce.ts @@ -0,0 +1,17 @@ +import { useCallback, useRef } from "react"; + +export const useDebounce = (fn: Function, ms: number = 500) => { + const timeoutRef = useRef(null); + + return useCallback( + (...args: any[]) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + return fn(...args); + }, ms); + }, + [fn, ms] + ); +}; diff --git a/hooks/use-fuse.ts b/hooks/use-fuse.ts new file mode 100644 index 0000000..e336171 --- /dev/null +++ b/hooks/use-fuse.ts @@ -0,0 +1,29 @@ +import Fuse from "fuse.js"; +import { useCallback } from "react"; + +export const useFuse = ( + data: any[], + keys: string[], + setter: (values: { label: string; value: string }[]) => void +) => { + return useCallback( + (search) => { + const fuse = new Fuse(data, { + keys, + threshold: 0.4, + }); + if (search.length <= 2) { + setter([]); + } else if (search.length > 2) { + const results = fuse.search(search).slice(0, 10); + setter( + results.map(({ item }) => ({ + label: `${item.nom} (${item.code})`, + value: item.code, + })) + ); + } + }, + [data, keys, setter] + ); +}; diff --git a/package.json b/package.json index 78eedc1..aea1aff 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "cors": "^2.8.5", "date-fns": "^2.29.3", "express": "^4.18.2", + "fuse.js": "6.6.2", "got": "11.8", "lodash": "^4.17.21", "mongodb": "^5.6.0", diff --git a/pages/communes.tsx b/pages/communes.tsx index 50a6a0e..ec1a4c3 100644 --- a/pages/communes.tsx +++ b/pages/communes.tsx @@ -1,59 +1,58 @@ -import {useState, useEffect} from 'react' -import {useRouter} from 'next/router' -import allCommunes from '@etalab/decoupage-administratif/data/communes.json' -import {SearchBar} from '@codegouvfr/react-dsfr/SearchBar' +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import allCommunes from "@etalab/decoupage-administratif/data/communes.json"; +import { SearchBar } from "@codegouvfr/react-dsfr/SearchBar"; -import SearchInput from '@/components/search-input' +import SearchInput from "@/components/search-input"; +import { useFuse } from "@/hooks/use-fuse"; +import { useDebounce } from "@/hooks/use-debounce"; -const communes = (allCommunes as Array<{nom: string; code: string; type: string}>).filter(c => ['commune-actuelle', 'arrondissement-municipal'].includes(c.type)) +const communes = ( + allCommunes as Array<{ nom: string; code: string; type: string }> +).filter((c) => + ["commune-actuelle", "arrondissement-municipal"].includes(c.type) +); const Communes = () => { - const router = useRouter() + const router = useRouter(); - const [value, setValue] = useState(null) - const [inputValue, setInputValue] = useState('') - const [options, setOptions] = useState([]) + const [value, setValue] = useState(null); + const [inputValue, setInputValue] = useState(""); + const [options, setOptions] = useState<{ label: string; value: string }[]>( + [] + ); + const fuzzySearch = useFuse(communes, ["nom", "code"], setOptions); + const debouncedFuzzySearch = useDebounce(fuzzySearch, 500); useEffect(() => { - if (inputValue.length <= 2) { - setOptions([]) - } else if (inputValue.length > 2) { - const newOptions = communes - .filter(c => - String(c.code).toLowerCase().includes(inputValue.toLowerCase()) - || c.nom.toLowerCase().includes(inputValue.toLowerCase()), - ) - .map(c => ({label: `${c.nom} (${c.code})`, value: c.code})) - .slice(0, 10) - setOptions(newOptions) - } - }, [inputValue]) + debouncedFuzzySearch(inputValue); + }, [debouncedFuzzySearch, inputValue]); useEffect(() => { if (value?.value) { - void router.push({pathname: `/communes/${(value.value as string)}`}) + void router.push({ pathname: `/communes/${value.value as string}` }); } - }, [value, router]) + }, [value, router]); return ( -
-
+
+

Recherche

-
-
+
+
( + renderInput={({ className, id, placeholder, type }) => ( { - setValue(newValue) + onChange={(newValue) => { + setValue(newValue); }} - onInputChange={newValue => { - setInputValue(newValue) + onInputChange={(newValue) => { + setInputValue(newValue); }} /> )} @@ -62,9 +61,7 @@ const Communes = () => {
+ ); +}; - ) -} - -export default Communes - +export default Communes; diff --git a/yarn.lock b/yarn.lock index 5ab812e..487f681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2015,6 +2015,11 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +fuse.js@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111" + integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA== + get-intrinsic@^1.0.2: version "1.1.2" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"