171 lines
5.7 KiB
TypeScript
171 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { SAMPLE_LISTING_SLUG } from '../lib/sampleListing';
|
|
import { useI18n } from './components/I18nProvider';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
type LatestListing = {
|
|
id: string;
|
|
title: string;
|
|
slug: string;
|
|
teaser: string | null;
|
|
coverImage: string | null;
|
|
city: string;
|
|
region: string;
|
|
};
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
const highlights = [
|
|
{
|
|
keyTitle: 'highlightQualityTitle',
|
|
keyBody: 'highlightQualityBody',
|
|
},
|
|
{
|
|
keyTitle: 'highlightLocalTitle',
|
|
keyBody: 'highlightLocalBody',
|
|
},
|
|
{
|
|
keyTitle: 'highlightApiTitle',
|
|
keyBody: 'highlightApiBody',
|
|
},
|
|
];
|
|
|
|
export default function HomePage() {
|
|
const { t } = useI18n();
|
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
|
const apiBase = process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:3000/api';
|
|
const appEnv = process.env.APP_ENV || 'local';
|
|
const [latest, setLatest] = useState<LatestListing[]>([]);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const [loadingLatest, setLoadingLatest] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setLoadingLatest(true);
|
|
fetch('/api/listings?limit=8', { cache: 'no-store' })
|
|
.then((res) => res.json())
|
|
.then((data) => setLatest(data.listings ?? []))
|
|
.catch(() => setLatest([]))
|
|
.finally(() => setLoadingLatest(false));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!latest.length) return;
|
|
const id = setInterval(() => {
|
|
setActiveIndex((prev) => (prev + 1) % latest.length);
|
|
}, 4200);
|
|
return () => clearInterval(id);
|
|
}, [latest]);
|
|
|
|
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>
|
|
<div className="cta-row">
|
|
<Link className="button" href={`/listings/${SAMPLE_LISTING_SLUG}`}>
|
|
{t('ctaViewSample')}
|
|
</Link>
|
|
<Link className="button secondary" href="/listings">
|
|
{t('ctaBrowse')}
|
|
</Link>
|
|
<a className="button secondary" href="/api/health" target="_blank" rel="noreferrer">
|
|
{t('ctaHealth')}
|
|
</a>
|
|
</div>
|
|
</section>
|
|
|
|
<div className="cards">
|
|
{highlights.map((item) => (
|
|
<div key={item.keyTitle} className="panel">
|
|
<h3 className="card-title">{t(item.keyTitle as any)}</h3>
|
|
<p>{t(item.keyBody as any)}</p>
|
|
</div>
|
|
))}
|
|
<div className="panel env-card">
|
|
<h3 className="card-title">{t('runtimeConfigTitle')}</h3>
|
|
<div className="meta-grid">
|
|
<span>
|
|
<strong>{t('runtimeAppEnv')}</strong> <code>{appEnv}</code>
|
|
</span>
|
|
<span>
|
|
<strong>{t('runtimeSiteUrl')}</strong> <code>{siteUrl}</code>
|
|
</span>
|
|
<span>
|
|
<strong>{t('runtimeApiBase')}</strong> <code>{apiBase}</code>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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-grid">
|
|
<div className="latest-card">
|
|
{activeListing.coverImage ? (
|
|
<img src={activeListing.coverImage} alt={activeListing.title} className="latest-cover" />
|
|
) : (
|
|
<div className="latest-cover placeholder" />
|
|
)}
|
|
<div className="latest-meta">
|
|
<span className="badge">
|
|
{activeListing.city}, {activeListing.region}
|
|
</span>
|
|
<h3 style={{ margin: '6px 0 4px' }}>{activeListing.title}</h3>
|
|
<p style={{ margin: 0 }}>{activeListing.teaser}</p>
|
|
<div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
|
|
<Link className="button secondary" href={`/listings/${activeListing.slug}`}>
|
|
{t('openListing')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="latest-rail">
|
|
{latest.map((item, idx) => (
|
|
<button
|
|
key={item.id}
|
|
className={`rail-item ${idx === activeIndex ? 'active' : ''}`}
|
|
onClick={() => setActiveIndex(idx)}
|
|
>
|
|
<div className="rail-thumb">
|
|
{item.coverImage ? <img src={item.coverImage} alt={item.title} /> : <div className="rail-fallback" />}
|
|
</div>
|
|
<div className="rail-text">
|
|
<div className="rail-title">{item.title}</div>
|
|
<div className="rail-sub">{item.city}, {item.region}</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|