feat: polish latest listings carousel and stabilize map
This commit is contained in:
parent
86de2cabdf
commit
1257042a66
5 changed files with 82 additions and 31 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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<any> {
|
||||
async function loadLeaflet(): Promise<any> {
|
||||
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<any>(null);
|
||||
const markersRef = useRef<any[]>([]);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [mapError, setMapError] = useState<string | null>(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 (
|
||||
<div className="map-frame">
|
||||
{!ready ? <div className="map-placeholder">{loadingText}</div> : null}
|
||||
{mapError ? <div className="map-placeholder">{mapError}</div> : null}
|
||||
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
14
app/page.tsx
14
app/page.tsx
|
|
@ -40,23 +40,24 @@ export default function HomePage() {
|
|||
const [latest, setLatest] = useState<LatestListing[]>([]);
|
||||
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 ? (
|
||||
<p style={{ color: '#cbd5e1', marginTop: 10 }}>{t('mapNoResults')}</p>
|
||||
) : (
|
||||
<div className="latest-grid">
|
||||
<div className="latest-grid" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
|
||||
<div className="latest-card">
|
||||
{activeListing.coverImage ? (
|
||||
<img src={activeListing.coverImage} alt={activeListing.title} className="latest-cover" />
|
||||
|
|
@ -162,6 +163,11 @@ export default function HomePage() {
|
|||
</div>
|
||||
</button>
|
||||
))}
|
||||
<div className="dot-row">
|
||||
{latest.map((_, idx) => (
|
||||
<span key={idx} className={`dot ${idx === activeIndex ? 'active' : ''}`} onClick={() => setActiveIndex(idx)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue