Api do wyszukiwania dostepności, korekty w powiązanych stronach

This commit is contained in:
dm
2025-11-24 08:40:05 +01:00
parent 20ef0d5293
commit e8881dd23b
22 changed files with 15600 additions and 225 deletions

View 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>
);
}

View File

@@ -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
View 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>
);
}