/* Sea La Vie — Guest portal UI (account.html). Account helpers + useAccount hook live in data.jsx so they're available site-wide; this file is just the page-specific UI. */ const { useState: useAc, useEffect: useAcEf, useMemo: useAcMm } = React; // ─── UI: HEADER FOR ACCOUNT PAGE ─────────────────────────────────────────── function AccountHeader({ onBack }) { return (
← Back to site
); } // ─── UI: SIGN-IN GATE ────────────────────────────────────────────────────── function SignInGate() { const [mode, setMode] = useAc("signin"); const [form, setForm] = useAc({ name: "", email: "", password: "" }); const [err, setErr] = useAc(""); const [busy, setBusy] = useAc(false); const up = (k, v) => setForm(f => ({ ...f, [k]: v })); async function submit(e) { e.preventDefault(); setBusy(true); setErr(""); const r = mode === "signin" ? await acctLogin({ email: form.email, password: form.password }) : await acctRegister({ name: form.name, email: form.email, password: form.password }); setBusy(false); if (!r.ok) setErr(r.error || "Something went wrong."); // session change triggers a re-render via useAccount } return (
Guest portal

{mode === "signin" ? <>Welcome back : <>Open your chapter}

{mode === "signin" ? "Sign in to see your stays, track your loyalty, and unlock perks reserved for returning guests." : "Create your account in twenty seconds. The first night you book is the start of Karibu."}

{mode === "register" && (
up("name", e.target.value)} autoFocus placeholder="As on your passport" />
)}
up("email", e.target.value)} placeholder="you@example.com" autoFocus={mode === "signin"} />
up("password", e.target.value)} placeholder={mode === "register" ? "Choose a password" : "Your password"} />
{err &&
{err}
}
{mode === "signin" ? ( <>New here? ) : ( <>Already with us? )}
Try the demo:
demo@sealaviediani.com / demo
); } // ─── UI: LOYALTY LADDER ──────────────────────────────────────────────────── function LoyaltyLadder({ nights }) { const current = tierForNights(nights); const next = nextTierFor(nights); return (
Sea La Vie · Loyalty

The deeper you stay, the further we go.

{/* Progress strip */}
{TIERS.map(t => { const reached = nights >= t.minNights; const isCurrent = t.id === current.id; const pos = next ? (t.minNights === 0 ? 0 : t.minNights === 10 ? 50 : 100) : (t.minNights === 0 ? 0 : t.minNights === 10 ? 50 : 100); return (
{t.name}
{t.minNights === 0 ? "from night 1" : `${t.minNights} nights`}
); })}
{/* Status callout */}
Your tier
{current.name} · {current.sub}
Nights with us
{nights}
{next ? "Next tier" : "All unlocked"}
{next ? <> {next.name}{" "} in {next.minNights - nights} night{next.minNights - nights === 1 ? "" : "s"} : You've reached Mwenyeji. Asante sana.}
{/* Tier cards */}
{TIERS.map(t => { const reached = nights >= t.minNights; const isCurrent = t.id === current.id; return (
{t.minNights === 0 ? "From your first night" : `Unlocks at ${t.minNights} nights`}

{t.name}

— {t.sub}
{isCurrent &&
Your tier
} {!reached &&
}

{t.summary}

    {t.perks.map((p, i) => (
  • {p.l}
    {p.s}
  • ))} {Array.from({length: t.hiddenCount}).map((_, i) => (
  • {reached ? A perk discovered on arrival. : A perk you'll discover here, one day.}
    {reached ? "Reserved for your eyes only — your travel curator will share details when you next check in." : "We don't write our best gestures down. They're how we say thank you for staying."}
  • ))}
); })}
); } // ─── UI: STAYS LIST ──────────────────────────────────────────────────────── function StayCard({ b, villa, onCancel }) { const arr = new Date(b.arrival); const dep = new Date(b.departure); const monthName = MONTHS[arr.getMonth()]; const today = new Date(); today.setHours(0,0,0,0); const isPast = b.status === "completed" || dep < today; const canCancel = !isPast && b.status !== "cancelled" && arr > today; const daysUntil = Math.ceil((arr - today) / 86400000); const statusLabel = { confirmed: "Confirmed", held: "Holding · pending confirmation", completed: "Completed", cancelled: "Cancelled", enquiry: "Enquiry" }[b.status] || b.status; return (
{monthName.slice(0,3).toUpperCase()}
{arr.getDate()}
{arr.getFullYear()}
{b.ref}

{villa ? villa.name.join(" ") : (b.villaId || "").toUpperCase()} {villa ? villa.badge : ""}

