feat(plugins): seasonality + min-stay (Phase 3.2 + 3.4)

Plugin seasonality :
- Migration : Carbet.seasonalConstraints JSONB nullable
- lib/seasonality.ts : enum Season (DRY|LOW_WATER|WET), currentSeason() helper
  Guyane (juil-sept sèche, oct-nov étiage, déc-juin pluies), parseSeasonalConstraints,
  isCurrentlyOpen, SEASON_META (label/emoji/tone)
- Composant <SeasonBanner /> server, gated par plugin, ajouté dans layout
  au-dessus de tout le contenu — bandeau couleur+emoji+message contextuel

Plugin min-stay :
- Migration : Carbet.minStayNights, maxStayNights, minCapacity nullable
- Composant <StayConstraints /> client, gated par plugin — pill text
  '2 nuits minimum', '2-7 nuits', 'groupe 4+ recommandé'
- Carbet card et fiche enrichies avec les contraintes

Tous deux désactivables : sans le toggle, comportement legacy inchangé.
This commit is contained in:
Claude Integration 2026-05-31 08:50:26 +00:00
parent 4842a44746
commit be2391998d
7 changed files with 196 additions and 1 deletions

View file

@ -0,0 +1,41 @@
import { isPluginEnabled } from "@/lib/plugins/server";
import { currentSeason, SEASON_META } from "@/lib/seasonality";
/**
* Bandeau saison affiché en haut de la home et de /carbets si le plugin
* `seasonality` est activé. Server component pur, pas de fetch DB.
*/
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;
return (
<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="hidden text-[var(--color-karbe-bone)]/85 sm:inline">
· {messages[season]}
</span>
</span>
</div>
</aside>
);
}

View file

@ -0,0 +1,47 @@
"use client";
import { useIsPluginEnabled } from "@/lib/plugins/client";
/**
* Composant client qui affiche les contraintes de séjour si le plugin
* `min-stay` est activé. Sinon, retourne null (legacy = pas de contraintes).
*/
export function StayConstraints({
minNights,
maxNights,
minCapacity,
className = "",
}: {
minNights?: number | null;
maxNights?: number | null;
minCapacity?: number | null;
className?: string;
}) {
const enabled = useIsPluginEnabled("min-stay");
if (!enabled) return null;
if (!minNights && !maxNights && !minCapacity) return null;
const parts: string[] = [];
if (minNights && maxNights && minNights !== maxNights) {
parts.push(`${minNights}${maxNights} nuits`);
} else if (minNights) {
parts.push(
minNights === 1 ? "1 nuit minimum" : `${minNights} nuits minimum`,
);
} else if (maxNights) {
parts.push(`Max ${maxNights} nuits`);
}
if (minCapacity && minCapacity > 1) {
parts.push(`groupe de ${minCapacity}+ recommandé`);
}
if (!parts.length) return null;
return (
<span
className={`inline-flex items-center gap-1 rounded-full bg-[var(--color-karbe-wood-300)]/30 px-2 py-0.5 text-[11px] font-medium text-[var(--color-karbe-wood-700)] ring-1 ring-[var(--color-karbe-wood-500)]/20 ${className}`}
title="Contraintes de séjour fixées par le loueur"
>
🌙 {parts.join(" · ")}
</span>
);
}