// pages/reservation.jsx — 4-step reservation flow with working state
function ReservationPage() {
const { t, lang, navigate, currency, route } = useApp();
const [step, setStep] = useState(0);
// Default check-in: tomorrow; check-out: 3 days later
const today = new Date();
const defaultIn = new Date(today.getTime() + 86400000);
const defaultOut = new Date(today.getTime() + 86400000 * 4);
const fmt = (d) => d.toISOString().slice(0, 10);
const [booking, setBooking] = useState({
checkIn: fmt(defaultIn),
checkOut: fmt(defaultOut),
adults: 2,
children: 0,
promo: "",
roomIdx: route.params && route.params.roomIdx != null ? route.params.roomIdx : null,
firstName: "",
lastName: "",
email: "",
phone: "",
country: lang === "pl" ? "Polska" : "Poland",
notes: "",
terms: false,
// Przelewy24 is temporarily disabled (no production credentials yet), so
// Revolut transfer is the only enabled method. See the payment section below.
paymentMethod: "revolut_transfer",
bookingRef: null,
payment: null,
paymentStatus: null,
});
const update = (patch) => setBooking((b) => ({ ...b, ...patch }));
const [submitError, setSubmitError] = useState(null);
const [submitting, setSubmitting] = useState(false);
// If user navigated in with a roomIdx, jump to step 2
useEffect(() => {
if (booking.roomIdx != null) setStep(1);
}, []); // eslint-disable-line
// When user returns from Przelewy24 with ?ref=, jump to confirmation and poll status
useEffect(() => {
// Ref arrives in the query string (/reservation?ref=...); also accept the
// legacy hash form for any in-flight links from before path routing.
const m = (window.location.search + window.location.hash).match(/[?&]ref=([A-Z0-9-]+)/i);
if (!m) return;
const ref = m[1];
update({ bookingRef: ref });
setStep(3);
let cancelled = false;
const poll = async () => {
for (let i = 0; i < 12 && !cancelled; i++) {
try {
const r = await fetch(`${window.API_BASE}/api/reservations/${ref}`);
if (r.ok) {
const data = await r.json();
update({ paymentStatus: data.payment_status });
if (data.status === "confirmed") return;
}
} catch {}
await new Promise((res) => setTimeout(res, 2500));
}
};
poll();
return () => { cancelled = true; };
}, []); // eslint-disable-line
const nights = useMemo(() => {
const a = new Date(booking.checkIn);
const b = new Date(booking.checkOut);
return Math.max(1, Math.round((b - a) / 86400000));
}, [booking.checkIn, booking.checkOut]);
const roomCard = booking.roomIdx != null ? t.rooms.cards[booking.roomIdx] : null;
const subtotal = roomCard ? roomCard.priceFrom * nights : 0;
const taxes = Math.round(subtotal * 0.08);
const total = subtotal + taxes;
const submitReservation = async () => {
setSubmitError(null);
setSubmitting(true);
try {
const body = {
room_id: booking.roomIdx + 1,
check_in: booking.checkIn,
check_out: booking.checkOut,
adults: booking.adults,
children: booking.children,
first_name: booking.firstName,
last_name: booking.lastName,
email: booking.email,
phone: booking.phone,
country: booking.country,
notes: booking.notes || null,
promo: booking.promo || null,
payment_method: booking.paymentMethod,
terms: booking.terms,
language: lang,
};
const r = await fetch(`${window.API_BASE}/api/reservations`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!r.ok) {
const txt = await r.text();
if (r.status === 409) {
throw new Error(lang === "pl" ? "Wybrany pokoj nie jest dostepny w tych dniach." : "The selected room is not available for these dates.");
}
throw new Error(txt || "request failed");
}
const data = await r.json();
update({ bookingRef: data.booking_ref, payment: data.payment });
if (data.payment.method === "przelewy24") {
window.location.href = data.payment.redirect_url;
return;
}
setStep(3);
window.scrollTo({ top: 0, behavior: "smooth" });
} catch (err) {
setSubmitError(lang === "pl"
? "Nie udalo sie utworzyc rezerwacji. " + (err.message || "")
: "Could not create the reservation. " + (err.message || ""));
} finally {
setSubmitting(false);
}
};
const goNext = () => {
track("reservation_step", { step: step + 1, room_idx: booking.roomIdx });
if (step === 2) {
submitReservation();
return;
}
setStep((s) => Math.min(3, s + 1));
window.scrollTo({ top: 0, behavior: "smooth" });
};
const goBack = () => {
setStep((s) => Math.max(0, s - 1));
window.scrollTo({ top: 0, behavior: "smooth" });
};
const canContinue =
step === 0 ? booking.checkIn && booking.checkOut && nights > 0 :
step === 1 ? booking.roomIdx != null :
step === 2 ? booking.firstName && booking.lastName && booking.email && booking.terms :
true;
return (
{t.reservation.title}
{step < 3
? t.reservation["step" + (step + 1)].title
: t.reservation.step4.title}
i < step && setStep(i)} />
{step < 3 ? (
{step === 0 &&
}
{step === 1 &&
}
{step === 2 &&
}
{submitError && (
{submitError}
)}
{step > 0 ? (
{t.common.back}
) : }
{submitting ? (lang === "pl" ? "Przetwarzanie..." : "Processing...") : (step === 2 ? t.common.confirm : t.common.next)}
) : (
)}
);
}
// ─── Stepper ───────────────────────────────────────────────────────────────
function Stepper({ steps, active, onJump }) {
return (
{steps.map((s, i) => {
const done = i < active;
const isActive = i === active;
return (
onJump(i)} style={{
background: "none", border: 0, padding: "20px 0",
borderTop: isActive ? "2px solid var(--accent)" : done ? "2px solid var(--accent)" : "2px solid transparent",
marginTop: -1,
display: "flex", alignItems: "center", gap: 14,
cursor: done ? "pointer" : "default",
textAlign: "left",
color: isActive || done ? "var(--ink)" : "var(--ink-3)",
}}>
{done ? : i + 1}
{s}
);
})}
);
}
// ─── Step 1: Dates ────────────────────────────────────────────────────────
function StepDates({ booking, update, nights }) {
const { t, lang } = useApp();
return (
{t.reservation.step1.sub}
update({ checkIn: v })} />
update({ checkOut: v })} />
{lang === "pl" ? "Długość pobytu" : "Length of stay"}
{nights} {nightsLabel(nights, lang)}
update({ adults: v })} />
update({ children: v })} />
{t.reservation.step1.promo}
update({ promo: e.target.value })} placeholder="DLUGA10" />
);
}
function DateField({ label, value, onChange }) {
return (
);
}
function Stepper2({ label, value, min, max, onChange }) {
return (
{label}
onChange(Math.max(min, value - 1))} style={{ background: "none", border: 0, padding: "14px 18px", fontSize: 18, color: "var(--ink-2)", cursor: "pointer" }}>−
{value}
onChange(Math.min(max, value + 1))} style={{ background: "none", border: 0, padding: "14px 18px", fontSize: 18, color: "var(--ink-2)", cursor: "pointer" }}>+
);
}
function nightsLabel(n, lang) {
if (lang === "en") return n === 1 ? "night" : "nights";
// Polish plural: 1 noc, 2-4 noce, else nocy
if (n === 1) return "noc";
const mod10 = n % 10, mod100 = n % 100;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return "noce";
return "nocy";
}
// ─── Step 2: Choose room ──────────────────────────────────────────────────
function StepRoom({ booking, update }) {
const { t, currency } = useApp();
return (
{t.reservation.step2.sub}
{t.rooms.cards.map((c, i) => {
const selected = booking.roomIdx === i;
return (
update({ roomIdx: i })}
style={{
background: "var(--paper)",
border: "1px solid " + (selected ? "var(--accent)" : "var(--line)"),
outline: selected ? "1px solid var(--accent)" : "none",
outlineOffset: -2,
padding: 0,
display: "grid",
gridTemplateColumns: "260px 1fr auto",
gap: 28,
textAlign: "left",
cursor: "pointer",
transition: "border-color .3s",
}}
>
{c.tag}
{c.name}
{c.desc}
{c.meta.map((m, j) => {m} )}
{t.common.from}
{formatPrice(c.priceFrom, currency)}
{t.common.perNight}
{selected ? <> {useApp().lang === "pl" ? "Wybrano" : "Selected"}> : useApp().lang === "pl" ? "Wybierz" : "Select"}
);
})}
);
}
// ─── Step 3: Guest details ────────────────────────────────────────────────
function StepGuest({ booking, update }) {
const { t, lang } = useApp();
return (
);
}
function PaymentOption({ value, current, onChange, title, sub, disabled = false }) {
const selected = current === value;
return (
!disabled && onChange(value)} style={{ marginTop: 4, accentColor: "var(--accent)" }} />
);
}
// ─── Summary panel ────────────────────────────────────────────────────────
function ReservationSummary({ booking, nights, subtotal, taxes, total, roomCard }) {
const { t, lang, currency } = useApp();
const niceDate = (d) => {
const date = new Date(d);
return date.toLocaleDateString(lang === "pl" ? "pl-PL" : "en-GB", { day: "2-digit", month: "short", year: "numeric" });
};
return (
{t.common.summary}
{roomCard ? roomCard.name : (lang === "pl" ? "Wybierz pokój" : "Choose a room")}
{roomCard && }
{roomCard ? (
<>
{t.common.total}
{formatPrice(total, currency)}
{lang === "pl" ? "VAT i opłata klimatyczna w cenie" : "VAT and local tax included"}
>
) : (
{lang === "pl" ? "Przejdź dalej, aby zobaczyć dostępne pokoje." : "Continue to see available rooms."}
)}
);
}
function Line({ label, value }) {
return (
{label}
{value}
);
}
// ─── Step 4: Confirmation ─────────────────────────────────────────────────
function StepConfirmation({ booking, nights, total, roomCard }) {
const { t, lang, navigate, currency } = useApp();
const niceDate = (d) => new Date(d).toLocaleDateString(lang === "pl" ? "pl-PL" : "en-GB", { day: "2-digit", month: "long", year: "numeric" });
return (
{t.reservation.step4.sub}
{t.reservation.step4.ref}
{booking.bookingRef}
{lang === "pl" ? "Gość" : "Guest"}
{booking.firstName} {booking.lastName}
{t.common.checkIn}
{niceDate(booking.checkIn)}
{lang === "pl" ? "od 15:00" : "from 3:00 pm"}
{t.common.checkOut}
{niceDate(booking.checkOut)}
{lang === "pl" ? "do 12:00" : "until 12:00 pm"}
{t.common.room}
{roomCard && roomCard.name}
{nights} {nightsLabel(nights, lang)}
{t.common.total}
{formatPrice(total, currency)}
{booking.payment && booking.payment.method === "revolut_transfer"
? (lang === "pl" ? "przelew na konto Revolut" : "bank transfer to Revolut")
: booking.paymentStatus === "paid"
? (lang === "pl" ? "oplacono" : "paid")
: (lang === "pl" ? "oczekuje na platnosc" : "awaiting payment")}
{booking.payment && booking.payment.method === "revolut_transfer" && (
)}
{lang === "pl"
? "Zameldowanie jest samodzielne — instrukcje wyślemy przed przyjazdem. W razie pytań jesteśmy dostępni telefonicznie i na WhatsApp."
: "Check-in is self-service — we'll send instructions before arrival. For any questions, reach us by phone or WhatsApp."}
navigate("home")}>{lang === "pl" ? "Powrót na stronę" : "Back to homepage"}
navigate("reservation")}>{t.reservation.step4.newReservation}
);
}
function RevolutTransferBox({ payment, lang }) {
return (
{lang === "pl" ? "Dane do płatności" : "Payment details"}
{lang === "pl" ? "Odbiorca" : "Recipient"}
{payment.recipient}
{payment.iban && (
<>
IBAN
{payment.iban}
>
)}
{lang === "pl" ? "Tytuł" : "Title"}
{payment.title}
{lang === "pl" ? "Kwota" : "Amount"}
{payment.amount} {payment.currency}
{payment.link && (
{lang === "pl" ? "Zapłać przez Revolut" : "Pay with Revolut"}
)}
{payment.instructions}
);
}
Object.assign(window, { ReservationPage });