/* BoatsFun — Page demande de réservation V1 (checkout-combine.html)
Réservation simple via ?boat=slug, ou combinée via ?slugs=...®ion=...&date=...&slot=...
Affiche le récap des bateaux + totaux ESTIMÉS (aucun paiement), le mode d'opération,
les services sur demande et un formulaire "Vos informations".
CTA "Envoyer la demande de réservation" → handleReservationRequestSubmit :
regroupe la demande dans un objet et fait un console.log (Zapier/email à brancher post-V1). */
(function () {
const { I } = window.BF;
const { BOATS, bySlug, blockTotal, depositFor, TIME_SLOTS } = window.BFBoats;
const { useState, useMemo } = React;
const COMBINE_KEY = "bf.combine.selection";
// Images temporaires (Unsplash) — style nautique / premium / lifestyle.
// À remplacer par les visuels finaux dans assets/ quand fournis.
const SERVICES = [
{ id: "crew", title: "Équipage à bord", desc: "Personnel de bord supplémentaire pour accompagner votre sortie.", img: "https://images.unsplash.com/photo-1540541338287-41700207dee6?w=800&q=80&auto=format&fit=crop" },
{ id: "traiteur", title: "Service traiteur", desc: "Repas préparés, bouchées ou service complet selon votre événement.", img: "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&q=80&auto=format&fit=crop" },
{ id: "dj", title: "DJ privé", desc: "Un DJ pour créer une ambiance festive directement à bord.", img: "https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=800&q=80&auto=format&fit=crop" },
{ id: "barman", title: "Barman privé", desc: "Service de cocktails et boissons préparées pendant la sortie.", img: "https://images.unsplash.com/photo-1514362545857-3bc16c4c7d1b?w=800&q=80&auto=format&fit=crop" },
{ id: "transport", title: "Transport privé / limousine / party bus", desc: "Transport premium pour arriver à la marina sans stress.", img: "https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=800&q=80&auto=format&fit=crop" },
{ id: "media", title: "Photos & vidéo souvenir", desc: "Capturez votre sortie avec du contenu photo ou vidéo professionnel.", img: "https://images.unsplash.com/photo-1492144534655-ae79c964c9d7?w=800&q=80&auto=format&fit=crop" },
];
// Critères d'admissibilité pour l'option « opérer le bateau soi-même » (tous requis).
const ELIG_ITEMS = [
{ k: "competence", label: "Je confirme avoir une preuve de compétence valide pour opérer une embarcation motorisée." },
{ k: "experience", label: "Je confirme avoir au moins 2 ans d'expérience de navigation pertinente." },
{ k: "similaire", label: "J'ai déjà opéré un bateau de taille similaire." },
{ k: "manoeuvres", label: "Je suis à l'aise avec les manœuvres, l'accostage, les règles de navigation et les conditions météo." },
{ k: "documents", label: "Je comprends que BoatsFun peut demander des documents justificatifs avant d'approuver ma demande." },
];
function readParams() {
if (typeof window === "undefined") return { slugs: [], boat: "", region: "", date: "", timeSlot: "" };
const p = new URLSearchParams(window.location.search);
const slugsParam = p.get("slugs") || "";
const slugs = slugsParam.split(",").map((s) => s.trim()).filter(Boolean);
return {
slugs,
boat: (p.get("boat") || "").trim(), // réservation simple depuis une fiche bateau
region: p.get("region") || "",
date: p.get("date") || "",
timeSlot: p.get("slot") || ""
};
}
function readFallback() {
try {
const raw = localStorage.getItem(COMBINE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch (e) { return null; }
}
// Formatte les zones de navigation communes entre les bateaux sélectionnés
function commonZones(boats) {
if (!boats.length) return [];
const set = new Set(boats[0].compatibleMeetingZones || []);
boats.slice(1).forEach((b) => {
const z = new Set(b.compatibleMeetingZones || []);
for (const v of Array.from(set)) if (!z.has(v)) set.delete(v);
});
return Array.from(set);
}
function formatDate(iso) {
if (!iso) return "Non renseignée";
try {
const d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("fr-CA", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
} catch (e) { return iso; }
}
const styles = `
.bf .cc-hero { background:linear-gradient(180deg, #002244 0%, #003a66 100%); color:#fff; padding:56px 0 48px; }
.bf .cc-hero .eyebrow { color:var(--teal); }
.bf .cc-hero h1 { font-family:'Poppins', sans-serif; font-size:42px; line-height:1.1; font-weight:800; color:#fff; margin:12px 0 12px; letter-spacing:-1.1px; }
.bf .cc-hero p.lead { font-size:16.5px; line-height:1.55; color:rgba(255,255,255,.82); max-width:680px; }
.bf .cc-wrap { padding:48px 0 var(--section-pad); }
.bf .cc-grid { display:grid; grid-template-columns: 1.5fr 1fr; gap:28px; align-items:start; }
/* Cartes bateaux */
.bf .cc-section { background:#fff; border:1px solid var(--hair); border-radius:18px; padding:24px 26px; }
.bf .cc-section h2 { font-family:'Poppins', sans-serif; font-size:22px; font-weight:700; color:var(--navy); margin-bottom:18px; letter-spacing:-.4px; }
.bf .cc-boats { display:flex; flex-direction:column; gap:16px; }
.bf .cc-boat { display:grid; grid-template-columns: 120px 1fr auto; gap:16px; align-items:center; padding:12px; border:1px solid var(--hair); border-radius:14px; transition:border-color .15s; }
.bf .cc-boat:hover { border-color:var(--navy); }
.bf .cc-boat-img { width:120px; height:90px; border-radius:10px; overflow:hidden; background:var(--sea-mist); }
.bf .cc-boat-img img { width:100%; height:100%; object-fit:cover; display:block; }
.bf .cc-boat-info { min-width:0; }
.bf .cc-boat-info h3 { font-family:'Poppins', sans-serif; font-size:16.5px; font-weight:700; color:var(--navy); line-height:1.25; letter-spacing:-.2px; margin-bottom:4px; }
.bf .cc-boat-info .ccb-type { display:inline-block; background:var(--sea-mist); color:var(--navy); font-family:'Inter', sans-serif; font-size:11.5px; font-weight:600; padding:2px 9px; border-radius:999px; margin-right:6px; }
.bf .cc-boat-info .ccb-meta { font-size:13px; color:var(--body); margin-top:6px; line-height:1.4; }
.bf .cc-boat-info .ccb-meta svg { width:13px; height:13px; vertical-align:-2px; margin-right:4px; color:var(--teal); }
.bf .cc-boat-price { text-align:right; }
.bf .cc-boat-price .pp { font-family:'Poppins', sans-serif; font-size:18px; font-weight:700; color:var(--navy); }
.bf .cc-boat-price .pd { font-size:12px; color:var(--muted); margin-top:3px; }
/* Sidebar : récap + paiement */
.bf .cc-side { display:flex; flex-direction:column; gap:18px; position:sticky; top:96px; }
.bf .cc-summary { background:#fff; border:1px solid var(--hair); border-radius:18px; padding:24px 26px; }
.bf .cc-summary h2 { font-family:'Poppins', sans-serif; font-size:20px; font-weight:700; color:var(--navy); margin-bottom:14px; }
.bf .cc-summary .ccs-row { display:flex; justify-content:space-between; padding:9px 0; border-bottom:1px dashed var(--hair); font-size:14px; color:var(--body); }
.bf .cc-summary .ccs-row:last-of-type { border-bottom:none; }
.bf .cc-summary .ccs-row b { color:var(--navy); font-weight:600; }
.bf .cc-summary .ccs-totals { margin-top:18px; padding-top:18px; border-top:2px solid var(--hair); }
.bf .cc-summary .ccs-tot { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:8px; font-size:14.5px; color:var(--body); }
.bf .cc-summary .ccs-tot.is-big { font-family:'Poppins', sans-serif; font-size:22px; font-weight:700; color:var(--navy); margin-bottom:0; }
.bf .cc-summary .ccs-tot .amount { color:var(--navy); font-weight:700; }
.bf .cc-summary .ccs-tot.is-big .amount { color:var(--orange); }
.bf .cc-summary .ccs-cta { width:100%; margin-top:18px; background:var(--orange); border:none; color:#fff; font-family:'Inter', sans-serif; font-weight:600; font-size:15px; padding:15px 24px; border-radius:999px; cursor:pointer; box-shadow:var(--cta-shadow-orange); transition:all .2s; display:inline-flex; align-items:center; justify-content:center; gap:8px; }
.bf .cc-summary .ccs-cta:hover { background:#FF9420; transform:translateY(-2px); box-shadow:var(--cta-shadow-orange-hover); }
.bf .cc-summary .ccs-cta:disabled { opacity:.5; cursor:not-allowed; transform:none; }
.bf .cc-note { background:var(--sea-mist); border-left:3px solid var(--teal); border-radius:10px; padding:14px 16px; font-size:13.5px; color:var(--body); line-height:1.55; }
.bf .cc-note strong { color:var(--navy); display:block; margin-bottom:4px; font-size:13.5px; }
.bf .cc-empty { text-align:center; padding:80px 24px; background:#fff; border:1px dashed var(--hair); border-radius:18px; }
.bf .cc-empty h2 { font-family:'Poppins', sans-serif; font-size:22px; color:var(--navy); margin-bottom:8px; }
.bf .cc-empty p { color:var(--body); margin-bottom:18px; }
@media (max-width: 1024px) {
.bf .cc-grid { grid-template-columns: 1fr; }
.bf .cc-side { position:static; }
}
@media (max-width: 860px) {
.bf .cc-hero h1 { font-size:30px; }
.bf .cc-section, .bf .cc-summary { padding:18px 18px; }
.bf .cc-boat { grid-template-columns: 90px 1fr; }
.bf .cc-boat-img { width:90px; height:75px; }
.bf .cc-boat-price { grid-column:1 / -1; text-align:left; padding-top:6px; border-top:1px dashed var(--hair); }
}
/* Add-ons */
.bf .cc-left { display:flex; flex-direction:column; gap:18px; }
.bf .cc-addons-sec { background:#fff; border:1px solid var(--hair); border-radius:18px; padding:24px 26px; }
.bf .cc-addons-sec h2 { font-family:'Poppins', sans-serif; font-size:22px; font-weight:700; color:var(--navy); margin:0 0 4px; letter-spacing:-.4px; }
.bf .cc-addons-sub { font-size:14px; color:var(--muted); margin:0 0 20px; }
.bf .cc-addons-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }
.bf .cc-addon-card { border:1.5px solid var(--hair); border-radius:16px; padding:0; cursor:pointer; transition:border-color .2s, box-shadow .25s, transform .2s; text-align:left; background:#fff; width:100%; box-sizing:border-box; overflow:hidden; display:flex; flex-direction:column; }
.bf .cc-addon-card:hover { border-color:var(--teal); box-shadow:0 10px 26px rgba(0,34,68,.13); transform:translateY(-3px); }
.bf .cc-addon-card.is-active { border-color:var(--teal); box-shadow:0 10px 26px rgba(0,181,226,.22); }
.bf .cc-addon-media { position:relative; width:100%; aspect-ratio:16 / 10; background:linear-gradient(135deg,#002244,#00B5E2); overflow:hidden; }
.bf .cc-addon-media img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .4s ease; }
.bf .cc-addon-card:hover .cc-addon-media img { transform:scale(1.05); }
.bf .cc-addon-media::after { content:""; position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,34,68,0) 50%, rgba(0,34,68,.30)); pointer-events:none; }
.bf .cc-addon-badge { position:absolute; top:10px; left:10px; z-index:1; display:inline-flex; align-items:center; gap:5px; background:var(--teal); color:#fff; font-size:11px; font-weight:700; letter-spacing:.2px; padding:5px 10px; border-radius:999px; box-shadow:0 4px 12px rgba(0,34,68,.25); }
.bf .cc-addon-body { padding:14px 16px 16px; display:flex; flex-direction:column; flex:1; }
.bf .cc-addon-title { font-family:'Poppins', sans-serif; font-size:14.5px; font-weight:700; color:var(--navy); margin:0 0 6px; line-height:1.25; }
.bf .cc-addon-desc { font-size:12.5px; color:var(--body); line-height:1.5; margin:0 0 14px; flex:1; }
.bf .cc-addon-footer { display:flex; justify-content:space-between; align-items:center; margin-top:auto; }
.bf .cc-addon-price { font-size:12px; color:var(--muted); font-style:italic; }
.bf .cc-addon-toggle { width:32px; height:32px; border-radius:50%; border:1.5px solid rgba(0,34,68,.18); background:#fff; color:var(--navy); font-size:19px; line-height:1; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; transition:all .18s; padding:0; flex-shrink:0; }
.bf .cc-addon-card:hover .cc-addon-toggle { border-color:var(--teal); color:var(--teal); }
.bf .cc-addon-card.is-active .cc-addon-toggle { background:var(--teal); border-color:var(--teal); color:#fff; font-size:15px; font-weight:700; }
.bf .cc-addons-note { margin-top:14px; background:var(--sea-mist); border-left:3px solid var(--teal); border-radius:10px; padding:12px 14px; font-size:12.5px; color:var(--body); line-height:1.55; }
.bf .cc-addons-sum { margin-top:14px; padding-top:14px; border-top:1px dashed var(--hair); }
.bf .cc-addons-sum-hd { font-size:13px; font-weight:700; color:var(--navy); margin-bottom:6px; }
.bf .cc-addons-sum-row { display:flex; justify-content:space-between; font-size:13px; color:var(--body); padding:3px 0; }
.bf .cc-addons-sum-row .sum-muted { color:var(--muted); }
@media (max-width:960px) { .bf .cc-addons-grid { grid-template-columns:repeat(2,1fr); } }
@media (max-width:600px) { .bf .cc-addons-grid { grid-template-columns:1fr; } .bf .cc-addons-sec { padding:18px; } }
`;
const opStyles = `
/* Mode d'opération */
.bf .cc-op-sec { background:#fff; border:1px solid var(--hair); border-radius:18px; padding:24px 26px; }
.bf .cc-op-sec h2 { font-family:'Poppins', sans-serif; font-size:22px; font-weight:700; color:var(--navy); margin:0 0 4px; letter-spacing:-.4px; }
.bf .cc-op-sub { font-size:14px; color:var(--muted); margin:0 0 20px; }
.bf .cc-op-cards { display:grid; grid-template-columns:repeat(2,1fr); gap:14px; }
.bf .cc-op-card { position:relative; text-align:left; border:1.5px solid var(--hair); border-radius:14px; padding:18px; cursor:pointer; background:#fff; width:100%; box-sizing:border-box; transition:border-color .2s, background .2s, box-shadow .2s; }
.bf .cc-op-card:hover { border-color:var(--teal); box-shadow:0 4px 16px rgba(0,181,226,.12); }
.bf .cc-op-card.is-active { border-color:var(--teal); background:rgba(0,181,226,.07); }
.bf .cc-op-card-head { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; }
.bf .cc-op-title { font-family:'Poppins', sans-serif; font-size:15.5px; font-weight:700; color:var(--navy); margin:0; line-height:1.25; }
.bf .cc-op-badge { font-family:'Inter', sans-serif; font-size:11px; font-weight:700; padding:3px 10px; border-radius:999px; white-space:nowrap; flex-shrink:0; }
.bf .cc-op-badge.is-reco { background:var(--teal); color:#fff; }
.bf .cc-op-badge.is-valid { background:var(--sea-mist); color:var(--navy); border:1px solid var(--hair); }
.bf .cc-op-text { font-size:13px; color:var(--body); line-height:1.55; margin:0 0 10px; }
.bf .cc-op-mention { font-size:12px; color:var(--muted); font-style:italic; margin:0; }
/* Sous-section admissibilité opérateur */
.bf .cc-elig { margin-top:18px; padding-top:18px; border-top:1px dashed var(--hair); }
.bf .cc-elig h3 { font-family:'Poppins', sans-serif; font-size:16px; font-weight:700; color:var(--navy); margin:0 0 8px; }
.bf .cc-elig-intro { font-size:13px; color:var(--body); line-height:1.6; margin:0 0 16px; }
.bf .cc-elig-list { display:flex; flex-direction:column; gap:10px; margin-bottom:18px; }
.bf .cc-elig-check { display:flex; align-items:flex-start; gap:10px; font-size:13.5px; color:var(--navy); line-height:1.45; cursor:pointer; }
.bf .cc-elig-check input { width:18px; height:18px; margin-top:1px; accent-color:var(--teal); cursor:pointer; flex-shrink:0; }
.bf .cc-fields { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.bf .cc-field { display:flex; flex-direction:column; gap:5px; }
.bf .cc-field.is-full { grid-column:1 / -1; }
.bf .cc-field label { font-size:12.5px; font-weight:600; color:var(--navy); }
.bf .cc-field input, .bf .cc-field textarea { font-family:'Inter', sans-serif; font-size:14px; color:var(--navy); border:1.5px solid var(--hair); border-radius:10px; padding:10px 12px; background:#fff; width:100%; box-sizing:border-box; }
.bf .cc-field input:focus, .bf .cc-field textarea:focus { outline:none; border-color:var(--teal); }
.bf .cc-field textarea { resize:vertical; min-height:64px; }
.bf .cc-uploads { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-top:14px; }
.bf .cc-upload { border:1.5px dashed var(--hair); border-radius:12px; padding:18px 14px; text-align:center; color:var(--muted); font-size:13px; background:var(--sea-mist); line-height:1.4; }
.bf .cc-upload .cc-upload-ic { font-size:22px; display:block; margin-bottom:6px; }
.bf .cc-upload .cc-upload-soon { display:inline-block; margin-top:8px; font-size:11px; font-weight:700; color:var(--navy); background:#fff; border:1px solid var(--hair); border-radius:999px; padding:2px 8px; }
/* Note légale + avertissement */
.bf .cc-op-legal { margin-top:18px; background:var(--sea-mist); border-left:3px solid var(--navy); border-radius:10px; padding:14px 16px; font-size:12.5px; color:var(--body); line-height:1.6; }
.bf .cc-op-warning { margin-top:14px; display:flex; align-items:flex-start; gap:10px; background:#fff; border:1.5px solid var(--orange); border-radius:10px; padding:14px 16px; font-size:13.5px; color:var(--navy); font-weight:600; line-height:1.5; }
.bf .cc-op-warning .cc-warn-ic { flex-shrink:0; font-size:16px; line-height:1.3; }
.bf .cc-cta-hint { margin-top:10px; font-size:12px; color:var(--muted); text-align:center; line-height:1.5; }
@media (max-width: 860px) { .bf .cc-op-sec { padding:18px 18px; } }
@media (max-width: 600px) { .bf .cc-op-cards { grid-template-columns:1fr; } .bf .cc-fields { grid-template-columns:1fr; } .bf .cc-uploads { grid-template-columns:1fr; } }
/* ===== Vos informations (demande de réservation V1) ===== */
.bf .cc-info-sec { background:#fff; border:1px solid var(--hair); border-radius:18px; padding:24px 26px; }
.bf .cc-info-sec h2 { font-family:'Poppins', sans-serif; font-size:22px; font-weight:700; color:var(--navy); margin:0 0 4px; letter-spacing:-.4px; }
.bf .cc-req { color:var(--orange); }
.bf .cc-field.is-error input, .bf .cc-field.is-error select, .bf .cc-field.is-error textarea { border-color:#E0322F; background:#FFF6F5; }
.bf .cc-summary .ccs-estimate-note { margin:10px 0 0; font-size:11.5px; color:var(--muted); line-height:1.45; }
.bf .cc-summary .ccs-cta.is-inactive { opacity:.6; box-shadow:none; }
.bf .cc-summary .ccs-cta.is-inactive:hover { background:var(--orange); transform:none; box-shadow:none; }
/* ===== Confirmation d'envoi ===== */
.bf .cc-success { text-align:center; max-width:640px; margin:0 auto; background:#fff; border:1px solid var(--hair); border-radius:22px; padding:48px 32px; box-shadow:0 20px 46px -32px rgba(0,34,68,.4); }
.bf .cc-success-ic { width:72px; height:72px; border-radius:50%; background:rgba(0,181,226,.12); color:var(--teal); display:inline-flex; align-items:center; justify-content:center; font-size:34px; font-weight:800; margin-bottom:20px; }
.bf .cc-success h2 { font-family:'Poppins', sans-serif; font-size:26px; color:var(--navy); margin:0 0 12px; letter-spacing:-.5px; }
.bf .cc-success > p { font-size:15px; color:var(--body); line-height:1.6; margin:0 auto 26px; max-width:520px; }
.bf .cc-success-recap { text-align:left; background:var(--sea-mist); border-radius:14px; padding:18px 20px; margin-bottom:26px; }
.bf .cc-success-recap .sr-row { display:flex; justify-content:space-between; gap:16px; padding:8px 0; font-size:14px; color:var(--body); border-bottom:1px dashed var(--hair); }
.bf .cc-success-recap .sr-row:last-child { border-bottom:none; }
.bf .cc-success-recap .sr-row b { color:var(--navy); font-weight:600; text-align:right; }
.bf .cc-success-actions { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; }
.bf .cc-success-actions a { display:inline-flex; align-items:center; justify-content:center; padding:13px 24px; border-radius:999px; font-family:'Inter', sans-serif; font-weight:600; font-size:14.5px; text-decoration:none; transition:transform .2s, background .2s, color .2s, box-shadow .2s; }
.bf .cc-success-actions .is-primary { background:var(--orange); color:#fff; box-shadow:var(--cta-shadow-orange); }
.bf .cc-success-actions .is-primary:hover { background:#FF9420; transform:translateY(-2px); }
.bf .cc-success-actions .is-secondary { background:#fff; color:var(--navy); border:1.5px solid rgba(0,34,68,.22); }
.bf .cc-success-actions .is-secondary:hover { background:var(--navy); color:#fff; }
@media (max-width: 600px) { .bf .cc-info-sec { padding:18px 18px; } .bf .cc-success { padding:36px 20px; } }
`;
function BoatRow({ b }) {
return (
{b.name}
{b.type}
{b.pers} pers.
{b.region} · {b.sector}{b.marina ? ` · ${b.marina}` : ""}
{blockTotal(b).toLocaleString("fr-CA")} $
Bloc 4h · Dépôt {depositFor(b).toLocaleString("fr-CA")} $
);
}
function AddonCard({ addon, selected, onToggle }) {
return (
onToggle(addon.id)}
aria-pressed={selected ? "true" : "false"}
>
{ e.currentTarget.style.display = "none"; }}
/>
{selected &&
Ajouté ✓ }
{addon.title}
{addon.desc}
Prix sur demande
{selected ? "✓" : "+"}
);
}
function BFPageCheckoutCombine() {
// 1) Priorité URL params, fallback localStorage (cas redirection avec sélection vide)
const url = useMemo(readParams, []);
const fallback = useMemo(() => (url.slugs.length === 0 ? readFallback() : null), [url]);
const [selectedServices, setSelectedServices] = useState([]);
const toggleService = (id) => setSelectedServices(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
// Mode d'opération : "" (à choisir) | "captain" | "self"
const [operationMode, setOperationMode] = useState("");
const [elig, setElig] = useState({});
const toggleElig = (k) => setElig(prev => ({ ...prev, [k]: !prev[k] }));
const [certType, setCertType] = useState("");
const [expYears, setExpYears] = useState("");
const [expDesc, setExpDesc] = useState("");
const allEligChecked = ELIG_ITEMS.every(it => elig[it.k]);
const selfNeedsCaptain = operationMode === "self" && !allEligChecked;
// Mode d'opération valide : capitaine demandé, ou auto-opération avec tous les critères cochés.
const operationReady = operationMode === "captain" || (operationMode === "self" && allEligChecked);
// Réservation simple si ?boat=slug fourni, sinon réservation combinée (≥ 2 bateaux)
const isSingle = !!url.boat;
const slugs = isSingle
? [url.boat]
: (url.slugs.length > 0 ? url.slugs : (fallback?.slugs || []));
const region = url.region || fallback?.ctx?.region || "";
const boats = slugs.map(bySlug).filter(Boolean);
const totalPrice = boats.reduce((sum, b) => sum + blockTotal(b), 0);
const totalDeposit = boats.reduce((sum, b) => sum + depositFor(b), 0);
const navigationZones = Array.from(new Set(boats.map((b) => b.navigationZone).filter(Boolean)));
const zones = commonZones(boats);
const boatNotFound = isSingle && boats.length === 0; // ?boat= invalide
const ready = isSingle ? boats.length >= 1 : boats.length >= 2;
// 2) Détails de sortie éditables (pré-remplis depuis l'URL / la sélection)
const [date, setDate] = useState(url.date || fallback?.ctx?.date || "");
const [timeSlot, setTimeSlot] = useState(url.timeSlot || fallback?.ctx?.timeSlot || "");
const [guests, setGuests] = useState("");
// 3) Informations client (requises pour envoyer la demande)
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [message, setMessage] = useState("");
const [triedSubmit, setTriedSubmit] = useState(false);
const [submitted, setSubmitted] = useState(false);
const maxGuests = boats.length ? (isSingle ? boats[0].pers : boats.reduce((s, b) => s + (b.pers || 0), 0)) : 0;
const slotOptions = (isSingle && boats[0] && boats[0].timeSlots && boats[0].timeSlots.length)
? boats[0].timeSlots
: (TIME_SLOTS || []);
const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
const formValid = !!(
firstName.trim() && lastName.trim() && emailValid &&
phone.trim() && date && timeSlot && String(guests).trim() && Number(guests) >= 1
);
// Envoi autorisé : mode d'opération valide ET informations client complètes.
const canSubmit = operationReady && formValid;
// V1 : regroupe toute la demande dans un objet propre. Aucun paiement, aucun
// appel API. Branchement Zapier / email à faire plus tard (voir handoff).
const handleReservationRequestSubmit = (e) => {
if (e && e.preventDefault) e.preventDefault();
setTriedSubmit(true);
if (!canSubmit) return;
const reservationRequest = {
typeDemande: isSingle ? "reservation-simple" : "reservation-combinee",
bateau: isSingle
? { slug: boats[0].slug, nom: boats[0].name, type: boats[0].type, secteur: boats[0].sector, marina: boats[0].marina }
: boats.map((b) => ({ slug: b.slug, nom: b.name, type: b.type, secteur: b.sector, marina: b.marina })),
date,
plageHoraire: timeSlot,
nombrePersonnes: Number(guests),
modeOperation: operationMode, // "captain" | "self"
operateurAutonome: operationMode === "self"
? { criteres: { ...elig }, typeCertificat: certType, anneesExperience: expYears, descriptionExperience: expDesc }
: null,
servicesAdditionnels: selectedServices
.map((id) => (SERVICES.find((s) => s.id === id) || {}).title)
.filter(Boolean),
client: {
prenom: firstName.trim(),
nom: lastName.trim(),
email: email.trim(),
telephone: phone.trim()
},
message: message.trim(),
totalEstime: totalPrice || null,
depotEstime: totalDeposit || null,
soumisLe: new Date().toISOString()
};
// TODO (post-V1) : envoyer reservationRequest vers Zapier / email au lieu du console.log.
console.log("[BoatsFun] Demande de réservation V1 :", reservationRequest);
setSubmitted(true);
if (typeof window !== "undefined" && window.scrollTo) {
window.scrollTo({ top: 0, behavior: "smooth" });
}
};
return (
{isSingle ? "Réservation" : "Réservation combinée"}
{isSingle ? "Réservez votre sortie" : "Vos bateaux pour une sortie coordonnée"}
{isSingle
? "Vérifiez les détails de votre sortie, ajoutez vos services sur demande et envoyez votre demande de réservation."
: "Vous réservez plusieurs bateaux dans le même secteur pour une sortie coordonnée. Dépôt 100 % remboursable si l'un des propriétaires refuse."}
{submitted ? (
✓
Demande de réservation envoyée
Votre demande de réservation a été envoyée. L'équipe BoatsFun vous contactera rapidement pour confirmer la disponibilité.
{isSingle ? "Bateau" : "Bateaux"} {isSingle ? (boats[0] && boats[0].name) : boats.map((b) => b.name).join(", ")}
Date {formatDate(date)}
Plage horaire {timeSlot || "—"}
Personnes {guests || "—"}
Mode d'opération {operationMode === "captain" ? "Capitaine demandé" : "Opération autonome — sur validation"}
Contact {firstName} {lastName}
) : !ready ? (
{boatNotFound ? (
Bateau introuvable
Bateau introuvable. Retournez au catalogue pour choisir un bateau.
Retour au catalogue
) : (
Aucune sélection trouvée
Retournez au catalogue, activez le mode Combiner plusieurs bateaux et sélectionnez au moins 2 bateaux compatibles.
Retour au catalogue
)}
) : (
{/* Colonne gauche : bateaux + add-ons */}
{isSingle ? "Votre bateau" : `${boats.length} bateaux sélectionnés`}
{boats.map((b) => )}
Comment souhaitez-vous organiser votre sortie ?
Choisissez comment votre bateau sera opéré pendant la sortie.
setOperationMode("captain")}
>
Demander un capitaine
Recommandé
Profitez de votre sortie sans gérer la navigation. Cette option est recommandée pour la majorité des groupes.
Prix à confirmer selon le bateau, la durée et la disponibilité.
setOperationMode("self")}
>
Opérer le bateau moi-même
Sur validation
Disponible uniquement si vous répondez aux critères d'expérience, de compétence et de validation requis.
Documents et informations additionnels requis.
{operationMode === "self" && (
Vérification d'admissibilité opérateur
Pour des raisons de sécurité, d'assurance et de conformité, les demandes sans capitaine sont sujettes à validation. BoatsFun peut refuser une demande autonome si les documents ou l'expérience fournis ne sont pas suffisants.
{ELIG_ITEMS.map(it => (
toggleElig(it.k)} />
{it.label}
))}
📄
Preuve de compétence
Téléversement bientôt disponible
🪪
Pièce d'identité
Téléversement bientôt disponible
{selfNeedsCaptain && (
⚠️
Pour continuer, veuillez choisir « Demander un capitaine » ou compléter tous les critères d'admissibilité pour une demande autonome.
)}
)}
Le choix d'opérer le bateau soi-même ne garantit pas l'approbation de la réservation. BoatsFun, le propriétaire ou leurs représentants peuvent demander des documents additionnels, refuser une demande autonome ou recommander un capitaine selon le bateau, l'expérience déclarée, la météo, le lieu de départ ou les exigences applicables.
Services additionnels
Tous sur demande — prix à confirmer.
Ces services sont sur demande. Notre équipe confirmera la disponibilité et le tarif après la demande de réservation.
Vos informations
Pour vous recontacter et confirmer la disponibilité. Aucun paiement n'est requis à cette étape.
{triedSubmit && !formValid && (
⚠️
Veuillez remplir tous les champs requis (*) avec une adresse email valide.
)}
{/* Colonne droite : récap + demande */}
{isSingle ? "Votre réservation" : "Votre sortie"}
{isSingle ? (
Bateau {boats[0].name}
{date && Date {formatDate(date)}
}
{timeSlot && Plage horaire {timeSlot}
}
{guests && Personnes {guests}
}
Durée {boats[0].durationHours || 4} heures
) : (
Date {formatDate(date)}
Plage horaire {timeSlot || "Non renseignée"}
Région {region || boats[0]?.region || "—"}
Zone de navigation {navigationZones.join(", ") || "—"}
{zones.length > 0 && (
Point de rendez-vous {zones[0].replace(/-/g, " ")}
)}
)}
Mode d'opération
{operationMode === "captain" ? "Capitaine demandé" : operationMode === "self" ? "Opération autonome — sur validation" : "À choisir"}
Services demandés
{selectedServices.length > 0 ? (
selectedServices.map(id => {
const a = SERVICES.find(s => s.id === id);
return (
{a.title}
Sur demande
);
})
) : (
Aucun service additionnel sélectionné.
)}
{isSingle ? "Prix estimé du bloc (4h)" : "Prix total estimé"} {totalPrice} $
{isSingle ? "Dépôt estimé (25 %)" : "Dépôt total estimé"} {totalDeposit} $
Estimations hors taxes. Aucun montant n'est prélevé maintenant.
Envoyer la demande de réservation
{!canSubmit && (
{!operationReady
? (operationMode === "self"
? "Cochez tous les critères d'admissibilité, ou demandez un capitaine, pour continuer."
: "Choisissez un mode d'opération pour continuer.")
: "Complétez vos informations pour envoyer la demande."}
)}
Aucun paiement maintenant
{isSingle
? "L'envoi de cette demande est gratuit et ne réserve pas encore le bateau. Les montants ci-dessus sont des estimations. L'équipe BoatsFun confirme d'abord la disponibilité ; le dépôt et le solde sont traités plus tard, selon les conditions de réservation."
: "L'envoi de cette demande est gratuit et ne réserve pas encore les bateaux. Les montants ci-dessus sont des estimations. L'équipe BoatsFun confirme d'abord la disponibilité auprès de chaque propriétaire ; le dépôt et le solde sont traités plus tard, selon les conditions de réservation."}
)}
);
}
window.BFPageCheckoutCombine = BFPageCheckoutCombine;
})();