{fmtDate(arr)} → {fmtDate(dep)} · {b.nights} night{b.nights !== 1 ? "s" : ""} · {b.guests} guest{b.guests !== 1 ? "s" : ""} {!isPast && b.status !== "cancelled" && daysUntil > 0 && ( <> · in {daysUntil} day{daysUntil !== 1 ? "s" : ""} )}
{b.totalUSD ?
USD {b.totalUSD.toLocaleString()}
: null}
{statusLabel}
{canCancel && ( )} {!isPast && b.status !== "cancelled" && ( Manage → )}
); } function StaysSection({ bookings, onCancel }) { const villaIndex = useAcMm(() => { const map = {}; VILLAS.forEach(v => { map[v.id] = v; }); return map; }, []); const sorted = [...(bookings || [])].sort((a, b) => (a.arrival || "").localeCompare(b.arrival || "")); const today = new Date(); today.setHours(0,0,0,0); const upcoming = sorted.filter(b => b.status !== "cancelled" && b.status !== "completed" && new Date(b.departure) >= today); const past = sorted.filter(b => b.status === "completed" || (b.status !== "cancelled" && new Date(b.departure) < today)).reverse(); const cancelled = sorted.filter(b => b.status === "cancelled"); return (
Your stays

Time you've spent at home with us.

{upcoming.length === 0 && past.length === 0 && (
No stays yet

Your first reservation is one click away.

Check availability →
)} {upcoming.length > 0 && ( <>

Upcoming · {upcoming.length}

{upcoming.map(b => )}
)} {past.length > 0 && ( <>

Previous · {past.length}

{past.map(b => )}
)} {cancelled.length > 0 && ( <>

Cancelled · {cancelled.length}

{cancelled.map(b => )}
)}
); } // ─── UI: CANCEL MODAL ───────────────────────────────────────────────────── function CancelModal({ booking, villa, onClose, onConfirm }) { const [busy, setBusy] = useAc(false); const [err, setErr] = useAc(""); if (!booking) return null; const arr = new Date(booking.arrival); const today = new Date(); today.setHours(0,0,0,0); const daysAhead = Math.ceil((arr - today) / 86400000); // Policy tiers, mirroring typical luxury-coast practice. Adjust on the // server if you have a real Ts&Cs document. let refundPct, policyNote; if (daysAhead >= 60) { refundPct = 100; policyNote = "Full refund — you're more than sixty days out."; } else if (daysAhead >= 30) { refundPct = 50; policyNote = "50% refund — between thirty and sixty days before arrival."; } else if (daysAhead >= 14) { refundPct = 25; policyNote = "25% refund — between fourteen and thirty days before arrival."; } else { refundPct = 0; policyNote = "No refund within fourteen days. Your travel curator may still be able to help — reach us first."; } const refundAmt = booking.totalUSD ? Math.round(booking.totalUSD * refundPct / 100) : null; async function confirm() { setBusy(true); setErr(""); const r = await acctCancelBooking({ ref: booking.ref }); setBusy(false); if (!r.ok) { setErr(r.error || "Could not cancel. Please reach us."); return; } onConfirm && onConfirm(); } return (
e.stopPropagation()}>
Cancel reservation

Are you sure you want to release these dates?

Reference{booking.ref}
Residence{villa ? villa.name.join(" ") : booking.villaId}
Arrival{fmtDate(arr)}
Departure{fmtDate(new Date(booking.departure))}
Nights{booking.nights}
Cancellation policy
{refundPct}% refund of {booking.totalUSD ? `USD ${booking.totalUSD.toLocaleString()}` : "reservation"} {refundAmt !== null && refundAmt > 0 && ( ≈ USD {refundAmt.toLocaleString()} )}

{policyNote}

A confirmation will go to {(currentSession() && currentSession().email) || "your email"} within twenty-four hours. Refunds reach the original payment method in five to seven working days.

{err &&
{err}
}

Rather speak to a human? {CONTACT.email} · {CONTACT.phone}

); } // ─── UI: ACCOUNT APP ─────────────────────────────────────────────────────── function AccountApp() { const { session, account, loading, refresh } = useAccount(); const [cancelling, setCancelling] = useAc(null); // booking to cancel if (loading) { return (
Loading…
); } if (!session || !account) { return (
); } const stats = deriveStats(account.bookings); const tier = tierForNights(stats.nights); const next = nextTierFor(stats.nights); const villaIdx = {}; VILLAS.forEach(v => { villaIdx[v.id] = v; }); return (
{/* Hero greeting */}
{tier.name} · {tier.sub}

Karibu, {account.name.split(" ")[0]}.

{stats.nights > 0 ? <>You've spent {stats.nights} night{stats.nights === 1 ? "" : "s"} with us. {next ? <>Another {next.minNights - stats.nights} and {next.name} opens its quieter doors. : <>You've reached every door we have. Asante sana.} : <>Your first stay will begin your Karibu chapter — and start the clock toward Rafiki and Mwenyeji.}

{stats.nights}
Nights
{stats.stays}
Stays
{stats.upcoming}
Upcoming
{tier.name}
Tier
Book another stay
{/* Profile / settings */}
Your details

Profile

Name
{account.name}
Email
{account.email}
Joined
{fmtDate(new Date(account.joinedAt))}
Curator
Asha Mwakio · {CONTACT.email}

To update your name, dietary preferences or stored passport details, write to your curator. We keep this lightweight by design — every Sea La Vie guest has a named human, not a form.

Sea La Vie · Diani Beach

Built and run by a Kenyan family. Reach us.

{cancelling && ( setCancelling(null)} onConfirm={() => { setCancelling(null); refresh(); }} /> )}
); } ReactDOM.createRoot(document.getElementById("root")).render();