Api do wyszukiwania dostepności, korekty w powiązanych stronach
This commit is contained in:
33
src/islands/MapRangeSwitch.jsx
Normal file
33
src/islands/MapRangeSwitch.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import "../styles/offers/offers-switches.css";
|
||||
|
||||
export default function MapRangeSwitch() {
|
||||
const [selected, setSelected] = useState("none");
|
||||
|
||||
const options = [
|
||||
{ id: "swiatlowodowy", nazwa: "Światłowodowy" },
|
||||
{ id: "radiowy", nazwa: "Radiowy" },
|
||||
{ id: "none", nazwa: "Ukryj" }
|
||||
];
|
||||
|
||||
const update = (id) => {
|
||||
setSelected(id);
|
||||
window?.handleMapSwitch?.(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="map-range-switch fuz-switches-wrapper">
|
||||
<div class="fuz-switch-group flex sm:flex-row flex-col w-full">
|
||||
{options.map((op) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`fuz-switch ${selected === op.id ? "active" : ""} w-full sm:w-auto`}
|
||||
onClick={() => update(op.id)}
|
||||
>
|
||||
{op.nazwa}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export default function OffersExtraServices({
|
||||
|
||||
return (
|
||||
<div class="fuz-extra-services">
|
||||
<h3 class="fuz-title-small">Usługi dodatkowe</h3>
|
||||
<h1 class="fuz-title-small">Usługi dodatkowe</h1>
|
||||
|
||||
<div class="fuz-table-wrapper">
|
||||
<table class="fuz-table">
|
||||
|
||||
350
src/islands/RangeForm.jsx
Normal file
350
src/islands/RangeForm.jsx
Normal file
@@ -0,0 +1,350 @@
|
||||
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;
|
||||
|
||||
// =====================================================
|
||||
// AUTOCOMPLETE MIASTO
|
||||
// =====================================================
|
||||
async function fetchCitySuggestions(q) {
|
||||
const res = await fetch(`/api/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]);
|
||||
|
||||
// =====================================================
|
||||
// AUTO-MATCH MIASTO — NOWE!
|
||||
// =====================================================
|
||||
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]);
|
||||
|
||||
// =====================================================
|
||||
// DOSTĘPNOŚĆ ULIC
|
||||
// =====================================================
|
||||
async function updateStreetAvailability(currentCity) {
|
||||
if (!currentCity) {
|
||||
setStreetDisabled(true);
|
||||
setStreetRequired(false);
|
||||
setStreet("");
|
||||
setStreetSuggest([]);
|
||||
setStreetHighlightIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/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]);
|
||||
|
||||
// =====================================================
|
||||
// AUTOCOMPLETE ULICA
|
||||
// =====================================================
|
||||
async function fetchStreetSuggestions(q, c) {
|
||||
const res = await fetch(
|
||||
`/api/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]);
|
||||
|
||||
// =====================================================
|
||||
// AUTO-MATCH ULICA — NOWE!
|
||||
// =====================================================
|
||||
useEffect(() => {
|
||||
if (!streetSuggest.length) return;
|
||||
|
||||
const exact = streetSuggest.find(
|
||||
(s) => s.toLowerCase() === street.toLowerCase()
|
||||
);
|
||||
|
||||
if (exact) {
|
||||
setStreet(exact);
|
||||
setStreetSuggest([]);
|
||||
setStreetHighlightIndex(-1);
|
||||
}
|
||||
}, [street, streetSuggest]);
|
||||
|
||||
// =====================================================
|
||||
// SUBMIT
|
||||
// =====================================================
|
||||
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/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);
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// RENDER
|
||||
// =====================================================
|
||||
return (
|
||||
<form class="flex flex-col gap-2" onSubmit={onSubmit} autocomplete="off">
|
||||
|
||||
|
||||
{/* CITY */}
|
||||
<div class="autocomplete-wrapper">
|
||||
<input
|
||||
class={`fuz-input w-full ${citySuggest.length ? "autocomplete-open" : ""}`}
|
||||
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={`fuz-input w-full ${
|
||||
streetSuggest.length ? "autocomplete-open" : ""
|
||||
} ${streetDisabled ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
placeholder={
|
||||
streetDisabled
|
||||
? "W tej miejscowości nie występują"
|
||||
: "Wpisz ulicę..."
|
||||
}
|
||||
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="fuz-input"
|
||||
placeholder="np. 1A"
|
||||
value={number}
|
||||
onInput={(e) => setNumber(e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* ERROR */}
|
||||
{error && (
|
||||
<p class="text-red-600 dark:text-red-400 text-sm">{error}</p>
|
||||
)}
|
||||
|
||||
{/* BUTTON */}
|
||||
<button
|
||||
class="btn btn-outline w-full py-3 flex items-center justify-center gap-2"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<span class="w-5 h-5 border-2 border-[var(--fuz-accent)] border-t-transparent rounded-full animate-spin"></span>
|
||||
) : (
|
||||
"Sprawdź dostępność →"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user