// appointment.jsx — multi-step private appointment booking + lookbook lightbox
const { useState, useEffect, useMemo, useRef } = React;
// ─── APPOINTMENT MODAL ───────────────────────────────────────────────────────
const STEPS = ['Atelier', 'Occasion', 'Date', 'Details', 'Confirm'];
function AppointmentModal({ open, onClose }) {
const [step, setStep] = useState(0);
const [data, setData] = useState({
atelier: '',
occasion: '',
date: null, // { y, m, d }
name: '',
email: '',
phone: '',
message: '',
});
const [submitted, setSubmitted] = useState(false);
const scrollRef = useRef(null);
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
// reset on close after small delay (so closing transitions feel right)
const t = setTimeout(() => {
setStep(0);
setSubmitted(false);
}, 400);
return () => clearTimeout(t);
}
}, [open]);
useEffect(() => {
// ESC closes
if (!open) return;
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
useEffect(() => {
if (scrollRef.current) scrollRef.current.scrollTop = 0;
}, [step]);
if (!open) return null;
const canNext = (() => {
if (step === 0) return !!data.atelier;
if (step === 1) return !!data.occasion;
if (step === 2) return !!data.date;
if (step === 3) return data.name.trim() && /.+@.+\..+/.test(data.email);
return true;
})();
const next = () => setStep((s) => Math.min(s + 1, STEPS.length - 1));
const back = () => setStep((s) => Math.max(s - 1, 0));
const formatDate = (d) => {
if (!d) return '';
const date = new Date(d.y, d.m, d.d);
return date.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
};
const submit = () => {
setSubmitted(true);
next();
// open mailto in new tab as a backup courtesy — non-blocking
try {
const body = [
`Name: ${data.name}`,
`Email: ${data.email}`,
`Phone: ${data.phone}`,
``,
`Atelier: ${data.atelier}`,
`Occasion: ${data.occasion}`,
`Preferred date: ${formatDate(data.date)}`,
``,
`Message:`,
data.message || '—',
].join('\n');
const subject = `Private appointment — ${data.atelier} · ${data.occasion}`;
const href = `mailto:lamiaabinader@hotmail.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
// store on confirm button for user to copy/open
window.__lan_mailto = href;
} catch (e) {}
};
return (
Close
×
{/* progress */}
{STEPS.map((s, i) => (
))}
Step {Math.min(step + 1, STEPS.length)} / {STEPS.length}
{STEPS[step]}
{/* step body */}
{step === 0 && }
{step === 1 && }
{step === 2 && }
{step === 3 && }
{step === 4 && }
{/* nav */}
{step < 4 && (
← Back
{step === 3 ? 'Send Request' : 'Continue'}
→
)}
{step === 4 && (
)}
);
}
const overlayStyle = {
position: 'fixed', inset: 0, zIndex: 100,
display: 'grid', placeItems: 'center',
padding: 'clamp(12px, 4vw, 40px)',
};
const overlayBackdrop = {
position: 'absolute', inset: 0,
background: 'color-mix(in oklab, var(--ink) 75%, transparent)',
backdropFilter: 'blur(8px)',
animation: 'fadeIn 300ms ease',
};
const modalShell = {
position: 'relative',
width: 'min(1100px, 100%)',
display: 'grid',
animation: 'modalIn 500ms cubic-bezier(.2,.7,.2,1)',
};
const closeBtnStyle = {
position: 'absolute', top: -36, right: 0,
display: 'inline-flex', alignItems: 'center',
color: 'rgba(255,255,255,0.9)',
fontFamily: 'var(--sans)', fontSize: 10.5, letterSpacing: '0.22em', textTransform: 'uppercase',
cursor: 'default',
};
// inject modal keyframes once
if (!document.getElementById('__appt_kfs')) {
const s = document.createElement('style');
s.id = '__appt_kfs';
s.textContent = `
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
@keyframes modalIn { from{opacity:0; transform: translateY(18px)} to{opacity:1; transform:none} }
.appt-card { animation: fadeIn 400ms ease 80ms both; }
`;
document.head.appendChild(s);
}
// ─── STEP 1 — ATELIER ────────────────────────────────────────────────────────
const ATELIERS = [
{ id: 'Dubai', t: 'Dubai', s: 'd3 — Dubai Design District. Bridal & evening fittings.', i: 'i' },
{ id: 'Beirut', t: 'Beirut', s: 'Jisr El Basha, Mkalles. The original maison.', i: 'ii' },
];
function StepAtelier({ data, setData }) {
return (
<>
Where would you like to be seen ?
Choose the atelier nearest you. Travel between Beirut and Dubai is possible on request.
{ATELIERS.map((a) => (
setData((d) => ({ ...d, atelier: a.id }))}
className={`opt-card ${data.atelier === a.id ? 'is-selected' : ''}`}
>
Atelier {a.i}
{a.t}
))}
>
);
}
// ─── STEP 2 — OCCASION ───────────────────────────────────────────────────────
const OCCASIONS = [
{ id: 'Bridal', t: 'Bridal', s: 'Wedding gown, civil dress, post-ceremony look.' },
{ id: 'Evening', t: 'Evening', s: 'Gala, opera, première, state ceremony.' },
{ id: 'Other', t: 'Other', s: 'Mother of the bride, debutante, private commission.' },
];
function StepOccasion({ data, setData }) {
return (
<>
What is the occasion ?
The atelier shapes the appointment around the commission. You can change this at any time.
{OCCASIONS.map((o, idx) => (
setData((d) => ({ ...d, occasion: o.id }))}
className={`opt-card ${data.occasion === o.id ? 'is-selected' : ''}`}
>
N° {String(idx + 1).padStart(2, '0')}
{o.t}
{o.s}
))}
>
);
}
// ─── STEP 3 — DATE ───────────────────────────────────────────────────────────
function StepDate({ data, setData }) {
const today = new Date(); today.setHours(0,0,0,0);
const [view, setView] = useState(() => ({ y: today.getFullYear(), m: today.getMonth() }));
const monthName = useMemo(() => {
return new Date(view.y, view.m, 1).toLocaleDateString('en-GB', { month: 'long', year: 'numeric' });
}, [view]);
const cells = useMemo(() => {
const first = new Date(view.y, view.m, 1);
const lastDay = new Date(view.y, view.m + 1, 0).getDate();
// start grid on Mon
const dow = (first.getDay() + 6) % 7;
const arr = [];
for (let i = 0; i < dow; i++) arr.push(null);
for (let d = 1; d <= lastDay; d++) arr.push(d);
return arr;
}, [view]);
const prev = () => setView((v) => {
const nm = v.m - 1;
return nm < 0 ? { y: v.y - 1, m: 11 } : { y: v.y, m: nm };
});
const next = () => setView((v) => {
const nm = v.m + 1;
return nm > 11 ? { y: v.y + 1, m: 0 } : { y: v.y, m: nm };
});
const isPast = (d) => {
const dt = new Date(view.y, view.m, d);
return dt < today;
};
const sel = data.date;
const isSelected = (d) => sel && sel.y === view.y && sel.m === view.m && sel.d === d;
return (
<>
A preferred date .
The atelier will confirm a precise time after your request is received.
‹
{monthName}
›
{['Mo','Tu','We','Th','Fr','Sa','Su'].map((d) =>
{d}
)}
{cells.map((d, i) => {
if (!d) return
;
const past = isPast(d);
const sel = isSelected(d);
return (
!past && setData((dt) => ({ ...dt, date: { y: view.y, m: view.m, d } }))}
>
{d}
);
})}
{data.date && (
Requested · {new Date(data.date.y, data.date.m, data.date.d).toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}
)}
>
);
}
// ─── STEP 4 — DETAILS ────────────────────────────────────────────────────────
function StepDetails({ data, setData }) {
const upd = (k) => (e) => setData((d) => ({ ...d, [k]: e.target.value }));
return (
<>
Your details .
Shared in confidence with the atelier. We respond personally to every enquiry.
>
);
}
// ─── STEP 5 — CONFIRM ────────────────────────────────────────────────────────
function StepConfirm({ data, formatDate }) {
return (
Request received
Merci — nous reviendrons .
Your request has been sent to the atelier. A member of the maison will contact you at {data.email || 'your email'} to confirm time and details, typically within two working days.
Atelier {data.atelier}
Occasion {data.occasion}
Requested {formatDate(data.date)}
Name {data.name}
{data.phone &&
Phone {data.phone}
}
All appointments are private — by invitation. Price on request.
);
}
// ─── LOOKBOOK LIGHTBOX ───────────────────────────────────────────────────────
function LookLightbox({ look, onClose, onPrev, onNext }) {
useEffect(() => {
if (!look) return;
document.body.style.overflow = 'hidden';
const onKey = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft') onPrev();
if (e.key === 'ArrowRight') onNext();
};
window.addEventListener('keydown', onKey);
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', onKey);
};
}, [look, onClose, onPrev, onNext]);
if (!look) return null;
return (
Close ×
‹ Prev
Next ›
{look.col} · N° {look.n}
{look.name}
);
}
Object.assign(window, { AppointmentModal, LookLightbox });