Skip to content

Commit

Permalink
Feat/address autocomplete (#44)
Browse files Browse the repository at this point in the history
* feat: add address autocompletion

* feat: add loading badge and fix some styles

* fix(address): allow user to use a custom value
  • Loading branch information
ledouxm authored Dec 17, 2024
1 parent 8c8131c commit 0b8f5ff
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 16 deletions.
139 changes: 139 additions & 0 deletions packages/frontend/src/components/SmartAddressInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { css } from "#styled-system/css";
import { Flex, Stack, styled } from "#styled-system/jsx";
import { fr } from "@codegouvfr/react-dsfr";
import Badge from "@codegouvfr/react-dsfr/Badge";
import Input from "@codegouvfr/react-dsfr/Input";
import { useQuery } from "@tanstack/react-query";
import { useRef, useState } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useDebounce } from "react-use";
import { Report } from "../db/AppSchema";
import { AddressResult, searchAddress } from "../features/address";
import { useIsFormDisabled } from "../features/DisabledContext";
import { Combobox } from "./Combobox";

export const SmartAddressInput = () => {
const form = useFormContext<Report>();
const isFormDisabled = useIsFormDisabled();

const [isFrozen, setIsFrozen] = useState(true);

const applicantAddress = useWatch({ control: form.control, name: "applicantAddress" });
const prevValueRef = useRef(applicantAddress);

const [debouncedAddress, setDebouncedAddress] = useState(applicantAddress);

useDebounce(() => setDebouncedAddress(applicantAddress), 500, [applicantAddress]);

const isEnabled = !isFormDisabled && debouncedAddress && debouncedAddress.length > 4 && !isFrozen;

const addressQuery = useQuery({
queryKey: ["address", debouncedAddress],
queryFn: () => searchAddress(debouncedAddress!),
enabled: !!isEnabled,
});

const isLoading = addressQuery.isLoading && isEnabled;
const suggestions = addressQuery.data;

return (
<Stack mb="28px">
<Combobox.Root
disabled={isFormDisabled}
itemToString={(item) => (item as AddressResult).address ?? ""}
itemToValue={(item) => (item as AddressResult).label ?? ""}
items={suggestions ?? []}
value={applicantAddress ? [applicantAddress.toString()] : undefined}
inputValue={applicantAddress ?? ""}
onBlur={() => {
if (prevValueRef.current) {
form.setValue("applicantAddress", prevValueRef.current);
}
}}
onInputValueChange={(e) => {
prevValueRef.current = applicantAddress;
form.setValue("applicantAddress", e.value);
setIsFrozen(false);
}}
onValueChange={(e) => {
if (e.items?.length === 0) return;
prevValueRef.current = null;
form.setValue("applicantAddress", (e.items?.[0] as AddressResult)?.address ?? "");
form.setValue("zipCode", (e.items?.[0] as AddressResult)?.zipCode ?? "");
form.setValue("city", (e.items?.[0] as AddressResult)?.city ?? "");
setIsFrozen(true);
}}
>
<Combobox.Control>
<Combobox.Input asChild placeholder="">
<ProxyInput isLoading={isLoading} disabled={isFormDisabled} />
</Combobox.Input>
{/* <Combobox.Trigger asChild top="unset !important" bottom="28px">
<Button iconId="ri-arrow-down-line" aria-label="open" priority="tertiary no outline" size="small"></Button>
</Combobox.Trigger> */}
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content maxH="400px" bgColor="background-contrast-grey" overflow="auto">
<Combobox.ItemGroup id="service-instructeur">
{suggestions?.length
? suggestions.map((item: AddressResult) => (
<Combobox.Item key={item.label} item={item}>
<Combobox.ItemText>{item.label}</Combobox.ItemText>
</Combobox.Item>
))
: null}
</Combobox.ItemGroup>
</Combobox.Content>
</Combobox.Positioner>
</Combobox.Root>
{isLoading ? (
<styled.div hideFrom="lg" mt="8px">
<LoadingBadge />
</styled.div>
) : null}
</Stack>
);
};

