/* BoatsFun — Page Catalogue (bateaux.html) Grille complète des bateaux + filtres (recherche texte, type, capacité, fourchette de prix). Consomme window.BFBoats (BOATS, TYPES) et réutilise Header/Footer partagés. */ (function () { const { I, Photo } = window.BF; const { BOATS, TYPES, TIME_SLOTS, getRegions, areCombinable, blockTotal, depositFor, startingPriceLabel } = window.BFBoats; // Tranches de capacité const CAPS = [ { id: "all", label: "Toutes", min: 0, max: 999 }, { id: "1-4", label: "1-4 pers", min: 1, max: 4 }, { id: "5-8", label: "5-8 pers", min: 5, max: 8 }, { id: "9-12", label: "9-12 pers",min: 9, max: 12 }, { id: "13+", label: "13+ pers", min: 13, max: 999 } ]; const SORTS = [ { id: "popular", label: "Recommandés" }, { id: "price-asc", label: "Prix : moins cher au plus cher" }, { id: "price-desc",label: "Prix : plus cher au moins cher" }, { id: "cap-desc", label: "Capacité" } ]; // localStorage key pour persister la sélection combinée entre catalogue ↔ checkout const COMBINE_KEY = "bf.combine.selection"; function loadSelection() { try { return JSON.parse(localStorage.getItem(COMBINE_KEY)) || { slugs: [], ctx: {} }; } catch (e) { return { slugs: [], ctx: {} }; } } function saveSelection(data) { try { localStorage.setItem(COMBINE_KEY, JSON.stringify(data)); } catch (e) {} } const styles = ` .bf .cat-hero { background:linear-gradient(180deg, #002244 0%, #003a66 100%); color:#fff; padding:64px 0 56px; } .bf .cat-hero .eyebrow { color:var(--teal); } .bf .cat-hero h1 { font-family:'Poppins', sans-serif; font-size:48px; line-height:1.1; font-weight:800; color:#fff; margin:14px 0 14px; letter-spacing:-1.2px; } .bf .cat-hero p.lead { font-size:17px; line-height:1.55; color:rgba(255,255,255,.82); max-width:640px; } .bf .cat-hero .ch-meta { display:flex; gap:18px; margin-top:22px; flex-wrap:wrap; color:rgba(255,255,255,.7); font-size:13.5px; } .bf .cat-hero .ch-meta span { display:inline-flex; align-items:center; gap:8px; } .bf .cat-hero .ch-meta svg { color:var(--teal); width:16px; height:16px; } /* Bloc filtres */ .bf .cat-filters { background:#fff; border:1px solid var(--hair); border-radius:18px; box-shadow:0 8px 24px -16px rgba(0,34,68,.18); padding:18px 20px; margin-top:-36px; position:relative; z-index:2; } .bf .cf-row { display:grid; grid-template-columns: 1.6fr 1fr 1fr 1fr 1fr; gap:14px; align-items:end; } .bf .cf-row-2 { display:grid; grid-template-columns: 1fr auto; gap:14px; align-items:center; margin-top:14px; } .bf .cf-field { display:flex; flex-direction:column; gap:6px; min-width:0; } .bf .cf-field label { font-family:'Inter', sans-serif; font-size:11.5px; font-weight:700; letter-spacing:1.4px; text-transform:uppercase; color:var(--muted); } .bf .cf-search { display:flex; align-items:center; gap:10px; background:var(--sea-mist); border:1px solid var(--hair); border-radius:12px; padding:10px 14px; } .bf .cf-search svg { color:var(--navy); width:18px; height:18px; flex-shrink:0; } .bf .cf-search input { flex:1; background:transparent; border:none; outline:none; font-family:'Inter', sans-serif; font-size:14.5px; color:var(--navy); min-width:0; } .bf .cf-search input::placeholder { color:var(--muted); } .bf .cf-select { background:var(--sea-mist); border:1px solid var(--hair); border-radius:12px; padding:11px 14px; font-family:'Inter', sans-serif; font-size:14px; color:var(--navy); font-weight:500; outline:none; cursor:pointer; appearance:none; background-image:url("data:image/svg+xml;utf8,"); background-repeat:no-repeat; background-position:right 14px center; padding-right:38px; } .bf .cf-reset { background:transparent; border:none; color:var(--muted); font-family:'Inter', sans-serif; font-weight:600; font-size:13px; cursor:pointer; padding:12px 4px; white-space:nowrap; transition:color .15s; } .bf .cf-reset:hover { color:var(--orange); } .bf .cf-input { background:var(--sea-mist); border:1px solid var(--hair); border-radius:12px; padding:11px 14px; font-family:'Inter', sans-serif; font-size:14px; color:var(--navy); font-weight:500; outline:none; width:100%; min-width:0; } .bf .cf-input::placeholder { color:var(--muted); } /* Combiner toggle */ .bf .cf-combine { display:flex; align-items:flex-start; gap:14px; background:linear-gradient(135deg, #002244 0%, #003a66 100%); color:#fff; border-radius:14px; padding:14px 18px; } .bf .cf-combine .ccb-text { flex:1; min-width:0; } .bf .cf-combine .ccb-text strong { display:block; font-family:'Inter', sans-serif; font-weight:700; font-size:14px; letter-spacing:.01em; color:#fff; } .bf .cf-combine .ccb-text span { display:block; font-size:12.5px; color:rgba(255,255,255,.72); margin-top:3px; line-height:1.4; } .bf .cf-combine .ccb-switch { position:relative; width:46px; height:26px; flex-shrink:0; cursor:pointer; } .bf .cf-combine .ccb-switch input { position:absolute; opacity:0; width:0; height:0; } .bf .cf-combine .ccb-track { position:absolute; inset:0; background:rgba(255,255,255,.22); border-radius:999px; transition:background .2s; } .bf .cf-combine .ccb-thumb { position:absolute; top:3px; left:3px; width:20px; height:20px; background:#fff; border-radius:50%; transition:transform .2s; } .bf .cf-combine.is-on .ccb-track { background:var(--orange); } .bf .cf-combine.is-on .ccb-thumb { transform:translateX(20px); } /* Combine bar (sticky bottom) */ .bf .combine-bar { position:fixed; bottom:18px; left:50%; transform:translate(-50%, 120%); z-index:55; background:#002244; color:#fff; border-radius:16px; box-shadow:0 24px 60px -16px rgba(0,34,68,.5); padding:14px 18px; display:flex; align-items:center; gap:20px; min-width:min(680px, calc(100vw - 28px)); max-width:calc(100vw - 28px); transition:transform .3s ease; } .bf .combine-bar.is-visible { transform:translate(-50%, 0); } .bf .combine-bar .cb-info { flex:1; min-width:0; } .bf .combine-bar .cb-count { font-family:'Inter', sans-serif; font-weight:700; font-size:15px; } .bf .combine-bar .cb-count b { color:var(--teal); } .bf .combine-bar .cb-meta { font-family:'Inter', sans-serif; font-size:12.5px; color:rgba(255,255,255,.7); margin-top:2px; } .bf .combine-bar .cb-clear { background:transparent; border:1px solid rgba(255,255,255,.22); color:#fff; font-family:'Inter', sans-serif; font-size:12.5px; font-weight:600; padding:8px 14px; border-radius:999px; cursor:pointer; transition:all .15s; } .bf .combine-bar .cb-clear:hover { background:rgba(255,255,255,.1); } .bf .combine-bar .cb-cta { background:var(--orange); border:none; color:#fff; font-family:'Inter', sans-serif; font-weight:600; font-size:14px; padding:12px 22px; border-radius:999px; cursor:pointer; display:inline-flex; align-items:center; gap:8px; transition:all .2s; box-shadow:var(--cta-shadow-orange); white-space:nowrap; } .bf .combine-bar .cb-cta:hover:not(:disabled) { background:#FF9420; transform:translateY(-2px); } .bf .combine-bar .cb-cta:disabled { opacity:.45; cursor:not-allowed; } /* Carte : bouton combiner + state sélectionné / désactivé */ .bf .cat-card .cc-combine-btn { width:100%; margin-top:10px; background:#fff; border:1.5px solid var(--navy); color:var(--navy); font-family:'Inter', sans-serif; font-weight:600; font-size:13px; padding:9px 12px; border-radius:999px; cursor:pointer; transition:all .18s; display:inline-flex; align-items:center; justify-content:center; gap:6px; } .bf .cat-card .cc-combine-btn:hover { background:var(--navy); color:#fff; } .bf .cat-card.is-selected { border-color:var(--orange); box-shadow:0 0 0 2px var(--orange) inset, 0 18px 40px -20px rgba(255,130,0,.35); } .bf .cat-card.is-selected .cc-combine-btn { background:var(--orange); border-color:var(--orange); color:#fff; } .bf .cat-card.is-incompatible { opacity:.55; } .bf .cat-card.is-incompatible .cc-combine-btn { background:transparent; border-color:var(--hair); color:var(--muted); cursor:not-allowed; } .bf .cat-card.is-incompatible .cc-combine-btn:hover { background:transparent; color:var(--muted); } .bf .cat-card .cc-incompatible-note { font-size:11.5px; color:var(--muted); margin-top:6px; font-style:italic; } /* Pills filtres (type) */ .bf .cat-pills { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; padding-top:14px; border-top:1px dashed var(--hair); } .bf .cat-pill { background:#fff; border:1px solid var(--hair); border-radius:999px; padding:9px 18px; font-family:'Inter', sans-serif; font-size:13.5px; font-weight:600; color:var(--navy); cursor:pointer; transition:all .18s ease; } .bf .cat-pill:hover { border-color:var(--navy); } .bf .cat-pill.active { background:var(--navy); color:#fff; border-color:var(--navy); } /* Toolbar : résultats + tri */ .bf .cat-toolbar { display:flex; justify-content:space-between; align-items:center; margin-top:32px; margin-bottom:18px; gap:14px; flex-wrap:wrap; } .bf .cat-toolbar .ct-count { font-family:'Inter', sans-serif; font-size:14.5px; color:var(--body); } .bf .cat-toolbar .ct-count b { color:var(--navy); font-weight:700; } .bf .cat-toolbar .ct-sort { display:flex; align-items:center; gap:10px; font-family:'Inter', sans-serif; font-size:13.5px; color:var(--muted); } /* Grille bateaux */ .bf .cat-section { padding:0 0 var(--section-pad); } .bf .cat-grid { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:22px; } .bf .cat-card { display:flex; flex-direction:column; background:#fff; border:1px solid var(--hair); border-radius:18px; overflow:hidden; text-decoration:none; color:inherit; transition:transform .25s ease, box-shadow .25s ease, border-color .25s ease; } .bf .cat-card:hover { transform:translateY(-4px); box-shadow:0 18px 40px -20px rgba(0,34,68,.22); border-color:transparent; } .bf .cat-card .cc-imgwrap { position:relative; aspect-ratio:16/11; overflow:hidden; background:var(--sea-mist); } .bf .cat-card .cc-imgwrap img { width:100%; height:100%; object-fit:cover; display:block; transition:transform .5s ease; } .bf .cat-card:hover .cc-imgwrap img { transform:scale(1.05); } .bf .cat-card .cc-badges { position:absolute; top:12px; left:12px; display:flex; gap:6px; flex-wrap:wrap; } .bf .cat-card .cc-badge { background:#fff; color:var(--navy); font-family:'Inter', sans-serif; font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; padding:5px 10px; border-radius:999px; box-shadow:0 2px 6px rgba(0,34,68,.15); } .bf .cat-card .cc-badge.is-orange { background:var(--orange); color:#fff; } .bf .cat-card .cc-type { position:absolute; bottom:12px; right:12px; background:rgba(0,34,68,.85); color:#fff; backdrop-filter:blur(8px); font-family:'Inter', sans-serif; font-size:11.5px; font-weight:600; padding:5px 11px; border-radius:999px; } .bf .cat-card .cc-body { padding:18px 20px 20px; display:flex; flex-direction:column; gap:8px; flex:1; } .bf .cat-card h3 { font-family:'Poppins', sans-serif; font-size:18px; font-weight:700; color:var(--navy); line-height:1.25; letter-spacing:-.3px; } .bf .cat-card .cc-loc { display:inline-flex; align-items:center; gap:6px; font-size:13.5px; color:var(--body); } .bf .cat-card .cc-loc svg { width:14px; height:14px; color:var(--teal); } .bf .cat-card .cc-meta { display:flex; gap:14px; margin-top:4px; font-size:13px; color:var(--muted); } .bf .cat-card .cc-meta span { display:inline-flex; align-items:center; gap:5px; } .bf .cat-card .cc-meta svg { width:13px; height:13px; } .bf .cat-card .cc-foot { display:flex; justify-content:space-between; align-items:center; margin-top:auto; padding-top:14px; border-top:1px dashed var(--hair); } .bf .cat-card .cc-price { font-family:'Poppins', sans-serif; font-size:20px; font-weight:700; color:var(--navy); } .bf .cat-card .cc-price .pu { font-family:'Inter', sans-serif; font-size:12.5px; font-weight:500; color:var(--muted); margin-left:3px; } .bf .cat-card .cc-cta { font-family:'Inter', sans-serif; font-size:13px; font-weight:600; color:var(--orange); display:inline-flex; align-items:center; gap:6px; transition:gap .2s ease; } .bf .cat-card:hover .cc-cta { gap:10px; } .bf .cat-card .cc-cta svg { width:14px; height:14px; } /* Empty state */ .bf .cat-empty { text-align:center; padding:80px 24px; background:#fff; border:1px dashed var(--hair); border-radius:18px; } .bf .cat-empty h3 { font-family:'Poppins', sans-serif; font-size:22px; color:var(--navy); margin-bottom:8px; } .bf .cat-empty p { color:var(--body); margin-bottom:18px; } /* ============ Responsive ============ */ @media (max-width: 1180px) { .bf .cat-grid { grid-template-columns:repeat(2, minmax(0, 1fr)); } } @media (max-width: 1180px) { .bf .cf-row { grid-template-columns: 1fr 1fr 1fr; } .bf .cf-row .cf-search-field { grid-column: 1 / -1; } } @media (max-width: 1024px) { .bf .cat-hero h1 { font-size:40px; } .bf .cf-row { grid-template-columns: 1fr 1fr; } .bf .cf-row-2 { grid-template-columns: 1fr; } } @media (max-width: 860px) { .bf .cat-hero { padding:48px 0 56px; } .bf .cat-hero h1 { font-size:32px; } .bf .cat-hero p.lead { font-size:15.5px; } .bf .cat-filters { margin-top:-28px; padding:16px; } .bf .cf-row { grid-template-columns: 1fr; gap:12px; } .bf .cat-pill { padding:8px 14px; font-size:13px; } .bf .cat-grid { grid-template-columns:1fr; gap:18px; } .bf .cat-toolbar { margin-top:24px; } .bf .combine-bar { left:14px; right:14px; transform:translate(0, 120%); min-width:0; flex-direction:column; align-items:stretch; gap:12px; padding:14px; } .bf .combine-bar.is-visible { transform:translate(0, 0); } .bf .combine-bar .cb-cta { width:100%; justify-content:center; } .bf .combine-bar .cb-clear { align-self:flex-end; } } @media (max-width: 500px) { .bf .cat-hero h1 { font-size:27px; } .bf .cat-hero p.lead { font-size:14.5px; } .bf .cat-card .cc-body { padding:16px; } .bf .cat-card h3 { font-size:16.5px; } } `; function BoatCard({ b, combineMode, isSelected, isCompatible, incompatibleReason, onToggleSelect }) { const href = `/bateau?id=${encodeURIComponent(b.slug)}`; const cardClass = [ "cat-card", combineMode && isSelected ? "is-selected" : "", combineMode && !isCompatible ? "is-incompatible" : "" ].filter(Boolean).join(" "); const handleCombineClick = (e) => { e.preventDefault(); e.stopPropagation(); if (!isCompatible) return; onToggleSelect(b.slug); }; return (
{b.name} {b.type}

{b.name}

{b.loc}
{b.pers} pers. {b.length_ft} pi {b.captain && Capitaine sur demande}
{startingPriceLabel(b.lowestPricePerHour)} Voir
{combineMode && ( {!isCompatible && incompatibleReason && ( {incompatibleReason} )} )}
); } // Lecture des paramètres URL (homepage redirige avec ?region=...&date=...&slot=...&combine=1) function readQueryParams() { if (typeof window === "undefined") return {}; const p = new URLSearchParams(window.location.search); return { region: p.get("region") || "", cap: p.get("cap") || "all", date: p.get("date") || "", timeSlot: p.get("slot") || "", combine: p.get("combine") === "1" }; } function BFPageCatalogue() { const initial = React.useMemo(readQueryParams, []); const regions = React.useMemo(() => getRegions(), []); const [type, setType] = React.useState("Tous"); const [region, setRegion] = React.useState(initial.region || ""); const [cap, setCap] = React.useState(initial.cap || "all"); const [date, setDate] = React.useState(initial.date || ""); const [timeSlot, setTimeSlot] = React.useState(initial.timeSlot || ""); const [combine, setCombine] = React.useState(!!initial.combine); const [sort, setSort] = React.useState("popular"); const [selectedSlugs, setSelectedSlugs] = React.useState(() => { const s = loadSelection(); return Array.isArray(s.slugs) ? s.slugs : []; }); // Persiste la sélection + contexte (date / plage / région) pour la page checkout combinée React.useEffect(() => { saveSelection({ slugs: selectedSlugs, ctx: { region, date, timeSlot } }); }, [selectedSlugs, region, date, timeSlot]); // Reset sélection quand on quitte le mode combine React.useEffect(() => { if (!combine && selectedSlugs.length > 0) { setSelectedSlugs([]); } }, [combine]); const filtered = React.useMemo(() => { let list = BOATS.slice(); if (type !== "Tous") list = list.filter((b) => b.type === type); if (region) list = list.filter((b) => b.region === region); const capRule = CAPS.find((c) => c.id === cap); if (capRule && cap !== "all") list = list.filter((b) => b.pers >= capRule.min && b.pers <= capRule.max); if (timeSlot) list = list.filter((b) => (b.timeSlots || []).includes(timeSlot)); // En mode combine : ne montrer que les bateaux éligibles à la combinaison if (combine) list = list.filter((b) => b.canCombineWithOtherBoats); if (sort === "price-asc") list.sort((a, b) => a.lowestPricePerHour - b.lowestPricePerHour); if (sort === "price-desc") list.sort((a, b) => b.lowestPricePerHour - a.lowestPricePerHour); if (sort === "cap-desc") list.sort((a, b) => b.pers - a.pers); return list; }, [type, region, cap, timeSlot, sort, combine]); const selectedBoats = React.useMemo( () => selectedSlugs.map((s) => BOATS.find((b) => b.slug === s)).filter(Boolean), [selectedSlugs] ); // Calcule la compatibilité de chaque bateau avec la sélection courante const compatibilityFor = React.useCallback((b) => { if (!combine) return { ok: true }; if (!b.canCombineWithOtherBoats) return { ok: false, reason: "Ce bateau n'accepte pas les sorties combinées." }; if (selectedSlugs.includes(b.slug)) return { ok: true }; // Si rien n'est sélectionné encore, tout candidat est compatible (si region/timeSlot du contexte cadrent) if (selectedBoats.length === 0) { if (region && b.region !== region) return { ok: false, reason: "Région différente de votre filtre." }; if (timeSlot && !(b.timeSlots || []).includes(timeSlot)) return { ok: false, reason: "Plage horaire indisponible." }; return { ok: true }; } // Sinon : doit être compatible avec TOUS les bateaux déjà sélectionnés const ctx = timeSlot ? { timeSlot } : null; const allCompat = selectedBoats.every((s) => areCombinable(s, b, ctx)); if (!allCompat) return { ok: false, reason: "Pas dans la même zone ou plage horaire que la sélection." }; return { ok: true }; }, [combine, selectedSlugs, selectedBoats, region, timeSlot]); const toggleSelect = (slug) => { setSelectedSlugs((prev) => prev.includes(slug) ? prev.filter((s) => s !== slug) : prev.concat(slug)); }; const clearSelection = () => setSelectedSlugs([]); const totalPrice = selectedBoats.reduce((sum, b) => sum + blockTotal(b), 0); const totalDeposit = selectedBoats.reduce((sum, b) => sum + depositFor(b), 0); const reset = () => { setType("Tous"); setRegion(""); setCap("all"); setDate(""); setTimeSlot(""); setCombine(false); setSort("popular"); setSelectedSlugs([]); }; const goCheckoutCombine = (e) => { e.preventDefault(); if (selectedSlugs.length < 2) return; const params = new URLSearchParams(); params.set("slugs", selectedSlugs.join(",")); if (region) params.set("region", region); if (date) params.set("date", date); if (timeSlot) params.set("slot", timeSlot); window.BF.nav(`/checkout-combine?${params.toString()}`); }; return (
{/* HERO */}
Catalogue complet

