feat: implement geocoding search with unique city filtering and improved UI

This commit is contained in:
Marco Pedone 2025-10-14 16:42:19 +02:00
parent 0512122243
commit cdcc92173c

View file

@ -1,13 +1,142 @@
import type { NextPage } from "next"; import type { NextPage } from "next";
import toast from "react-hot-toast"; import { useState } from "react";
import { useDebounce } from "react-use";
import z from "zod";
import Input from "~/components/custom_ui/input";
import { LoadingPage } from "~/components/loading";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
type NominatimResponse = z.infer<typeof responseSchema>;
export const responseSchema = z.object({
type: z.string(),
geocoding: z.object({
version: z.string(),
attribution: z.string(),
licence: z.string(),
query: z.string(),
}),
features: z.array(
z.object({
type: z.string(),
properties: z.object({
geocoding: z.object({
place_id: z.number(),
osm_type: z.string(),
osm_id: z.number(),
osm_key: z.string(),
osm_value: z.string(),
type: z.string(),
label: z.string(),
name: z.string(),
postcode: z.string().optional(),
county: z.string().optional(),
state: z.string().optional(),
country: z.string(),
country_code: z.string(),
admin: z.any(),
district: z.string().optional(),
city: z.string().optional(),
}),
}),
geometry: z.object({
type: z.string(),
coordinates: z.array(z.number()),
}),
}),
),
});
const Test: NextPage = () => { const Test: NextPage = () => {
const [value, setValue] = useState("");
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState<NominatimResponse["features"]>([]);
useDebounce(
() => {
if (value !== "") {
handleFetch();
}
},
1000,
[value],
);
const handleFetch = () => {
setLoading(true);
const api = `https://nominatim.openstreetmap.org/search?format=geocodejson&limit=5&addressdetails=1&q=${encodeURI(
value,
)}`;
fetch(api)
.then((response) => response.json())
.then((data) => {
const parsed = responseSchema.safeParse(data);
if (parsed.success) {
const filtered = parsed.data.features.filter(
(f) =>
f.properties.geocoding.osm_type === "way" &&
f.properties.geocoding.type === "street" &&
f.properties.geocoding.postcode,
);
// Then deduplicate by city
const uniqueResults = filtered.reduce(
(acc, current) => {
const city = current.properties.geocoding.city;
// If this city doesn't exist or we haven't seen it yet
if (
!city ||
!acc.some((item) => item.properties.geocoding.city === city)
) {
acc.push(current);
}
return acc;
},
[] as typeof filtered,
);
setOptions(uniqueResults);
} else {
console.error("Failed to fetch location data", parsed.error);
}
setLoading(false);
})
.catch((error) => {
console.error(error);
});
};
return ( return (
<div className="flex gap-4 p-4"> <div className="flex flex-col gap-2 p-4">
<Button onClick={() => toast.success("asdasdasdasdasda")} type="button"> <Input
test onChange={({ currentTarget }) => {
setValue(currentTarget.value);
}}
value={value}
/>
{isLoading ? (
<LoadingPage />
) : (
options.map((option, idx) => (
<Button
className="w-full justify-start truncate"
key={idx}
onClick={() => {
console.log(option.properties.geocoding);
}}
variant="outline"
>
<p>
{option.properties.geocoding.name} -{" "}
{option.properties.geocoding.city} -{" "}
{option.properties.geocoding.postcode} -{" "}
{option.properties.geocoding.county} -{" "}
{option.properties.geocoding.country}{" "}
{option.properties.geocoding.country_code}
</p>
</Button> </Button>
))
)}
</div> </div>
); );
}; };