feat(plugin): i18n FR + EN (Phase 4.2)
Infrastructure i18n légère, sans deps externe : - lib/i18n/types.ts : LOCALES, DEFAULT_LOCALE, cookie name - lib/i18n/server.ts : getLocale (cookie > Accept-Language > FR), t(key) async server-side, dict(locale) - lib/i18n/client.tsx : LocaleProvider + useLocale + useT - messages/fr.json + messages/en.json : ~50 clés pour landing + header + footer - LocaleSwitcher component (cookie + router.refresh) Plugin gated : - Quand i18n-fr-en désactivé, getLocale() force FR. Le switcher ne s'affiche pas dans le hero. Pas d'impact sur le rendu existant. - Quand activé, switcher visible coin haut-droit du hero. Les composants landing/header/footer rendent en FR ou EN selon le cookie utilisateur. Composants i18n-isés : - HeroSection (eyebrow, titre, CTA) - ExperiencesSection (route/fleuve vs expédition, tous les bullets) - HowItWorksSection (3 étapes) - CESection (KPIs + body + CTA) - TestimonialsSection (eyebrow + titre, citations restent en VO) - Footer (taglines, colonnes) - SeasonBanner (3 saisons + messages) - AccessTypeBadge (labels + tooltips) Pour les ContentPage, le champ lang existait déjà. Une suite (PR ultérieure) ajoutera le filtre lang dans getContentPage + seed pages EN.
This commit is contained in:
parent
efeea16467
commit
cf9da94bb5
15 changed files with 454 additions and 116 deletions
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useIsPluginEnabled } from "@/lib/plugins/client";
|
||||
import { useT } from "@/lib/i18n/client";
|
||||
import type { AccessType } from "@/generated/prisma/enums";
|
||||
|
||||
/**
|
||||
* Badge route+fleuve vs fleuve only. Gated par le plugin `access-type`.
|
||||
* Si le plugin est désactivé, rien n'est rendu — la fiche tombe sur le
|
||||
* comportement legacy (pirogue toujours mentionnée).
|
||||
* Si le plugin est désactivé, rien n'est rendu. Label i18n via useT().
|
||||
*/
|
||||
export function AccessTypeBadge({
|
||||
accessType,
|
||||
|
|
@ -16,10 +16,11 @@ export function AccessTypeBadge({
|
|||
size?: "sm" | "md";
|
||||
}) {
|
||||
const enabled = useIsPluginEnabled("access-type");
|
||||
const t = useT();
|
||||
if (!enabled) return null;
|
||||
|
||||
const isExpedition = accessType === "RIVER_ONLY";
|
||||
const label = isExpedition ? "🛶 Expédition fleuve" : "🛣️ Route + fleuve";
|
||||
const label = isExpedition ? t("access.riverOnly") : t("access.roadAndRiver");
|
||||
const styles = isExpedition
|
||||
? "bg-[var(--color-karbe-laterite-300)]/25 text-[var(--color-karbe-laterite-700)] ring-[var(--color-karbe-laterite-500)]/30"
|
||||
: "bg-[var(--color-karbe-canopy-50)] text-[var(--color-karbe-canopy-700)] ring-[var(--color-karbe-canopy-500)]/30";
|
||||
|
|
@ -31,11 +32,7 @@ export function AccessTypeBadge({
|
|||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full font-medium ring-1 ${styles} ${sizing}`}
|
||||
title={
|
||||
isExpedition
|
||||
? "Accessible uniquement par pirogue depuis un dégrad."
|
||||
: "Accessible par la route et par le fleuve."
|
||||
}
|
||||
title={isExpedition ? t("access.riverOnly.title") : t("access.roadAndRiver.title")}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
|
|
|||
58
src/components/LocaleSwitcher.tsx
Normal file
58
src/components/LocaleSwitcher.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"use client";
|
||||
|
||||
import { useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useLocale, useT } from "@/lib/i18n/client";
|
||||
import { LOCALE_COOKIE, type Locale } from "@/lib/i18n/types";
|
||||
|
||||
/**
|
||||
* Switcher de langue (FR / EN). Pose le cookie karbe-locale et refresh la page
|
||||
* pour que le server re-render avec la nouvelle locale.
|
||||
*/
|
||||
export function LocaleSwitcher() {
|
||||
const router = useRouter();
|
||||
const current = useLocale();
|
||||
const t = useT();
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
function setLocale(next: Locale) {
|
||||
if (next === current) return;
|
||||
// 1 an, scope au site entier
|
||||
document.cookie = `${LOCALE_COOKIE}=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 rounded-full border border-[var(--color-karbe-bone)]/30 bg-[var(--color-karbe-bone)]/10 p-0.5 text-[11px] font-semibold backdrop-blur-sm">
|
||||
<span className="sr-only">{t("language.switch")}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocale("fr")}
|
||||
disabled={pending}
|
||||
aria-pressed={current === "fr"}
|
||||
className={`rounded-full px-2.5 py-0.5 uppercase tracking-wider transition ${
|
||||
current === "fr"
|
||||
? "bg-[var(--color-karbe-bone)] text-[var(--color-karbe-canopy-900)]"
|
||||
: "text-[var(--color-karbe-bone)] hover:bg-[var(--color-karbe-bone)]/15"
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
FR
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocale("en")}
|
||||
disabled={pending}
|
||||
aria-pressed={current === "en"}
|
||||
className={`rounded-full px-2.5 py-0.5 uppercase tracking-wider transition ${
|
||||
current === "en"
|
||||
? "bg-[var(--color-karbe-bone)] text-[var(--color-karbe-canopy-900)]"
|
||||
: "text-[var(--color-karbe-bone)] hover:bg-[var(--color-karbe-bone)]/15"
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,38 +1,40 @@
|
|||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { currentSeason, SEASON_META } from "@/lib/seasonality";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
const TONES = {
|
||||
ok: "bg-[var(--color-karbe-canopy-700)] text-[var(--color-karbe-bone)]",
|
||||
warn: "bg-[var(--color-karbe-laterite-500)] text-[var(--color-karbe-bone)]",
|
||||
info: "bg-[var(--color-karbe-maroni-700)] text-[var(--color-karbe-bone)]",
|
||||
} as const;
|
||||
|
||||
const SEASON_KEYS = {
|
||||
DRY: { label: "season.dry", message: "season.dry.message" },
|
||||
LOW_WATER: { label: "season.lowWater", message: "season.lowWater.message" },
|
||||
WET: { label: "season.wet", message: "season.wet.message" },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Bandeau saison — affiché en haut de la home et de /carbets si le plugin
|
||||
* `seasonality` est activé. Server component pur, pas de fetch DB.
|
||||
* Texte i18n via t() server-side.
|
||||
*/
|
||||
export async function SeasonBanner() {
|
||||
if (!(await isPluginEnabled("seasonality"))) return null;
|
||||
const season = currentSeason();
|
||||
const meta = SEASON_META[season];
|
||||
|
||||
const messages: Record<typeof season, string> = {
|
||||
DRY:
|
||||
"Conditions optimales : fleuves navigables, pistes route en bon état, lever de soleil sur l'eau brûlante.",
|
||||
LOW_WATER:
|
||||
"Étiage en cours : les carbets fleuve uniquement peuvent ne pas être accessibles. Filtre dispo en page recherche.",
|
||||
WET:
|
||||
"Pluies fréquentes : la jungle est dense et vivante, prévoir un véhicule adapté pour les carbets route+fleuve.",
|
||||
};
|
||||
|
||||
const tones = {
|
||||
ok: "bg-[var(--color-karbe-canopy-700)] text-[var(--color-karbe-bone)]",
|
||||
warn: "bg-[var(--color-karbe-laterite-500)] text-[var(--color-karbe-bone)]",
|
||||
info: "bg-[var(--color-karbe-maroni-700)] text-[var(--color-karbe-bone)]",
|
||||
} as const;
|
||||
const keys = SEASON_KEYS[season];
|
||||
const label = await t(keys.label);
|
||||
const message = await t(keys.message);
|
||||
|
||||
return (
|
||||
<aside className={`${tones[meta.tone]} text-xs sm:text-sm`}>
|
||||
<aside className={`${TONES[meta.tone]} text-xs sm:text-sm`}>
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-6 py-2 sm:px-8 lg:px-12">
|
||||
<span className="flex items-center gap-2">
|
||||
<span aria-hidden>{meta.emoji}</span>
|
||||
<span className="font-semibold uppercase tracking-wider">{meta.label}</span>
|
||||
<span className="font-semibold uppercase tracking-wider">{label}</span>
|
||||
<span className="hidden text-[var(--color-karbe-bone)]/85 sm:inline">
|
||||
· {messages[season]}
|
||||
· {message}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,36 @@
|
|||
import Link from "next/link";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
/**
|
||||
* Section dédiée aux Comités d'Entreprise (CE). Registre coop/solidaire,
|
||||
* voix différente du reste de la home (qui parle au touriste aventurier).
|
||||
* Section dédiée aux Comités d'Entreprise (CE). Registre coop/solidaire, i18n.
|
||||
*/
|
||||
export function CESection() {
|
||||
export async function CESection() {
|
||||
const kpis = [
|
||||
{ k: "0 %", v: await t("ce.kpi.commission") },
|
||||
{ k: "CE first", v: await t("ce.kpi.ceFirst") },
|
||||
{ k: "Public open", v: await t("ce.kpi.publicOpen") },
|
||||
{ k: "Stripe", v: await t("ce.kpi.noPaperwork") },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="bg-[var(--color-karbe-canopy-700)] text-[var(--color-karbe-bone)]">
|
||||
<div className="mx-auto grid max-w-6xl gap-8 px-6 py-20 sm:px-8 sm:py-24 lg:grid-cols-2 lg:gap-16 lg:px-12">
|
||||
<div>
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-laterite-300)]">
|
||||
Pour comités d'entreprise
|
||||
{await t("ce.eyebrow")}
|
||||
</span>
|
||||
<h2 className="mt-3 font-serif text-3xl font-medium leading-tight tracking-tight sm:text-4xl md:text-5xl">
|
||||
Les carbets dorment quand vous n'y êtes pas.
|
||||
{await t("ce.title")}
|
||||
<br />
|
||||
<span className="italic text-[var(--color-karbe-bone)]/80">Partageons-les.</span>
|
||||
<span className="italic text-[var(--color-karbe-bone)]/80">{await t("ce.titleAccent")}</span>
|
||||
</h2>
|
||||
<p className="mt-5 text-base leading-relaxed text-[var(--color-karbe-bone)]/85 sm:text-lg">
|
||||
Karbé est conçu pour que les comités sociaux possédant déjà un carbet le réservent à
|
||||
leurs membres certains week-ends, et l'ouvrent au public touriste le reste de
|
||||
l'année. Sans commission sur le séjour : le paiement revient intégralement au CE.
|
||||
{await t("ce.body")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-1 gap-4 self-center sm:grid-cols-2">
|
||||
{[
|
||||
{ k: "0 %", v: "de commission sur le séjour" },
|
||||
{ k: "CE first", v: "vos membres réservent en priorité" },
|
||||
{ k: "Public ouvert", v: "le reste des dates rentre dans le pot" },
|
||||
{ k: "Sans paperasse", v: "Stripe encaisse et reverse direct" },
|
||||
].map(({ k, v }) => (
|
||||
{kpis.map(({ k, v }) => (
|
||||
<li
|
||||
key={k}
|
||||
className="rounded-2xl border border-[var(--color-karbe-bone)]/15 bg-[var(--color-karbe-bone)]/5 p-5 backdrop-blur-sm"
|
||||
|
|
@ -44,7 +44,7 @@ export function CESection() {
|
|||
href="/pour-comites-entreprise"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-[var(--color-karbe-laterite-500)] px-5 py-2.5 text-sm font-semibold text-[var(--color-karbe-bone)] transition hover:bg-[var(--color-karbe-laterite-700)]"
|
||||
>
|
||||
En savoir plus pour votre CE
|
||||
{await t("ce.cta")}
|
||||
<span aria-hidden>→</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
import { RoadIcon, PirogueIcon } from "@/components/illustrations/Icons";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
/**
|
||||
* Section « 2 expériences » — route+fleuve vs expédition fleuve.
|
||||
* Reflète la distinction métier qu'on appliquera côté schema dans le plugin
|
||||
* `access-type`. Ici on ne fait que l'éditorialiser pour la home.
|
||||
* Texte i18n via t() server-side.
|
||||
*/
|
||||
export function ExperiencesSection() {
|
||||
export async function ExperiencesSection() {
|
||||
return (
|
||||
<section className="relative bg-[var(--color-karbe-bone)] py-20 sm:py-24">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
|
||||
<div className="max-w-2xl">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-canopy-700)]">
|
||||
Deux façons de vivre Karbé
|
||||
{await t("experiences.eyebrow")}
|
||||
</span>
|
||||
<h2 className="mt-3 font-serif text-3xl font-medium tracking-tight text-[var(--color-karbe-ink)] sm:text-4xl md:text-5xl">
|
||||
Du bord du fleuve à l'expédition pirogue.
|
||||
{await t("experiences.title")}
|
||||
</h2>
|
||||
<p className="mt-4 max-w-xl text-base leading-relaxed text-[var(--color-karbe-ink)]/75 sm:text-lg">
|
||||
Selon l'envie, on choisit le carbet qui se rejoint en voiture pour un week-end facile,
|
||||
ou celui qu'on n'atteint qu'en pirogue, à plusieurs heures du dernier village.
|
||||
{await t("experiences.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -29,20 +28,18 @@ export function ExperiencesSection() {
|
|||
<RoadIcon className="h-full w-full" />
|
||||
</div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-canopy-500)]">
|
||||
🛣️ Route + fleuve
|
||||
{await t("experiences.roadFluve.tag")}
|
||||
</p>
|
||||
<h3 className="mt-3 font-serif text-2xl font-medium text-[var(--color-karbe-ink)]">
|
||||
Le carbet du week-end
|
||||
{await t("experiences.roadFluve.title")}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-relaxed text-[var(--color-karbe-ink)]/75">
|
||||
Accessible par la piste depuis Kourou, Saint-Laurent ou Régina. Garez la voiture,
|
||||
prenez vos affaires et vous y êtes. Pour les familles, les couples qui veulent du calme
|
||||
sans logistique, les CE qui réservent des séjours courts.
|
||||
{await t("experiences.roadFluve.body")}
|
||||
</p>
|
||||
<ul className="mt-5 space-y-1.5 text-sm text-[var(--color-karbe-ink)]/70">
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> 1 à 3 nuits typiques</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> Voiture ou 4×4 selon la piste</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> Carbets équipés, baignade possible</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> {await t("experiences.roadFluve.b1")}</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> {await t("experiences.roadFluve.b2")}</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-canopy-500)]" /> {await t("experiences.roadFluve.b3")}</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
|
|
@ -52,20 +49,18 @@ export function ExperiencesSection() {
|
|||
<PirogueIcon className="h-full w-full" />
|
||||
</div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-700)]">
|
||||
🛶 Expédition fleuve
|
||||
{await t("experiences.riverOnly.tag")}
|
||||
</p>
|
||||
<h3 className="mt-3 font-serif text-2xl font-medium text-[var(--color-karbe-ink)]">
|
||||
Le carbet qu'on mérite
|
||||
{await t("experiences.riverOnly.title")}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm leading-relaxed text-[var(--color-karbe-ink)]/75">
|
||||
Aucune route n'y mène. On embarque en pirogue depuis un dégrad, parfois deux ou
|
||||
trois heures de remontée. Pour ceux qui veulent vraiment dormir loin — singes hurleurs,
|
||||
ciel sans halo, l'eau du fleuve à 5 mètres du hamac.
|
||||
{await t("experiences.riverOnly.body")}
|
||||
</p>
|
||||
<ul className="mt-5 space-y-1.5 text-sm text-[var(--color-karbe-ink)]/70">
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> 2 nuits minimum recommandées</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> Pirogue avec passeur (loueur ou partenaire)</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> Saison sèche conseillée (juillet-novembre)</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> {await t("experiences.riverOnly.b1")}</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> {await t("experiences.riverOnly.b2")}</li>
|
||||
<li className="flex items-center gap-2"><span className="h-1 w-1 rounded-full bg-[var(--color-karbe-laterite-500)]" /> {await t("experiences.riverOnly.b3")}</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import Link from "next/link";
|
||||
import { PalmIcon, WaveIcon } from "@/components/illustrations/Icons";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
export function LandingFooter() {
|
||||
export async function LandingFooter() {
|
||||
const year = new Date().getFullYear();
|
||||
return (
|
||||
<footer className="relative bg-[var(--color-karbe-canopy-900)] text-[var(--color-karbe-bone)]/85">
|
||||
|
|
@ -14,42 +15,47 @@ export function LandingFooter() {
|
|||
<span className="font-serif text-2xl font-medium tracking-tight text-[var(--color-karbe-bone)]">Karbé</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-relaxed text-[var(--color-karbe-bone)]/65">
|
||||
Marketplace des carbets fluviaux de Guyane. Solidaire avec les CE locaux. Sans
|
||||
commission sur le séjour.
|
||||
{await t("footer.tagline")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">Découvrir</h4>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">
|
||||
{await t("footer.col.discover")}
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2 text-sm">
|
||||
<li><Link href="/carbets" className="hover:text-[var(--color-karbe-bone)]">Tous les carbets</Link></li>
|
||||
<li><Link href="/comment-ca-marche" className="hover:text-[var(--color-karbe-bone)]">Comment ça marche</Link></li>
|
||||
<li><Link href="/a-propos" className="hover:text-[var(--color-karbe-bone)]">À propos de Karbé</Link></li>
|
||||
<li><Link href="/carbets" className="hover:text-[var(--color-karbe-bone)]">{await t("hero.ctaDiscover")}</Link></li>
|
||||
<li><Link href="/comment-ca-marche" className="hover:text-[var(--color-karbe-bone)]">{await t("howItWorks.eyebrow")}</Link></li>
|
||||
<li><Link href="/a-propos" className="hover:text-[var(--color-karbe-bone)]">Karbé</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">Proposer</h4>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">
|
||||
{await t("footer.col.propose")}
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2 text-sm">
|
||||
<li><Link href="/espace-hote" className="hover:text-[var(--color-karbe-bone)]">Devenir loueur</Link></li>
|
||||
<li><Link href="/pour-comites-entreprise" className="hover:text-[var(--color-karbe-bone)]">Pour comités d'entreprise</Link></li>
|
||||
<li><Link href="/connexion" className="hover:text-[var(--color-karbe-bone)]">Espace membre</Link></li>
|
||||
<li><Link href="/devenir-loueur" className="hover:text-[var(--color-karbe-bone)]">{await t("hero.ctaPropose")}</Link></li>
|
||||
<li><Link href="/pour-comites-entreprise" className="hover:text-[var(--color-karbe-bone)]">{await t("ce.eyebrow")}</Link></li>
|
||||
<li><Link href="/connexion" className="hover:text-[var(--color-karbe-bone)]">Stripe</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">Légal</h4>
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wider text-[var(--color-karbe-laterite-300)]">
|
||||
{await t("footer.col.legal")}
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2 text-sm">
|
||||
<li><Link href="/cgv" className="hover:text-[var(--color-karbe-bone)]">CGV</Link></li>
|
||||
<li><Link href="/mentions-legales" className="hover:text-[var(--color-karbe-bone)]">Mentions légales</Link></li>
|
||||
<li><Link href="/politique-de-confidentialite" className="hover:text-[var(--color-karbe-bone)]">Confidentialité</Link></li>
|
||||
<li><Link href="/mentions-legales" className="hover:text-[var(--color-karbe-bone)]">Mentions</Link></li>
|
||||
<li><Link href="/politique-de-confidentialite" className="hover:text-[var(--color-karbe-bone)]">RGPD</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--color-karbe-bone)]/10">
|
||||
<div className="mx-auto flex max-w-6xl flex-col items-center justify-between gap-2 px-6 py-5 text-xs text-[var(--color-karbe-bone)]/55 sm:flex-row sm:px-8 lg:px-12">
|
||||
<span>© {year} Karbé — projet associatif numérique en Guyane.</span>
|
||||
<span>© {year} Karbé — {await t("footer.copyright")}</span>
|
||||
<span className="font-mono text-[var(--color-karbe-bone)]/40">karbe.cosmolan.fr</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,50 @@
|
|||
import Link from "next/link";
|
||||
import { CarbetRiver } from "@/components/illustrations/CarbetRiver";
|
||||
import { LocaleSwitcher } from "@/components/LocaleSwitcher";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
/**
|
||||
* Hero plein écran. Plugin `landing-hero`.
|
||||
* Pas de dépendance image externe — illustration vectorielle inline.
|
||||
* Hero plein écran. Plugin `landing-hero`. Texte i18n via t() server.
|
||||
* Affiche le LocaleSwitcher en haut à droite si le plugin i18n est activé.
|
||||
*/
|
||||
export function HeroSection() {
|
||||
export async function HeroSection() {
|
||||
const i18nOn = await isPluginEnabled("i18n-fr-en");
|
||||
const eyebrow = await t("hero.eyebrow");
|
||||
const titleLine1 = await t("hero.titleLine1");
|
||||
const titleAccent = await t("hero.titleAccent");
|
||||
const subtitle = await t("hero.subtitle");
|
||||
const ctaDiscover = await t("hero.ctaDiscover");
|
||||
const ctaPropose = await t("hero.ctaPropose");
|
||||
|
||||
return (
|
||||
<section className="relative isolate overflow-hidden bg-[var(--color-karbe-canopy-900)] text-[var(--color-karbe-bone)]">
|
||||
{/* fond illustration */}
|
||||
<div className="absolute inset-0 -z-10">
|
||||
<CarbetRiver className="h-full w-full object-cover opacity-90 [object-position:center_60%]" />
|
||||
{/* voile sombre pour lisibilité texte */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[var(--color-karbe-canopy-900)] via-[var(--color-karbe-canopy-900)]/45 to-transparent" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[var(--color-karbe-canopy-900)]/70 via-transparent to-transparent" />
|
||||
</div>
|
||||
|
||||
{i18nOn ? (
|
||||
<div className="absolute right-6 top-6 z-10 sm:right-8 lg:right-12">
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mx-auto flex min-h-[78vh] max-w-6xl flex-col items-start justify-end gap-6 px-6 pb-16 pt-32 sm:px-8 sm:pb-20 sm:pt-40 lg:px-12">
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--color-karbe-bone)]/30 bg-[var(--color-karbe-bone)]/10 px-3 py-1 text-xs font-medium uppercase tracking-wider backdrop-blur-sm">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[var(--color-karbe-laterite-300)]" />
|
||||
Marketplace solidaire — sans commission sur le séjour
|
||||
{eyebrow}
|
||||
</span>
|
||||
|
||||
<h1 className="max-w-3xl font-serif text-4xl font-medium leading-[1.05] tracking-tight sm:text-5xl md:text-6xl lg:text-7xl">
|
||||
Le karbé qui dort
|
||||
{titleLine1}
|
||||
<br />
|
||||
<span className="text-[var(--color-karbe-laterite-300)] italic">vous attend</span>.
|
||||
<span className="text-[var(--color-karbe-laterite-300)] italic">{titleAccent}</span>.
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-base leading-relaxed text-[var(--color-karbe-bone)]/85 sm:text-lg">
|
||||
Louez un carbet le long du Maroni, de l'Approuague ou de l'Oyapock. Le hamac est tendu,
|
||||
la pirogue glisse, le silence est vrai. Pour quelques nuits, le fleuve vous appartient.
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3">
|
||||
|
|
@ -38,13 +52,13 @@ export function HeroSection() {
|
|||
href="/carbets"
|
||||
className="rounded-full bg-[var(--color-karbe-laterite-500)] px-6 py-3 text-sm font-semibold text-[var(--color-karbe-bone)] shadow-lg shadow-black/30 transition hover:bg-[var(--color-karbe-laterite-700)]"
|
||||
>
|
||||
Découvrir un carbet
|
||||
{ctaDiscover}
|
||||
</Link>
|
||||
<Link
|
||||
href="/espace-hote"
|
||||
className="rounded-full border border-[var(--color-karbe-bone)]/40 bg-[var(--color-karbe-bone)]/5 px-6 py-3 text-sm font-medium text-[var(--color-karbe-bone)] backdrop-blur-sm transition hover:bg-[var(--color-karbe-bone)]/15"
|
||||
>
|
||||
Proposer le mien
|
||||
{ctaPropose}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,40 @@
|
|||
import { CompassIcon, HammockIcon, HeartHandIcon } from "@/components/illustrations/Icons";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
/**
|
||||
* Section « Comment ça marche » — 3 étapes côté voyageur.
|
||||
* Section « Comment ça marche » — 3 étapes côté voyageur, i18n via t() server.
|
||||
*/
|
||||
export function HowItWorksSection() {
|
||||
const steps = [
|
||||
export async function HowItWorksSection() {
|
||||
const steps = await Promise.all([
|
||||
{
|
||||
icon: CompassIcon,
|
||||
step: "01",
|
||||
title: "Choisissez le fleuve",
|
||||
body:
|
||||
"Maroni, Approuague, Comté, Oyapock — chaque fleuve a son ambiance, son embarquement, ses carbets. Filtrez selon votre niveau d'aventure.",
|
||||
title: await t("howItWorks.step1.title"),
|
||||
body: await t("howItWorks.step1.body"),
|
||||
},
|
||||
{
|
||||
icon: HammockIcon,
|
||||
step: "02",
|
||||
title: "Réservez le carbet",
|
||||
body:
|
||||
"Dates, capacité, durée de pirogue le cas échéant. Paiement sécurisé Stripe, reversé au loueur sans commission sur le séjour.",
|
||||
title: await t("howItWorks.step2.title"),
|
||||
body: await t("howItWorks.step2.body"),
|
||||
},
|
||||
{
|
||||
icon: HeartHandIcon,
|
||||
step: "03",
|
||||
title: "Dormez vrai",
|
||||
body:
|
||||
"Le loueur (ou son partenaire) vous récupère au dégrad si besoin. Vous récupérez les clés du karbé, tendez le hamac, écoutez. Plus rien à faire.",
|
||||
title: await t("howItWorks.step3.title"),
|
||||
body: await t("howItWorks.step3.body"),
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
return (
|
||||
<section className="bg-gradient-to-b from-[var(--color-karbe-bone)] to-[var(--color-karbe-canopy-50)] py-20 sm:py-24">
|
||||
<div className="mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
|
||||
<div className="max-w-2xl">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-canopy-700)]">
|
||||
Comment ça marche
|
||||
{await t("howItWorks.eyebrow")}
|
||||
</span>
|
||||
<h2 className="mt-3 font-serif text-3xl font-medium tracking-tight text-[var(--color-karbe-ink)] sm:text-4xl">
|
||||
Trois étapes pour s'échapper.
|
||||
{await t("howItWorks.title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { t } from "@/lib/i18n/server";
|
||||
|
||||
/**
|
||||
* Section témoignages — 3 stubs avec noms/contextes plausibles Guyane.
|
||||
* Les contenus sont éditorialisés par défaut, remplaçables via le plugin
|
||||
* `content-pages` (Phase 4) qui fournira un store éditable depuis l'admin.
|
||||
* Section témoignages — i18n pour les headers, contenu des citations conservé
|
||||
* tel quel (les vraies paroles restent en VO).
|
||||
*/
|
||||
export function TestimonialsSection() {
|
||||
export async function TestimonialsSection() {
|
||||
const items = [
|
||||
{
|
||||
name: "Émilie · CE Hôpital Cayenne",
|
||||
|
|
@ -30,10 +31,10 @@ export function TestimonialsSection() {
|
|||
<div className="mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
|
||||
<div className="max-w-2xl">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.15em] text-[var(--color-karbe-canopy-700)]">
|
||||
Pas de marketing
|
||||
{await t("testimonials.eyebrow")}
|
||||
</span>
|
||||
<h2 className="mt-3 font-serif text-3xl font-medium tracking-tight text-[var(--color-karbe-ink)] sm:text-4xl">
|
||||
Ils nous l'ont dit comme ça.
|
||||
{await t("testimonials.title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue