From 1257042a665800ca42a6b9fcde203c3c8613ee84 Mon Sep 17 00:00:00 2001 From: Tero Halla-aho Date: Mon, 24 Nov 2025 21:11:07 +0200 Subject: [PATCH] feat: polish latest listings carousel and stabilize map --- app/globals.css | 22 +++++++++++++++++++ app/listings/page.tsx | 50 ++++++++++++++++++++----------------------- app/page.tsx | 14 ++++++++---- package-lock.json | 25 ++++++++++++++++++++++ package.json | 2 ++ 5 files changed, 82 insertions(+), 31 deletions(-) diff --git a/app/globals.css b/app/globals.css index f3f714b..9907359 100644 --- a/app/globals.css +++ b/app/globals.css @@ -229,6 +229,28 @@ p { font-size: 13px; } +.dot-row { + display: flex; + gap: 6px; + align-items: center; + justify-content: center; + margin-top: 4px; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: rgba(148, 163, 184, 0.4); + cursor: pointer; + transition: transform 120ms ease, background 120ms ease; +} + +.dot.active { + background: var(--accent); + transform: scale(1.2); +} + .results-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); diff --git a/app/listings/page.tsx b/app/listings/page.tsx index e316d8e..2473d6a 100644 --- a/app/listings/page.tsx +++ b/app/listings/page.tsx @@ -47,33 +47,24 @@ function haversineKm(a: LatLng, b: LatLng) { return 2 * R * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); } -function loadLeaflet(): Promise { +async function loadLeaflet(): Promise { if (typeof window === 'undefined') return Promise.reject(); - if ((window as any).L) return Promise.resolve((window as any).L); - return new Promise((resolve, reject) => { - const existingStyle = document.querySelector('link[data-leaflet-style]'); - if (!existingStyle) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; - link.setAttribute('data-leaflet-style', 'true'); - document.head.appendChild(link); - } - - const existingScript = document.querySelector('script[data-leaflet]'); - if (existingScript) { - existingScript.addEventListener('load', () => resolve((window as any).L)); - existingScript.addEventListener('error', reject); - return; - } - const script = document.createElement('script'); - script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; - script.async = true; - script.setAttribute('data-leaflet', 'true'); - script.onload = () => resolve((window as any).L); - script.onerror = reject; - document.body.appendChild(script); - }); + if ((window as any).L) return (window as any).L; + 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); + } + try { + const mod = await import('leaflet'); + (window as any).L = mod; + return mod; + } catch (err) { + return Promise.reject(err); + } } function ListingsMap({ @@ -93,6 +84,7 @@ function ListingsMap({ const mapRef = useRef(null); const markersRef = useRef([]); const [ready, setReady] = useState(false); + const [mapError, setMapError] = useState(null); useEffect(() => { let cancelled = false; @@ -100,6 +92,7 @@ function ListingsMap({ .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); @@ -131,8 +124,10 @@ function ListingsMap({ mapRef.current.fitBounds(group.getBounds().pad(0.25)); } }) - .catch(() => { + .catch((err) => { + console.error('Leaflet load failed', err); setReady(false); + setMapError('Map could not be loaded right now.'); }); return () => { @@ -151,6 +146,7 @@ function ListingsMap({ return (
{!ready ?
{loadingText}
: null} + {mapError ?
{mapError}
: null}
); diff --git a/app/page.tsx b/app/page.tsx index 25f7ebd..5ccc609 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -40,23 +40,24 @@ export default function HomePage() { const [latest, setLatest] = useState([]); const [activeIndex, setActiveIndex] = useState(0); const [loadingLatest, setLoadingLatest] = useState(false); + const [paused, setPaused] = useState(false); useEffect(() => { setLoadingLatest(true); fetch('/api/listings?limit=8', { cache: 'no-store' }) .then((res) => res.json()) - .then((data) => setLatest(data.listings ?? [])) + .then((data) => setLatest((data.listings ?? []).slice(0, 5))) .catch(() => setLatest([])) .finally(() => setLoadingLatest(false)); }, []); useEffect(() => { - if (!latest.length) return; + if (!latest.length || paused) return; const id = setInterval(() => { setActiveIndex((prev) => (prev + 1) % latest.length); }, 4200); return () => clearInterval(id); - }, [latest]); + }, [latest, paused]); useEffect(() => { if (activeIndex >= latest.length) { @@ -126,7 +127,7 @@ export default function HomePage() { ) : !activeListing ? (

{t('mapNoResults')}

) : ( -
+
setPaused(true)} onMouseLeave={() => setPaused(false)}>
{activeListing.coverImage ? ( {activeListing.title} @@ -162,6 +163,11 @@ export default function HomePage() {
))} +
+ {latest.map((_, idx) => ( + setActiveIndex(idx)} /> + ))} +
)} diff --git a/package-lock.json b/package-lock.json index 6e41d0e..220cd85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@prisma/client": "^7.0.0", "bcryptjs": "^3.0.3", "jose": "^6.1.2", + "leaflet": "^1.9.4", "next": "^14.2.32", "nodemailer": "^7.0.10", "pg": "^8.16.3", @@ -20,6 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@types/leaflet": "^1.9.21", "@types/node": "^20.12.7", "@types/nodemailer": "^7.0.4", "@types/pg": "^8.15.6", @@ -2117,6 +2119,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -2124,6 +2133,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -5449,6 +5468,12 @@ "node": ">=0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 883e439..fb0694e 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@prisma/client": "^7.0.0", "bcryptjs": "^3.0.3", "jose": "^6.1.2", + "leaflet": "^1.9.4", "next": "^14.2.32", "nodemailer": "^7.0.10", "pg": "^8.16.3", @@ -23,6 +24,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@types/leaflet": "^1.9.21", "@types/node": "^20.12.7", "@types/nodemailer": "^7.0.4", "@types/pg": "^8.15.6",