// 5-step booking overlay
const { useMemo: _useMemo } = React;
const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const DAYS = ["Mo","Tu","We","Th","Fr","Sa","Su"];
function startOfMonth(y, m) { return new Date(y, m, 1); }
function daysInMonth(y, m) { return new Date(y, m + 1, 0).getDate(); }
function fmtDate(d) {
if (!d) return "—";
return d.toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" });
}
function nightsBetween(a, b) {
if (!a || !b) return 0;
return Math.round((b - a) / (1000 * 60 * 60 * 24));
}
function Calendar({ year, month, range, hover, onPick, onHover, onNav }) {
const first = startOfMonth(year, month);
// shift so Monday is first
const startDay = (first.getDay() + 6) % 7;
const days = daysInMonth(year, month);
const cells = [];
for (let i = 0; i < startDay; i++) cells.push(null);
for (let d = 1; d <= days; d++) cells.push(new Date(year, month, d));
const today = new Date(); today.setHours(0,0,0,0);
return (
{MONTHS[month]} {year}
onNav(-1)}>‹
onNav(1)}>›
{DAYS.map(d =>
{d}
)}
{cells.map((d, i) => {
if (!d) return
;
const t = d.getTime();
const disabled = d < today;
let cls = "day";
if (disabled) cls += " disabled";
if (range[0] && range[0].getTime() === t) cls += " start";
if (range[1] && range[1].getTime() === t) cls += " end";
if (range[0] && range[1] && t > range[0].getTime() && t < range[1].getTime()) cls += " in-range";
else if (range[0] && !range[1] && hover && t > range[0].getTime() && t < hover.getTime()) cls += " in-range";
return (
!disabled && onPick(d)}
onMouseEnter={() => !disabled && onHover(d)}
>
{d.getDate()}
);
})}
);
}
function StepIndicator({ step }) {
const steps = ["Dates", "Room", "Extras", "Guest", "Confirm"];
return (
{steps.map((s, i) => (
0{i+1} {s}
{i < steps.length - 1 &&
}
))}
);
}
function StepDates({ state, set }) {
const [calY, setCalY] = useState(state.checkIn ? state.checkIn.getFullYear() : 2026);
const [calM, setCalM] = useState(state.checkIn ? state.checkIn.getMonth() : 6);
const [hover, setHover] = useState(null);
const pick = (d) => {
if (!state.checkIn || (state.checkIn && state.checkOut)) {
set({ checkIn: d, checkOut: null });
} else if (d > state.checkIn) {
set({ checkOut: d });
} else {
set({ checkIn: d, checkOut: null });
}
};
const nextY = calM === 11 ? calY + 1 : calY;
const nextM = (calM + 1) % 12;
const nights = nightsBetween(state.checkIn, state.checkOut);
return (
STEP 01 / 05
When would you like to come?
Pick check-in, then check-out. Minimum stay is two nights in high season.
{
const nm = calM + dir;
if (nm < 0) { setCalY(calY - 1); setCalM(11); }
else if (nm > 11) { setCalY(calY + 1); setCalM(0); }
else setCalM(nm);
}}
/>
{
const nm = calM + dir;
if (nm < 0) { setCalY(calY - 1); setCalM(11); }
else if (nm > 11) { setCalY(calY + 1); setCalM(0); }
else setCalM(nm);
}}
/>
Your stay
Check in
{fmtDate(state.checkIn)}
Check out
{fmtDate(state.checkOut)}
Nights
{nights || "—"}
Who's coming
{[
{ k: "adults", t: "Adults", s: "13+", min: 1 },
{ k: "children", t: "Children", s: "3–12", min: 0 },
{ k: "infants", t: "Infants", s: "0–2", min: 0 }
].map(g => (
{g.t}{g.s}
set({ [g.k]: state[g.k] - 1 })}>−
{state[g.k]}
set({ [g.k]: state[g.k] + 1 })}>+
))}
);
}
function StepRoom({ state, set }) {
const allTypes = [];
window.SK_DATA.buildings.forEach(b => {
b.types.forEach(t => allTypes.push({ ...t, building: b.name, buildingId: b.id, hero: b.hero }));
});
const totalGuests = state.adults + state.children;
const cats = ["All", ...new Set(allTypes.map(t => t.cat))];
const [catFilter, setCatFilter] = useState("All");
let listed = allTypes;
if (catFilter !== "All") listed = listed.filter(t => t.cat === catFilter);
// Move non-fitting to the end
listed = [
...listed.filter(t => t.sleeps >= totalGuests && state.adults <= (t.maxAdults || t.sleeps)),
...listed.filter(t => !(t.sleeps >= totalGuests && state.adults <= (t.maxAdults || t.sleeps)))
];
const fittingCount = allTypes.filter(t => t.sleeps >= totalGuests && state.adults <= (t.maxAdults || t.sleeps)).length;
return (
STEP 02 / 05
Choose your apartment.
{fittingCount} of {allTypes.length} apartment types fit {state.adults} adult{state.adults!==1 && "s"}{state.children?` + ${state.children} child${state.children!==1?"ren":""}`:""}.
{cats.map(c => (
setCatFilter(c)}
>
{c}
{c === "All" ? allTypes.length : allTypes.filter(t => t.cat === c).length}
))}
{listed.map((t, i) => {
const fits = t.sleeps >= totalGuests && state.adults <= (t.maxAdults || t.sleeps);
const sel = state.roomId === `${t.buildingId}-${t.code}`;
return (
fits && set({ roomId: `${t.buildingId}-${t.code}`, roomName: t.name, roomBuilding: t.building, roomPrice: t.from })}
style={{ opacity: fits ? 1 : 0.45, cursor: fits ? "pointer" : "not-allowed" }}
>
{t.name}
{t.cat} · {t.building} · {t.units}
{t.size} m²
·
Sleeps {t.sleeps}
·
Max {t.maxAdults || t.sleeps} adults
·
{t.code === "MZN" ? "3 levels" : t.code === "DUP" || t.code === "JRD" ? "Duplex" : "One level"}
{t.code === "MZN" && "The biggest of the apartments — three levels of stone and timber, with a private roof terrace looking down the bay."}
{t.code === "DUP" && "Two-storey apartment with a bedroom upstairs and the main living space opening to the courtyard."}
{t.code === "1BR" && t.buildingId === "king" && "A compact one-bedroom with a small kitchen, terrace and the original stone walls."}
{t.code === "1BR" && t.buildingId === "prince" && "A compact one-bedroom on the Privé side — quietest part of the resort, kitchenette, private balcony."}
{t.code === "1BF" && "Same plan as the 1BR with an extra sofa-bed in the living room — for one child or a teenager."}
{t.code === "FAM" && "Family Studio with kitchen — open-plan, sofa bed, sleeps four. Soft-pastel walls, generous bathroom, garden view."}
{t.code === "JR1" && "Brand-new junior suite with kitchenette (not for cooking), marble bathroom and full-width balcony over the pool."}
{t.code === "JRD" && "The duplex junior suite — two floors, ideal for big families. Best view of the bay."}
{["Private balcony","A/C","Free Wi-Fi","Soundproof","Safe"].map(c => {c} )}
From
€{t.from}
/ night incl. tax
{sel ? "Selected ✓" : fits ? "Select" : "Too small"}
);
})}
);
}
const EXTRAS = [
{ id: "breakfast", t: "Breakfast buffet", d: "Daily buffet in the new restaurant. Local bread, eggs to order, fruit from the garden, Greek yoghurt.", cap: "Most popular", price: 14, per: "person / day" },
{ id: "transfer", t: "Airport transfer", d: "Private car from Thessaloniki airport. Driver waits with a small sign. One car, up to three guests.", cap: "Logistics", price: 140, per: "one way" },
{ id: "boat", t: "Diaporos boat tour", d: "Three hours around the nine islets with Captain Stelios. Stops at two hidden coves. Snorkels provided.", cap: "Half-day", price: 65, per: "person" },
{ id: "late", t: "Late check-out", d: "Keep the room until 6pm on departure day. Subject to availability — we'll confirm 24h before.", cap: "Comfort", price: 35, per: "one-time" },
{ id: "yoga", t: "Yoga package", d: "Seven sessions across your stay with The Harpy. Mat, towel and water included; outdoor deck.", cap: "Wellness", price: 90, per: "person / week" },
{ id: "wine", t: "Welcome wine basket", d: "A bottle of Naoussa Xinomavro, local cheeses, olives and bread waiting in the room on arrival.", cap: "Arrival", price: 45, per: "one-time" }
];
function StepExtras({ state, set }) {
const toggle = (id) => {
const e = { ...state.extras };
if (e[id]) delete e[id]; else e[id] = true;
set({ extras: e });
};
return (
STEP 03 / 05
Anything to add?
Optional. You can also arrange any of these once you arrive.
);
}
function StepGuest({ state, set }) {
const update = (k) => (e) => set({ guest: { ...state.guest, [k]: e.target.value } });
return (
STEP 04 / 05
Just a few details.
We use these only for the booking. No newsletter unless you ask.
);
}
function ReviewSide({ state }) {
const nights = nightsBetween(state.checkIn, state.checkOut) || 5;
const room = (state.roomPrice || 145) * nights;
const extraTotal = Object.keys(state.extras).reduce((s, id) => {
const e = EXTRAS.find(x => x.id === id);
if (!e) return s;
if (e.per.includes("person / day")) return s + e.price * (state.adults + state.children) * nights;
if (e.per.includes("person / week")) return s + e.price * (state.adults + state.children);
if (e.per.includes("person")) return s + e.price * (state.adults + state.children);
return s + e.price;
}, 0);
const taxes = Math.round((room + extraTotal) * 0.04);
const total = room + extraTotal + taxes;
return (
Your stay
{state.roomName || "Select a room"}
€{room}
{fmtDate(state.checkIn)} → {fmtDate(state.checkOut)} · {nights}n
€{state.roomPrice || 145}/n
{Object.keys(state.extras).length > 0 && (
<>
Extras
{Object.keys(state.extras).map(id => {
const e = EXTRAS.find(x => x.id === id);
return (
{e.t}
€{e.price}
);
})}
>
)}
Tax & city fee (4%)
€{taxes}
Total
€{total}
Pay 20% now · the rest on arrival
);
}
function StepConfirm({ state, reset }) {
const ref = "SK-" + Date.now().toString().slice(-6);
const nights = nightsBetween(state.checkIn, state.checkOut) || 5;
return (
✓
RESERVATION HELD · 24 HOURS
Καλωσήρθατε,{state.guest.first || "guest"} .
Your apartment is held. A confirmation went to {state.guest.email || "your email"} with the full details and a payment link for the 20% deposit. You can pay any time in the next twenty-four hours.
REF / {ref}
Make another reservation
Add to calendar →
WHILE YOU WAIT
The forecast for {fmtDate(state.checkIn)} is sunny, 28°C. Pack lighter than you think. We have everything you need at the bar.
);
}
function Booking({ open, close }) {
const [step, setStep] = useState(0);
const [state, setState] = useState({
checkIn: new Date(2026, 6, 14),
checkOut: new Date(2026, 6, 19),
adults: 2, children: 0, infants: 0,
roomId: null, roomName: null, roomBuilding: null, roomPrice: null,
extras: { breakfast: true },
guest: { first: "", last: "", email: "", phone: "", country: "Greece", arrival: "15:00 – 18:00", notes: "" }
});
const set = (patch) => setState(s => ({ ...s, ...patch }));
const reset = () => { setStep(0); setState(s => ({ ...s, roomId: null, guest: { first: "", last: "", email: "", phone: "", country: "Greece", arrival: "15:00 – 18:00", notes: "" }})); };
const nights = nightsBetween(state.checkIn, state.checkOut);
const room = (state.roomPrice || 145) * (nights || 5);
const extraTotal = Object.keys(state.extras).reduce((s, id) => {
const e = EXTRAS.find(x => x.id === id);
if (!e) return s;
return s + e.price;
}, 0);
const taxes = Math.round((room + extraTotal) * 0.04);
const total = room + extraTotal + taxes;
const canAdvance = (() => {
if (step === 0) return state.checkIn && state.checkOut && nights > 0;
if (step === 1) return !!state.roomId;
if (step === 3) return state.guest.first && state.guest.email;
return true;
})();
return (
{step === 0 && }
{step === 1 && }
{step === 2 && }
{step === 3 && }
{step === 4 && }
{step < 4 && (
Total · 20% deposit now
€{total} / €{Math.round(total * 0.2)} due
{step > 0 && setStep(s => s - 1)}>← Back }
canAdvance && setStep(s => s + 1)}
>
{step === 3 ? "Confirm reservation" : "Continue"} →
)}
);
}
Object.assign(window, { Booking });