329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
import { useState, useEffect, useRef } from "preact/hooks";
|
|
|
|
export default function RangeForm() {
|
|
const [city, setCity] = useState("");
|
|
const [citySuggest, setCitySuggest] = useState([]);
|
|
const [highlightIndex, setHighlightIndex] = useState(-1);
|
|
|
|
const [street, setStreet] = useState("");
|
|
const [streetSuggest, setStreetSuggest] = useState([]);
|
|
const [streetHighlightIndex, setStreetHighlightIndex] = useState(-1);
|
|
|
|
const [streetDisabled, setStreetDisabled] = useState(true);
|
|
const [streetRequired, setStreetRequired] = useState(false);
|
|
|
|
const [number, setNumber] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const cityListRef = useRef(null);
|
|
const streetListRef = useRef(null);
|
|
|
|
let timeoutCity = null;
|
|
let timeoutStreet = null;
|
|
|
|
async function fetchCitySuggestions(q) {
|
|
const res = await fetch(`/api/range/cities-autocomplete?q=${encodeURIComponent(q)}`);
|
|
setCitySuggest(await res.json());
|
|
setHighlightIndex(-1);
|
|
}
|
|
|
|
function onCityInput(e) {
|
|
const value = e.target.value;
|
|
setCity(value);
|
|
setError("");
|
|
|
|
if (timeoutCity) clearTimeout(timeoutCity);
|
|
|
|
if (value.trim().length < 2) {
|
|
setCitySuggest([]);
|
|
return;
|
|
}
|
|
|
|
timeoutCity = setTimeout(() => fetchCitySuggestions(value), 250);
|
|
}
|
|
|
|
function onCityKeyDown(e) {
|
|
if (!citySuggest.length) return;
|
|
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
setHighlightIndex((prev) => (prev + 1) % citySuggest.length);
|
|
}
|
|
|
|
if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
setHighlightIndex((prev) =>
|
|
prev <= 0 ? citySuggest.length - 1 : prev - 1
|
|
);
|
|
}
|
|
|
|
if (e.key === "Enter" && highlightIndex >= 0) {
|
|
e.preventDefault();
|
|
const chosen = citySuggest[highlightIndex];
|
|
setCity(chosen);
|
|
setCitySuggest([]);
|
|
setHighlightIndex(-1);
|
|
}
|
|
|
|
if (e.key === "Escape") {
|
|
setCitySuggest([]);
|
|
setHighlightIndex(-1);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (highlightIndex < 0 || !cityListRef.current) return;
|
|
cityListRef.current.children[highlightIndex]?.scrollIntoView({ block: "nearest" });
|
|
}, [highlightIndex]);
|
|
|
|
useEffect(() => {
|
|
if (!citySuggest.length) return;
|
|
|
|
const exact = citySuggest.find(
|
|
(c) => c.toLowerCase() === city.toLowerCase()
|
|
);
|
|
|
|
if (exact) {
|
|
setCity(exact);
|
|
setCitySuggest([]);
|
|
setHighlightIndex(-1);
|
|
updateStreetAvailability(exact);
|
|
}
|
|
}, [city, citySuggest]);
|
|
|
|
async function updateStreetAvailability(currentCity) {
|
|
if (!currentCity) {
|
|
setStreetDisabled(true);
|
|
setStreetRequired(false);
|
|
setStreet("");
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
return;
|
|
}
|
|
|
|
const res = await fetch(`/api/range/has-streets?city=${encodeURIComponent(currentCity)}`);
|
|
const { hasStreets } = await res.json();
|
|
|
|
if (!hasStreets) {
|
|
setStreetDisabled(true);
|
|
setStreetRequired(false);
|
|
setStreet("");
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
return;
|
|
}
|
|
|
|
setStreetDisabled(false);
|
|
setStreetRequired(true);
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
}
|
|
|
|
useEffect(() => {
|
|
updateStreetAvailability(city);
|
|
}, [city]);
|
|
|
|
async function fetchStreetSuggestions(q, c) {
|
|
const res = await fetch(
|
|
`/api/range/streets-autocomplete?city=${encodeURIComponent(c)}&q=${encodeURIComponent(q)}`
|
|
);
|
|
setStreetSuggest(await res.json());
|
|
setStreetHighlightIndex(-1);
|
|
}
|
|
|
|
function onStreetInput(e) {
|
|
const value = e.target.value;
|
|
setStreet(value);
|
|
setError("");
|
|
|
|
if (!city || streetDisabled) return;
|
|
|
|
if (!value) {
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
return;
|
|
}
|
|
|
|
if (timeoutStreet) clearTimeout(timeoutStreet);
|
|
timeoutStreet = setTimeout(() => fetchStreetSuggestions(value, city), 250);
|
|
}
|
|
|
|
function onStreetKeyDown(e) {
|
|
if (!streetSuggest.length) return;
|
|
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
setStreetHighlightIndex((prev) => (prev + 1) % streetSuggest.length);
|
|
}
|
|
|
|
if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
setStreetHighlightIndex((prev) =>
|
|
prev <= 0 ? streetSuggest.length - 1 : prev - 1
|
|
);
|
|
}
|
|
|
|
if (e.key === "Enter" && streetHighlightIndex >= 0) {
|
|
e.preventDefault();
|
|
setStreet(streetSuggest[streetHighlightIndex]);
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
}
|
|
|
|
if (e.key === "Escape") {
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (streetHighlightIndex < 0 || !streetListRef.current) return;
|
|
streetListRef.current.children[streetHighlightIndex]?.scrollIntoView({ block: "nearest" });
|
|
}, [streetHighlightIndex]);
|
|
|
|
useEffect(() => {
|
|
if (!streetSuggest.length) return;
|
|
|
|
const exact = streetSuggest.find(
|
|
(s) => s.toLowerCase() === street.toLowerCase()
|
|
);
|
|
|
|
if (exact) {
|
|
setStreet(exact);
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
}
|
|
}, [street, streetSuggest]);
|
|
|
|
async function onSubmit(e) {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
if (streetRequired && !street.trim()) {
|
|
setError("Ulica jest wymagana w tej miejscowości.");
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
const res = await fetch("/api/range/search", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ city, street, number }),
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (!data.ok || !data.result) {
|
|
setError("Adres nie znajduje się w zasięgu.");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
window.showAddressOnMap(data.result);
|
|
|
|
setCity("");
|
|
setStreet("");
|
|
setNumber("");
|
|
updateStreetAvailability(city);
|
|
setLoading(false);
|
|
}
|
|
|
|
return (
|
|
<form class="flex flex-col gap-2" onSubmit={onSubmit} autocomplete="off">
|
|
{/* CITY */}
|
|
<div class="autocomplete-wrapper">
|
|
<input
|
|
class={`f-input ${citySuggest.length ? "autocomplete-open" : ""}`}
|
|
name="formCity"
|
|
placeholder="Wpisz miejscowość..."
|
|
value={city}
|
|
onInput={onCityInput}
|
|
onKeyDown={onCityKeyDown}
|
|
required
|
|
/>
|
|
|
|
{citySuggest.length > 0 && (
|
|
<ul class="autocomplete-list" ref={cityListRef}>
|
|
{citySuggest.map((item, idx) => (
|
|
<li
|
|
class={idx === highlightIndex ? "active" : ""}
|
|
onPointerDown={() => {
|
|
setCity(item);
|
|
setCitySuggest([]);
|
|
setHighlightIndex(-1);
|
|
}}
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
{/* STREET */}
|
|
<div class="autocomplete-wrapper">
|
|
<input
|
|
class={`f-input ${
|
|
streetSuggest.length ? "autocomplete-open" : ""
|
|
} ${streetDisabled ? "opacity-50 cursor-not-allowed" : ""}`}
|
|
placeholder={
|
|
streetDisabled
|
|
? "W tej miejscowości nie występują"
|
|
: "Wpisz ulicę..."
|
|
}
|
|
name="formStreet"
|
|
value={street}
|
|
readOnly={streetDisabled}
|
|
onInput={streetDisabled ? undefined : onStreetInput}
|
|
onKeyDown={streetDisabled ? undefined : onStreetKeyDown}
|
|
required={streetRequired}
|
|
/>
|
|
|
|
{streetSuggest.length > 0 && (
|
|
<ul class="autocomplete-list" ref={streetListRef}>
|
|
{streetSuggest.map((item, idx) => (
|
|
<li
|
|
class={idx === streetHighlightIndex ? "active" : ""}
|
|
onPointerDown={() => {
|
|
setStreet(item);
|
|
setStreetSuggest([]);
|
|
setStreetHighlightIndex(-1);
|
|
}}
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
{/* NUMBER */}
|
|
<input
|
|
class="f-input"
|
|
name="formNumber"
|
|
placeholder="np. 1A"
|
|
value={number}
|
|
onInput={(e) => setNumber(e.target.value)}
|
|
required
|
|
/>
|
|
|
|
{error && (
|
|
<p class="text-red-600text-lg">{error}</p>
|
|
)}
|
|
|
|
<button
|
|
class="btn btn-primary"
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<span class="w-5 h-5 border-2 border-[var(--f-text)] border-t-transparent rounded-full animate-spin"></span>
|
|
) : (
|
|
"Sprawdź dostępność →"
|
|
)}
|
|
</button>
|
|
</form>
|
|
);
|
|
}
|