119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
import React from 'react';
|
||
import { QueryClientProvider } from 'react-query';
|
||
import { matchSorter } from 'match-sorter';
|
||
|
||
import ComboSearch, { ComboSearchProps } from './ComboSearch';
|
||
import { queryClient } from './shared/queryClient';
|
||
import { ComboDepartementsSearch } from './ComboDepartementsSearch';
|
||
import { useHiddenField, groupId } from './shared/hooks';
|
||
|
||
type CommuneResult = { code: string; nom: string; codesPostaux: string[] };
|
||
|
||
// Avoid hiding similar matches for precise queries (like "Sainte Marie")
|
||
function searchResultsLimit(term: string) {
|
||
return term.length > 5 ? 10 : 5;
|
||
}
|
||
|
||
function expandResultsWithMultiplePostalCodes(term: string, result: unknown) {
|
||
const results = result as CommuneResult[];
|
||
// A single result may have several associated postal codes.
|
||
// To make the search results more precise, we want to generate
|
||
// an actual result for each postal code.
|
||
const expandedResults = results.flatMap((result) =>
|
||
result.codesPostaux.map((codePostal) => ({
|
||
...result,
|
||
codesPostaux: [codePostal]
|
||
}))
|
||
);
|
||
|
||
// Some very large cities (like Paris) have A LOT of associated postal codes.
|
||
// As we generated one result per postal code, we now have a lot of results
|
||
// for the same city. If the number of results is above the threshold, we use
|
||
// local search to narrow the results.
|
||
const limit = searchResultsLimit(term);
|
||
if (expandedResults.length > limit) {
|
||
return matchSorter(expandedResults, term, {
|
||
keys: [(item) => `${item.nom} (${item.codesPostaux[0]})`, 'code'],
|
||
sorter: (rankedItems) => rankedItems
|
||
}).slice(0, limit + 1);
|
||
}
|
||
|
||
return expandedResults;
|
||
}
|
||
|
||
const placeholderDepartements = [
|
||
['63 – Puy-de-Dôme', 'Clermont-Ferrand'],
|
||
['77 – Seine-et-Marne', 'Melun'],
|
||
['22 – Côtes d’Armor', 'Saint-Brieuc'],
|
||
['47 – Lot-et-Garonne', 'Agen']
|
||
] as const;
|
||
const [placeholderDepartement, placeholderCommune] =
|
||
placeholderDepartements[
|
||
Math.floor(Math.random() * (placeholderDepartements.length - 1))
|
||
];
|
||
|
||
export default function ComboCommunesSearch({
|
||
id,
|
||
...props
|
||
}: ComboSearchProps<CommuneResult> & { id: string }) {
|
||
const group = groupId(id);
|
||
const [departementValue, setDepartementValue] = useHiddenField(
|
||
group,
|
||
'departement'
|
||
);
|
||
const [codeDepartement, setCodeDepartement] = useHiddenField(
|
||
group,
|
||
'code_departement'
|
||
);
|
||
const departementDescribedBy = `${id}_departement_notice`;
|
||
const communeDescribedBy = `${id}_commune_notice`;
|
||
|
||
return (
|
||
<QueryClientProvider client={queryClient}>
|
||
<div>
|
||
<div className="notice" id={departementDescribedBy}>
|
||
<p>
|
||
Choisissez le département dans lequel se situe la commune. Vous
|
||
pouvez entrer le nom ou le code.
|
||
</p>
|
||
</div>
|
||
<ComboDepartementsSearch
|
||
{...props}
|
||
id={!codeDepartement ? id : undefined}
|
||
describedby={departementDescribedBy}
|
||
placeholder={placeholderDepartement}
|
||
addForeignDepartement={false}
|
||
value={departementValue}
|
||
onChange={(_, result) => {
|
||
setDepartementValue(result?.nom ?? '');
|
||
setCodeDepartement(result?.code ?? '');
|
||
}}
|
||
/>
|
||
</div>
|
||
{codeDepartement ? (
|
||
<div>
|
||
<div className="notice" id={communeDescribedBy}>
|
||
<p>
|
||
Choisissez la commune. Vous pouvez entrer le nom ou le code
|
||
postal.
|
||
</p>
|
||
</div>
|
||
<ComboSearch
|
||
{...props}
|
||
id={id}
|
||
describedby={communeDescribedBy}
|
||
placeholder={placeholderCommune}
|
||
scope="communes"
|
||
scopeExtra={codeDepartement}
|
||
minimumInputLength={2}
|
||
transformResult={({ code, nom, codesPostaux }) => [
|
||
code,
|
||
`${nom} (${codesPostaux[0]})`
|
||
]}
|
||
transformResults={expandResultsWithMultiplePostalCodes}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
</QueryClientProvider>
|
||
);
|
||
}
|