189 lines
6.3 KiB
TypeScript
189 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useI18n } from "./components/I18nProvider";
|
|
|
|
type LatestListing = {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
teaser: string | null;
|
|
coverImage: string | null;
|
|
city: string;
|
|
region: string;
|
|
isSample: boolean;
|
|
priceWeekdayEuros: number | null;
|
|
priceWeekendEuros: number | null;
|
|
};
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default function HomePage() {
|
|
const { t } = useI18n();
|
|
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 ?? []).slice(0, 5)))
|
|
.catch(() => setLatest([]))
|
|
.finally(() => setLoadingLatest(false));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!latest.length || paused) return;
|
|
const id = setInterval(() => {
|
|
setActiveIndex((prev) => (prev + 1) % latest.length);
|
|
}, 4200);
|
|
return () => clearInterval(id);
|
|
}, [latest, paused]);
|
|
|
|
useEffect(() => {
|
|
if (activeIndex >= latest.length) {
|
|
setActiveIndex(0);
|
|
}
|
|
}, [activeIndex, latest.length]);
|
|
|
|
const activeListing = useMemo(() => {
|
|
if (!latest.length) return null;
|
|
return latest[Math.min(activeIndex, latest.length - 1)];
|
|
}, [latest, activeIndex]);
|
|
|
|
return (
|
|
<main>
|
|
<section className="hero">
|
|
<span className="eyebrow">{t("heroEyebrow")}</span>
|
|
<h1>{t("heroTitle")}</h1>
|
|
<p>{t("heroBody")}</p>
|
|
</section>
|
|
|
|
<section className="panel latest-panel">
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
}}
|
|
>
|
|
<div>
|
|
<h2 style={{ margin: 0 }}>{t("latestListingsTitle")}</h2>
|
|
<p style={{ marginTop: 4 }}>{t("latestListingsLead")}</p>
|
|
</div>
|
|
<Link className="button secondary" href="/listings">
|
|
{t("ctaBrowse")}
|
|
</Link>
|
|
</div>
|
|
{loadingLatest ? (
|
|
<p style={{ color: "#cbd5e1", marginTop: 10 }}>{t("loading")}</p>
|
|
) : !activeListing ? (
|
|
<p style={{ color: "#cbd5e1", marginTop: 10 }}>{t("mapNoResults")}</p>
|
|
) : (
|
|
<div
|
|
className="latest-carousel"
|
|
onMouseEnter={() => setPaused(true)}
|
|
onMouseLeave={() => setPaused(false)}
|
|
>
|
|
<div className="carousel-window">
|
|
<div
|
|
className="carousel-track"
|
|
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
|
|
>
|
|
{latest.map((item) => (
|
|
<article key={item.id} className="carousel-slide">
|
|
{item.coverImage ? (
|
|
<a
|
|
href={`/listings/${item.slug}`}
|
|
className="latest-cover-link"
|
|
aria-label={item.title}
|
|
>
|
|
<img
|
|
src={item.coverImage}
|
|
alt={item.title}
|
|
className="latest-cover"
|
|
/>
|
|
</a>
|
|
) : (
|
|
<a
|
|
href={`/listings/${item.slug}`}
|
|
className="latest-cover-link"
|
|
aria-label={item.title}
|
|
>
|
|
<div className="latest-cover placeholder" />
|
|
</a>
|
|
)}
|
|
<div className="latest-meta">
|
|
<div
|
|
style={{ display: "flex", gap: 6, flexWrap: "wrap" }}
|
|
>
|
|
<span className="badge">
|
|
{item.city}, {item.region}
|
|
</span>
|
|
{item.isSample ? (
|
|
<span className="badge warning">
|
|
{t("sampleBadge")}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<h3 style={{ margin: "6px 0 4px" }}>{item.title}</h3>
|
|
<p style={{ margin: 0 }}>{item.teaser}</p>
|
|
{item.priceWeekdayEuros || item.priceWeekendEuros ? (
|
|
<div
|
|
style={{
|
|
color: "#cbd5e1",
|
|
fontSize: 14,
|
|
marginTop: 2,
|
|
}}
|
|
>
|
|
{t("priceStartingFromShort", {
|
|
price: Math.min(
|
|
...[
|
|
item.priceWeekdayEuros,
|
|
item.priceWeekendEuros,
|
|
].filter(
|
|
(p): p is number => typeof p === "number",
|
|
),
|
|
),
|
|
})}
|
|
</div>
|
|
) : null}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
gap: 8,
|
|
marginTop: 10,
|
|
flexWrap: "wrap",
|
|
}}
|
|
>
|
|
<Link
|
|
className="button secondary"
|
|
href={`/listings/${item.slug}`}
|
|
>
|
|
{t("openListing")}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="dot-row">
|
|
{latest.map((_, idx) => (
|
|
<span
|
|
key={idx}
|
|
className={`dot ${idx === activeIndex ? "active" : ""}`}
|
|
onClick={() => setActiveIndex(idx)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|