"use client"; import "leaflet/dist/leaflet.css"; import Link from "next/link"; import { useEffect, useMemo, useRef, useState } from "react"; import { useI18n } from "../components/I18nProvider"; type ListingResult = { id: string; title: string; slug: string; teaser: string | null; locale: string | null; country: string; region: string; city: string; streetAddress: string | null; addressNote: string | null; latitude: number | null; longitude: number | null; hasSauna: boolean; hasFireplace: boolean; hasWifi: boolean; petsAllowed: boolean; byTheLake: boolean; hasAirConditioning: boolean; hasKitchen: boolean; hasDishwasher: boolean; hasWashingMachine: boolean; hasBarbecue: boolean; hasMicrowave: boolean; hasFreeParking: boolean; evChargingAvailable: boolean; evChargingOnSite: boolean; wheelchairAccessible: boolean; hasSkiPass: boolean; maxGuests: number; bedrooms: number; beds: number; bathrooms: number; priceWeekdayEuros: number | null; priceWeekendEuros: number | null; coverImage: string | null; isSample: boolean; hasCalendar: boolean; availableForDates?: boolean; }; type LatLng = { lat: number; lon: number }; function haversineKm(a: LatLng, b: LatLng) { const toRad = (v: number) => (v * Math.PI) / 180; const R = 6371; const dLat = toRad(b.lat - a.lat); const dLon = toRad(b.lon - a.lon); const lat1 = toRad(a.lat); const lat2 = toRad(b.lat); const sinLat = Math.sin(dLat / 2); const sinLon = Math.sin(dLon / 2); const h = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLon * sinLon; return 2 * R * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); } type LeafletLib = typeof import("leaflet"); async function loadLeaflet(): Promise { if (typeof window === "undefined") return Promise.reject(new Error("No window")); if ((window as any).L) return (window as any).L as LeafletLib; const linkId = "leaflet-css"; if (!document.getElementById(linkId)) { const link = document.createElement("link"); link.id = linkId; link.rel = "stylesheet"; link.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"; document.head.appendChild(link); } const mod: LeafletLib = await import("leaflet"); (window as any).L = mod; return mod; } const amenityIcons: Record = { sauna: "๐Ÿง–", fireplace: "๐Ÿ”ฅ", wifi: "๐Ÿ“ถ", pets: "๐Ÿพ", lake: "๐ŸŒŠ", ac: "โ„๏ธ", kitchen: "๐Ÿฝ๏ธ", dishwasher: "๐Ÿงผ", washer: "๐Ÿงบ", barbecue: "๐Ÿ–", microwave: "๐Ÿฒ", parking: "๐Ÿ…ฟ๏ธ", accessible: "โ™ฟ", ski: "โ›ท๏ธ", ev: "โšก", evOnSite: "๐Ÿ”Œ", }; function ListingsMap({ listings, center, selectedId, onSelect, loadingText, }: { listings: ListingResult[]; center: LatLng | null; selectedId: string | null; onSelect: (id: string) => void; loadingText: string; }) { const mapContainerRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef([]); const [ready, setReady] = useState(false); const [mapError, setMapError] = useState(null); useEffect(() => { let cancelled = false; loadLeaflet() .then((L) => { if (cancelled) return; setReady(true); setMapError(null); if (!mapContainerRef.current) return; if (!mapRef.current) { mapRef.current = L.map(mapContainerRef.current).setView( [64.5, 26], 5, ); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors", maxZoom: 18, }).addTo(mapRef.current); } markersRef.current.forEach((m) => m.remove()); markersRef.current = []; listings .filter((l) => l.latitude !== null && l.longitude !== null) .forEach((l) => { const marker = L.marker([l.latitude!, l.longitude!], { title: l.title, }); marker.addTo(mapRef.current); marker.on("click", () => onSelect(l.id)); markersRef.current.push(marker); }); const withCoords = listings.filter( (l) => l.latitude !== null && l.longitude !== null, ); if (center && mapRef.current) { mapRef.current.setView([center.lat, center.lon], 8); } else if (withCoords.length && mapRef.current) { const group = L.featureGroup( withCoords.map((l) => L.marker([l.latitude as number, l.longitude as number]), ), ); mapRef.current.fitBounds(group.getBounds().pad(0.25)); } }) .catch((err) => { console.error("Leaflet load failed", err); setReady(false); setMapError("Map could not be loaded right now."); }); return () => { cancelled = true; }; }, [listings, center, onSelect]); useEffect(() => { if (!mapRef.current || !selectedId) return; const listing = listings.find((l) => l.id === selectedId); if (listing && listing.latitude && listing.longitude) { mapRef.current.setView([listing.latitude, listing.longitude], 10); } }, [selectedId, listings]); return (
{!ready ?
{loadingText}
: null} {mapError ?
{mapError}
: null}
); } export default function ListingsIndexPage() { const { t } = useI18n(); const [query, setQuery] = useState(""); const [city, setCity] = useState(""); const [region, setRegion] = useState(""); const [listings, setListings] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedId, setSelectedId] = useState(null); const [addressQuery, setAddressQuery] = useState(""); const [addressCenter, setAddressCenter] = useState(null); const [radiusKm, setRadiusKm] = useState(50); const [geocoding, setGeocoding] = useState(false); const [geoError, setGeoError] = useState(null); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [amenities, setAmenities] = useState([]); const scrollRef = useRef(null); const filteredByAddress = useMemo(() => { if (!addressCenter) return listings; return listings.filter((l) => { if (l.latitude === null || l.longitude === null) return false; const d = haversineKm(addressCenter, { lat: l.latitude, lon: l.longitude, }); return d <= radiusKm; }); }, [listings, addressCenter, radiusKm]); const filtered = filteredByAddress; const amenityOptions = [ { key: "sauna", label: t("amenitySauna"), icon: amenityIcons.sauna }, { key: "fireplace", label: t("amenityFireplace"), icon: amenityIcons.fireplace, }, { key: "wifi", label: t("amenityWifi"), icon: amenityIcons.wifi }, { key: "pets", label: t("amenityPets"), icon: amenityIcons.pets }, { key: "lake", label: t("amenityLake"), icon: amenityIcons.lake }, { key: "ac", label: t("amenityAirConditioning"), icon: amenityIcons.ac }, { key: "kitchen", label: t("amenityKitchen"), icon: amenityIcons.kitchen }, { key: "dishwasher", label: t("amenityDishwasher"), icon: amenityIcons.dishwasher, }, { key: "washer", label: t("amenityWashingMachine"), icon: amenityIcons.washer, }, { key: "barbecue", label: t("amenityBarbecue"), icon: amenityIcons.barbecue, }, { key: "microwave", label: t("amenityMicrowave"), icon: amenityIcons.microwave, }, { key: "parking", label: t("amenityFreeParking"), icon: amenityIcons.parking, }, { key: "accessible", label: t("amenityWheelchairAccessible"), icon: amenityIcons.accessible, }, { key: "skipass", label: t("amenitySkiPass"), icon: amenityIcons.ski }, { key: "ev", label: t("amenityEvNearby"), icon: amenityIcons.ev }, { key: "ev-onsite", label: t("amenityEvOnSite"), icon: amenityIcons.evOnSite, }, ]; async function fetchListings() { setLoading(true); setError(null); try { const params = new URLSearchParams(); if (query) params.set("q", query); if (city) params.set("city", city); if (region) params.set("region", region); if (startDate) params.set("availableStart", startDate); if (endDate) params.set("availableEnd", endDate); const evSelected = amenities.includes("ev"); if (evSelected) params.set("evCharging", "true"); amenities .filter((a) => a !== "ev") .forEach((a) => params.append("amenity", a)); const res = await fetch(`/api/listings?${params.toString()}`, { cache: "no-store", }); const data = await res.json(); if (!res.ok || data.error) { throw new Error(data.error || "Failed to load listings"); } setListings(data.listings ?? []); setSelectedId(data.listings?.[0]?.id ?? null); } catch (e: any) { setError(e.message || "Failed to load listings"); } finally { setLoading(false); } } function toggleAmenity(key: string) { setAmenities((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key], ); } async function locateAddress() { if (!addressQuery.trim()) return; setGeocoding(true); setGeoError(null); try { const res = await fetch( `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(addressQuery)}&limit=1`, ); const data = await res.json(); if (Array.isArray(data) && data.length > 0) { const hit = data[0]; setAddressCenter({ lat: parseFloat(hit.lat), lon: parseFloat(hit.lon), }); } else { setGeoError(t("addressNotFound")); } } catch (e) { setGeoError(t("addressLookupFailed")); } finally { setGeocoding(false); } } useEffect(() => { fetchListings(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const countLabel = t("listingsFound", { count: filtered.length }); useEffect(() => { if (!selectedId) return; const el = document.querySelector( `[data-listing-id="${selectedId}"]`, ); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); } }, [selectedId]); return (
{t("homeCrumb")} / {t("navBrowse")}

{t("browseListingsTitle")}

{t("browseListingsLead")}

{t("availabilityOnlyWithCalendar")}
{t("searchAmenities")}
{amenityOptions.map((opt) => ( ))}
{countLabel}
{error ? (

{error}

) : null}
{addressCenter ? ( ) : null}
{geoError ?

{geoError}

: null}
{countLabel} {addressCenter ? ( {t("addressRadiusLabel", { km: radiusKm })} ) : null}
{filtered.length === 0 ? (

{t("mapNoResults")}

) : (
{filtered.map((l) => (
setSelectedId(l.id)} onClick={() => setSelectedId(l.id)} > {l.coverImage ? ( {l.title} ) : (
)}

{l.title}

{l.isSample ? ( {t("sampleBadge")} ) : null}

{l.teaser ?? ""}

{l.priceWeekdayEuros || l.priceWeekendEuros ? (
{t("priceStartingFromShort", { price: Math.min( ...[ l.priceWeekdayEuros, l.priceWeekendEuros, ].filter((p): p is number => typeof p === "number"), ), })}
) : null}
{l.streetAddress ? `${l.streetAddress}, ` : ""} {l.city}, {l.region}
{t("capacityGuests", { count: l.maxGuests })} {t("capacityBedrooms", { count: l.bedrooms })} {l.hasCalendar ? ( {t("calendarConnected")} ) : null} {startDate && endDate && l.availableForDates ? ( {t("availableForDates")} ) : null} {l.evChargingOnSite ? ( {t("amenityEvOnSite")} ) : null} {l.evChargingAvailable && !l.evChargingOnSite ? ( {t("amenityEvNearby")} ) : null} {l.wheelchairAccessible ? ( {t("amenityWheelchairAccessible")} ) : null} {l.hasSkiPass ? ( {t("amenitySkiPass")} ) : null} {l.hasAirConditioning ? ( {t("amenityAirConditioning")} ) : null} {l.hasKitchen ? ( {t("amenityKitchen")} ) : null} {l.hasDishwasher ? ( {t("amenityDishwasher")} ) : null} {l.hasWashingMachine ? ( {t("amenityWashingMachine")} ) : null} {l.hasBarbecue ? ( {t("amenityBarbecue")} ) : null} {l.hasMicrowave ? ( {t("amenityMicrowave")} ) : null} {l.hasFreeParking ? ( {t("amenityFreeParking")} ) : null} {l.hasSauna ? ( {t("amenitySauna")} ) : null} {l.hasWifi ? ( {t("amenityWifi")} ) : null}
{t("openListing")}
))}
)}
); }