// ===================================================================== // Art&Suites — Liste d'attente · logique du formulaire (React) // ===================================================================== // // WEBHOOK MAKE — module « Custom webhook » du scénario. // Le formulaire POSTe le payload JSON ci-dessous à cette adresse. // const WEBHOOK_URL = "https://hook.eu2.make.com/3ordu80g2n0dth071bhpkttkez6qqlkt"; // --------------------------------------------------------------------- // Suites // --------------------------------------------------------------------- const SUITES = [ { id: "sarah", caption: { fr: "Belle Époque Narbonne", en: "Belle Époque Narbonne", es: "Belle Époque Narbonne" }, name: "Sarah Bernhardt", meta: { fr: "80 m² · 1 à 6 pers.", en: "80 m² · 1–6 guests", es: "80 m² · 1 a 6 huésp." }, price: 80, swatch: "linear-gradient(150deg, #E2B791 0%, #C99873 55%, #8C6240 100%)", image: "assets/suite-sarah.webp", }, { id: "frida", caption: { fr: "Casa Azul Narbonne", en: "Casa Azul Narbonne", es: "Casa Azul Narbonne" }, name: "Frida Kahlo", meta: { fr: "42 m² · 1 à 4 pers.", en: "42 m² · 1–4 guests", es: "42 m² · 1 a 4 huésp." }, price: 75, swatch: "linear-gradient(150deg, #F5BFB4 0%, #F51549 60%, #8E0A29 100%)", image: "assets/suite-frida.webp", }, { id: "boheme", caption: { fr: "Portanelle Millénaire Gaillac", en: "Portanelle Millénaire Gaillac", es: "Portanelle Millénaire Gaillac" }, name: "La Bohème Saint Michel", meta: { fr: "75 m² · 1 à 5 pers.", en: "75 m² · 1–5 guests", es: "75 m² · 1 a 5 huésp." }, price: 70, swatch: "linear-gradient(150deg, #FCF2E6 0%, #E2B791 65%, #886040 100%)", image: "assets/suite-boheme.webp", }, ]; // --------------------------------------------------------------------- // i18n // --------------------------------------------------------------------- const T = { fr: { nav: "Les suites", eyebrow: "Liste d'attente", title: "Si une suite se libère, vous le saurez avant tout le monde !", lead: "Vos dates sont déjà complètes ?\nDès qu'une suite correspondant à vos attentes se libère, nous vous écrivons, souvent avant qu'elle ne reparaisse en ligne.", suiteQ: "La suite qui vous fait rêver", suiteHelp: "", perNight: "/ nuit", name: "Nom complet", namePh: "Camille Laurent", email: "Adresse e-mail", emailPh: "camille@exemple.fr", mobile: "Téléphone mobile", mobilePh: "06 12 34 56 78", mobileHelp: "Pour vous joindre vite si la suite se libère.", periodQ: "Période souhaitée", checkin: "Arrivée", checkout: "Départ", guests: "Nombre de personnes", guestUnit: ["personne", "personnes"], guestPlus: "6 personnes ou plus", rgpd: "J'accepte qu'Art\u0026Suites conserve mes coordonnées afin de me prévenir d'une disponibilité. Elles ne seront ni revendues, ni utilisées à d'autres fins.", submit: "Rejoindre la liste", sending: "Envoi…", reassure: "Sans engagement · Vous pouvez nous demander de vous retirer de la liste à tout moment.", errReq: "Ce champ est requis.", errEmail: "Cette adresse e-mail semble incomplète.", errSuite: "Choisissez une suite.", errConsent: "Merci de cocher cette case pour continuer.", errDates: "Le départ doit être après l'arrivée.", }, en: { nav: "The suites", eyebrow: "Waitlist", title: "When a suite opens up, you'll be the first to know.", lead: "Are your dates already booked? Leave us a few words. The moment a suite matching your stay frees up, we write to you — often before it reappears online.", suiteQ: "The suite you're dreaming of", suiteHelp: "", perNight: "/ night", name: "Full name", namePh: "Camille Laurent", email: "Email address", emailPh: "camille@example.com", mobile: "Mobile phone", mobilePh: "+33 6 12 34 56 78", mobileHelp: "So we can reach you quickly if it opens up.", periodQ: "Preferred dates", checkin: "Check-in", checkout: "Check-out", guests: "Number of guests", guestUnit: ["guest", "guests"], guestPlus: "6 guests or more", rgpd: "I agree that Art\u0026Suites may keep my details to notify me of availability. They will never be sold or used for any other purpose.", submit: "Join the list", sending: "Sending…", reassure: "No commitment · You can ask to be removed from the list at any time.", errReq: "This field is required.", errEmail: "This email address looks incomplete.", errSuite: "Please choose a suite.", errConsent: "Please tick this box to continue.", errDates: "Check-out must be after check-in.", }, es: { nav: "Las suites", eyebrow: "Lista de espera", title: "Cuando una suite se libere, será el primero en saberlo.", lead: "¿Sus fechas ya están completas? Déjenos unas palabras. En cuanto se libere una suite para su estancia, le escribimos — a menudo antes de que vuelva a aparecer en línea.", suiteQ: "La suite que le seduce", suiteHelp: "", perNight: "/ noche", name: "Nombre completo", namePh: "Camille Laurent", email: "Correo electrónico", emailPh: "camille@ejemplo.com", mobile: "Teléfono móvil", mobilePh: "+34 612 34 56 78", mobileHelp: "Para localizarle rápido si se libera.", periodQ: "Fechas deseadas", checkin: "Llegada", checkout: "Salida", guests: "Número de personas", guestUnit: ["persona", "personas"], guestPlus: "6 personas o más", rgpd: "Acepto que Art\u0026Suites conserve mis datos para avisarme de la disponibilidad. No serán vendidos ni usados para otros fines.", submit: "Unirse a la lista", sending: "Enviando…", reassure: "Sin compromiso · Puede pedir que le retiremos de la lista en cualquier momento.", errReq: "Este campo es obligatorio.", errEmail: "Este correo parece incompleto.", errSuite: "Elija una suite.", errConsent: "Marque esta casilla para continuar.", errDates: "La salida debe ser posterior a la llegada.", }, }; const LANGS = [["fr", "FR"], ["en", "EN"], ["es", "ES"]]; // --------------------------------------------------------------------- // Small pieces // --------------------------------------------------------------------- function CheckIcon({ stroke }) { return ( ); } function Field({ label, help, error, children, htmlFor }) { return (
{children} {help && !error && {help}} {error && {error}}
); } function SuiteTile({ suite, lang, selected, highlight, layout, onSelect }) { return ( ); } // --------------------------------------------------------------------- // App // --------------------------------------------------------------------- const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "lang": "fr", "presentation": "card", "suiteLayout": "list", "highlight": "#449BAA" }/*EDITMODE-END*/; function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const lang = T[t.lang] ? t.lang : "fr"; const tr = T[lang]; const [form, setForm] = React.useState({ suite: "", name: "", email: "", mobile: "", checkin: "", checkout: "", guests: "2", consent: false, }); const [errors, setErrors] = React.useState({}); const [sending, setSending] = React.useState(false); const set = (k, v) => setForm(f => ({ ...f, [k]: v })); function validate() { const e = {}; if (!form.suite) e.suite = tr.errSuite; if (!form.name.trim()) e.name = tr.errReq; if (!form.email.trim()) e.email = tr.errReq; else if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(form.email)) e.email = tr.errEmail; if (!form.mobile.trim()) e.mobile = tr.errReq; if (!form.checkin) e.checkin = tr.errReq; if (!form.checkout) e.checkout = tr.errReq; if (form.checkin && form.checkout && form.checkout <= form.checkin) e.checkout = tr.errDates; if (!form.consent) e.consent = tr.errConsent; setErrors(e); return Object.keys(e).length === 0; } async function submit(ev) { ev.preventDefault(); if (!validate()) return; const suiteObj = SUITES.find(s => s.id === form.suite); const payload = { source: "waitlist-art-and-suites", submitted_at: new Date().toISOString(), language: lang, suite_id: form.suite, suite_name: suiteObj ? suiteObj.name : "", full_name: form.name.trim(), email: form.email.trim(), mobile: form.mobile.trim(), check_in: form.checkin, check_out: form.checkout, guests: Number(form.guests), consent: form.consent, }; setSending(true); // Envoi vers Make. On tente d'abord un POST JSON classique ; si le // navigateur bloque (CORS / preflight), on bascule en requête « simple » // (no-cors, text/plain) que Make accepte et parse comme du JSON. Dans // tous les cas on route ensuite le client vers la page de remerciement. const sendOnce = (opts) => fetch(WEBHOOK_URL, { method: "POST", body: JSON.stringify(payload), keepalive: true, ...opts, }); try { await sendOnce({ headers: { "Content-Type": "application/json" }, mode: "cors" }); } catch (err) { console.warn("[waitlist] POST cors bloqué, repli no-cors :", err); try { await sendOnce({ headers: { "Content-Type": "text/plain;charset=UTF-8" }, mode: "no-cors" }); } catch (err2) { console.warn("[waitlist] webhook non joignable :", err2); } } console.log("[waitlist] payload envoyé à Make :", payload); const q = new URLSearchParams({ lang, name: form.name.trim().split(" ")[0] || form.name.trim(), suite: suiteObj ? suiteObj.name : "", }); window.location.href = "Merci.html?" + q.toString(); } const guestOpts = []; for (let i = 1; i <= 5; i++) { guestOpts.push(); } guestOpts.push(); return (
{/* header */}
ART & SUITES
{LANGS.map(([code, lbl]) => ( ))}
{/* intro */}
{tr.eyebrow}

{tr.title}

{/* form */}
{/* suites */}
{tr.suiteQ} {tr.suiteHelp && {tr.suiteHelp}}
{SUITES.map(s => ( { set("suite", id); setErrors(e => ({ ...e, suite: undefined })); }} /> ))}
{errors.suite && {errors.suite}}

{/* identity */} set("name", e.target.value)} autoComplete="name" />
set("email", e.target.value)} autoComplete="email" /> set("mobile", e.target.value)} autoComplete="tel" />
{/* period */}
{tr.periodQ}
set("checkin", e.target.value)} /> set("checkout", e.target.value)} />

{/* consent */} {errors.consent && {errors.consent}}

{tr.reassure}

setTweak("lang", v)} /> setTweak("presentation", v)} /> setTweak("suiteLayout", v)} /> setTweak("highlight", v)} />
); } ReactDOM.createRoot(document.getElementById("root")).render();