Trouvez le bateau parfait à Montréal.

Pontons, yachts, voiliers, speedboats. Réservez en quelques clics ou faites une offre — dépôt 100 % remboursable si le propriétaire refuse.

Paiement sécurisé Stripe Dépôt 100 % remboursable Montréal et région
{/* FILTRES */}
setDate(e.target.value)} />
{TYPES.map((t) => ( ))}
{/* TOOLBAR + GRILLE */}
{filtered.length} bateau{filtered.length > 1 ? "x" : ""} disponible{filtered.length > 1 ? "s" : ""} {combine && <> · mode combinaison actif} Trier par
{filtered.length === 0 ? (

Aucun bateau ne correspond à vos critères

Essayez d'élargir votre recherche ou de réinitialiser les filtres.

) : (
{filtered.map((b) => { const compat = compatibilityFor(b); return ( ); })}
)}
{/* COMBINE BAR (sticky bottom) */}
0 ? "is-visible" : ""}`} role="region" aria-label="Réservation combinée">
{selectedSlugs.length} bateau{selectedSlugs.length > 1 ? "x" : ""} sélectionné{selectedSlugs.length > 1 ? "s" : ""}
Total estimé : {totalPrice} $ · Dépôt total : {totalDeposit} $ {selectedSlugs.length < 2 && <> · sélectionnez au moins 2 bateaux}
); } window.BFPageCatalogue = BFPageCatalogue; })();