lomavuokraus/app/page.tsx
Tero Halla-aho 0bb709d9c5
Some checks failed
CI / checks (push) Has been cancelled
chore: fix audit alerts and formatting
2026-02-04 12:43:03 +02:00

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