karbe/src/app/carbets/page.tsx
Karbé Architect c2df6722f2 feat(carbets): public search + carbet detail page (SSR/SEO)
Implémente SYS-5 : la marketplace publique pour découvrir les carbets
fluviaux publiés par les hôtes.

- /carbets : page de recherche server-side avec filtres GET
  (fleuve, dates de séjour, capacité min.), grille de résultats
  avec photo de couverture, fleuve, capacité, durée pirogue
- /carbets/[slug] : fiche carbet SSR
  - generateMetadata (title/description + OpenGraph/Twitter cards)
  - galerie médias (photo couverture + vignettes vidéo/photo)
  - description, équipements (catalogue), accès, coords GPS,
    capacité, prénom de l'hôte
- robots.ts + sitemap.xml (incluant les carbets publiés)
- metadataBase / title.template au niveau du root layout, OG par
  défaut Karbé
- Lien "Découvrir les carbets" sur la home
- Helpers partagés : lib/carbet-search.ts (parse filters + query),
  lib/carbet-public.ts (fetch SSR mémoïsé via React cache),
  lib/format.ts (durée pirogue, troncature, coords)
- Nouvelle variable d'env NEXT_PUBLIC_SITE_URL (canonical/OG/sitemap)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-29 22:24:25 +00:00

87 lines
2.7 KiB
TypeScript

import type { Metadata } from "next";
import {
listPublishedRivers,
parseSearchFilters,
searchCarbets,
type RawSearchParams,
} from "@/lib/carbet-search";
import { CarbetCard } from "./_components/carbet-card";
import { SearchFilters } from "./_components/search-filters";
export const metadata: Metadata = {
title: "Rechercher un carbet",
description:
"Explorez les carbets fluviaux de Guyane disponibles sur Karbé : filtrez par fleuve, dates de séjour et capacité d'accueil.",
alternates: { canonical: "/carbets" },
openGraph: {
title: "Rechercher un carbet — Karbé",
description:
"Trouvez un carbet authentique le long des fleuves de Guyane. Filtrez par fleuve, dates et capacité.",
type: "website",
},
};
export default async function CarbetsSearchPage({
searchParams,
}: {
searchParams: Promise<RawSearchParams>;
}) {
const raw = await searchParams;
const filters = parseSearchFilters(raw);
const [results, rivers] = await Promise.all([
searchCarbets(filters),
listPublishedRivers(),
]);
const hasActiveFilters = Boolean(
filters.river ||
filters.startDate ||
filters.endDate ||
filters.capacity,
);
return (
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-10">
<header className="mb-6">
<h1 className="text-3xl font-semibold text-zinc-900 sm:text-4xl">
Carbets fluviaux de Guyane
</h1>
<p className="mt-2 max-w-2xl text-base text-zinc-600">
Sélectionnez votre fleuve, vos dates et le nombre de voyageurs pour
découvrir les carbets disponibles, depuis le Maroni jusqu&apos;à
l&apos;Oyapock.
</p>
</header>
<SearchFilters filters={filters} rivers={rivers} />
<section className="mt-8" aria-live="polite">
<h2 className="sr-only">Résultats de la recherche</h2>
{results.length === 0 ? (
<p className="rounded-md border border-dashed border-zinc-300 px-6 py-12 text-center text-sm text-zinc-500">
{hasActiveFilters
? "Aucun carbet ne correspond à votre recherche. Essayez d'élargir les filtres."
: "Aucun carbet publié pour le moment. Revenez bientôt !"}
</p>
) : (
<>
<p className="mb-4 text-sm text-zinc-600">
{results.length} carbet{results.length > 1 ? "s" : ""} trouvé
{results.length > 1 ? "s" : ""}.
</p>
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{results.map((carbet) => (
<li key={carbet.id}>
<CarbetCard carbet={carbet} />
</li>
))}
</ul>
</>
)}
</section>
</main>
);
}