const ProxyInput = ({ disabled, isLoading, ...props }: any) => {
return (
<Input
label={
<Flex flexDir="row" alignItems="center">
<styled.span mr="12px">Adresse (numéro, voie)</styled.span>
{isLoading ? (
<styled.div hideBelow="lg">
<LoadingBadge />
</styled.div>
) : null}
</Flex>
}
disabled={disabled}
nativeInputProps={{ ...props, autoComplete: "new-password" }}
/>
);
};

const LoadingBadge = () => {
return (
<Badge
className={css({
display: "flex",
flexDir: "row",
alignItems: "center",
fontWeight: "normal",
})}
severity="info"
noIcon
>
<styled.i
className={fr.cx("fr-icon-refresh-line", "fr-icon--sm")}
_before={{
verticalAlign: "middle",
mr: "4px",
}}
></styled.i>
Recherche en cours
</Badge>
);
};
6 changes: 4 additions & 2 deletions packages/frontend/src/features/InfoForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Report } from "../db/AppSchema";
import { db, useDbQuery } from "../db/db";
import { useIsFormDisabled } from "./DisabledContext";
import { ServiceInstructeurSelect } from "./ServiceInstructeurSelect";
import { SmartAddressInput } from "#components/SmartAddressInput.tsx";

export const InfoForm = () => {
const form = useFormContext<Report>();
Expand Down Expand Up @@ -150,12 +151,13 @@ export const InfoForm = () => {
nativeTextAreaProps={{ ...form.register("projectDescription"), rows: 5 }}
/>

<Input
<SmartAddressInput />
{/* <Input
className={css({ flex: { base: "none", lg: 2 }, mt: "16px", mb: { base: "24px", lg: undefined } })}
disabled={isFormDisabled}
label="Adresse"
nativeInputProps={form.register("applicantAddress")}
/>
/> */}
<Stack gap={{ base: "0", lg: "16px" }} direction={{ base: "column", lg: "row" }}>
<Input
className={css({ flex: { base: "none", lg: 1 }, mb: { base: "24px", lg: undefined } })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const ServiceInstructeurSelect = ({ disabled }: { disabled?: boolean }) =
</Combobox.Trigger>
</Combobox.Control>
<Combobox.Positioner>
<Combobox.Content maxH="400px" overflow="auto">
<Combobox.Content maxH="400px" mt="-1.5rem" bgColor="background-contrast-grey" overflow="auto">
<Combobox.ItemGroup id="service-instructeur">
{items?.length ? (
items.map((item) => (
Expand Down
48 changes: 48 additions & 0 deletions packages/frontend/src/features/address.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export const searchAddress = async (query: string) => {
const response = await fetch(`https://api-adresse.data.gouv.fr/search/?q=${query}&limit=10`);
const data = await response.json();

return data.features.map((feature: AddressFeature, index: number) => ({
index,
address: feature.properties.name,
zipCode: feature.properties.postcode,
city: feature.properties.city,
label: feature.properties.label,
}));
};

export type AddressResult = {
address: string;
zipCode: string;
city: string;
label: string;
index: number;
};

export interface AddressFeature {
type: string;
geometry: Geometry;
properties: Properties;
}

export interface Properties {
label: string;
score: number;
type: string;
importance: number;
id: string;
banId: string;
name: string;
postcode: string;
citycode: string;
x: number;
y: number;
city: string;
context: string;
locality: string;
}

export interface Geometry {
type: string;
coordinates: number[];
}
10 changes: 0 additions & 10 deletions packages/frontend/src/features/testPowersync.tsx

This file was deleted.

3 changes: 0 additions & 3 deletions packages/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { registerSW } from "virtual:pwa-register";
import { initFonts } from "@cr-vif/pdf";
import { powerSyncDb, setupPowersync } from "./db/db";
import { PowerSyncContext } from "@powersync/react";
import { TestPowersync } from "./features/testPowersync";

if ("serviceWorker" in navigator) {
registerSW({});
Expand Down Expand Up @@ -41,11 +40,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ErrorBoundary fallback={<div>Une erreur s'est produite</div>}>
<QueryClientProvider client={queryClient}>
{/* <TestPowersync /> */}
<AuthProvider>
<WithPowersync>
<App />
<TestPowersync />
</WithPowersync>
</AuthProvider>
</QueryClientProvider>
Expand Down

0 comments on commit 0b8f5ff

Please sign in to comment.