// booking-app.jsx — Customer-facing booking site for Windy Hill Homestay // Views: home → detail → checkout → confirm // ─── Helpers ──────────────────────────────────────────── const dateInputISO = (d) => { if (!d) return ''; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${dd}`; }; const parseDateInput = (s) => { if (!s) return null; const [y, m, d] = s.split('-').map(Number); return new Date(y, m - 1, d); }; const isRoomAvailable = (room, ci, co, bookings) => { if (room.status === 'maintenance') return false; if (!ci || !co) return true; const ciD = startOfDay(ci), coD = startOfDay(co); return !bookings.some(b => { if (b.roomId !== room.id) return false; if (b.status === 'cancelled') return false; const bi = startOfDay(b.checkIn), bo = startOfDay(b.checkOut); return ciD < bo && coD > bi; // intervals overlap }); }; // Find the next available start date for a room (within 60 days) const nextAvailableDate = (room, fromDate, nights, bookings) => { if (!fromDate || !nights) return null; for (let i = 1; i <= 60; i++) { const candidateIn = window.HotelData.addDays(fromDate, i); const candidateOut = window.HotelData.addDays(candidateIn, nights); if (isRoomAvailable(room, candidateIn, candidateOut, bookings)) { return candidateIn; } } return null; }; // Map room type/theme to placeholder SVG art const ROOM_ART = { 'suoi': <>, 'la': <>, 'da': <>, 'co': <>, 'may': <>, 'gio': <>, 'suong': <>, 'nang': <>, 'trang': <>, 'mountain': <>, 'thong': <>, 'sao': <>, }; function artForRoom(room) { const name = (room.name || '').toLowerCase(); if (name.includes('suối')) return ROOM_ART.suoi; if (name.includes('lá')) return ROOM_ART.la; if (name.includes('đá')) return ROOM_ART.da; if (name.includes('cỏ')) return ROOM_ART.co; if (name.includes('mây')) return ROOM_ART.may; if (name.includes('gió')) return ROOM_ART.gio; if (name.includes('sương')) return ROOM_ART.suong; if (name.includes('nắng')) return ROOM_ART.nang; if (name.includes('trăng')) return ROOM_ART.trang; if (name.includes('đỉnh núi')) return ROOM_ART.mountain; if (name.includes('thông')) return ROOM_ART.thong; if (name.includes('sao')) return ROOM_ART.sao; return ROOM_ART.may; // default } function PlaceholderArt({ room, kind = 'card', showName = true }) { const cls = room.type.startsWith('Suite') ? 'suite' : room.type === 'Deluxe' ? 'deluxe' : 'standard'; return (
{artForRoom(room)} {showName &&
{room.name || ('Phòng ' + room.number)}
}
); } // Amenity icons const AMENITY_ICONS = { 'View núi': '⛰️', 'View vườn': '🌿', 'View thung lũng': '🏞️', 'View rừng thông': '🌲', 'Hướng vườn': '🌳', 'View toàn cảnh': '🌅', 'View hồ': '🌊', 'View rừng': '🌲', 'Ban công': '🪟', 'Ban công lớn': '🪟', 'Sân riêng': '🌱', 'Sân thượng': '🏠', 'Sân thượng ngắm sao': '✨', 'Bồn tắm': '🛁', 'Bồn tắm ngoài trời': '🛁', 'Jacuzzi': '♨️', 'Vòi sen mưa': '🚿', 'Bếp nhỏ': '🍳', 'Bếp riêng': '🍳', 'Lò sưởi điện': '🔥', 'Lò sưởi củi': '🪵', 'Lò sưởi': '🔥', 'Tivi 4K': '📺', 'Máy lạnh': '❄️', 'Máy pha cà phê': '☕', 'Wi-Fi nhanh': '📶', 'Bàn làm việc': '💼', }; const amenityIcon = (a) => AMENITY_ICONS[a] || '✨'; // ─── Nav ───────────────────────────────────────────────── function Nav({ goHome, onBookNow }) { const [scrolled, setScrolled] = React.useState(false); React.useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 30); window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, []); return ( ); } // ─── Hero ──────────────────────────────────────────────── function Hero({ search, setSearch, onSearch }) { return (
Đà Lạt · Tháng Năm 2026 · 16–22°C

Nghỉ dưỡng giữa rừng thông Đà Lạt,
mỗi đêm là một kỷ niệm.

Windy Hill là homestay 12 phòng nằm bên sườn đồi, đón gió rừng thông buổi sáng và ngắm sao buổi tối. Đặt phòng trực tiếp để nhận giá tốt nhất, không qua trung gian.

12
Phòng
4.9★
238 đánh giá
2hrs
Đến trung tâm
4.9 · Superhost từ 2023
); } function SearchBar({ search, setSearch, onSearch }) { const todayISO = dateInputISO(new Date()); return (
Nhận phòng
setSearch(s => ({ ...s, checkIn: parseDateInput(e.target.value) }))} />
Trả phòng
setSearch(s => ({ ...s, checkOut: parseDateInput(e.target.value) }))} />
Số khách
); } // ─── Rooms grid ────────────────────────────────────────── function RoomsGrid({ rooms, bookings, search, onSelect }) { const [favs, setFavs] = React.useState(new Set()); const toggleFav = (id) => { setFavs(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }); }; const nights = search.checkIn && search.checkOut ? Math.max(1, daysBetween(search.checkIn, search.checkOut)) : 1; // Compute availability for each room once const datesSelected = !!(search.checkIn && search.checkOut); const roomsWithAvail = rooms.map(r => ({ r, available: isRoomAvailable(r, search.checkIn, search.checkOut, bookings), fitsGuests: r.cap >= search.guests, })); const availableCount = roomsWithAvail.filter(x => x.available && x.fitsGuests).length; return (
Phòng & Suite

Chọn không gian của bạn.

12 phòng với tên gọi và tinh thần riêng — từ Phòng Suối nhỏ xinh đến Suite Sao Trời ngắm trọn dải ngân hà.
{/* Availability summary */} {datesSelected && (
{availableCount}/{rooms.length} phòng còn trống
{formatDateShort(search.checkIn)} {formatDateShort(search.checkOut)} · {nights} đêm
{search.guests} khách
)}
{roomsWithAvail.map(({ r, available, fitsGuests }) => { const fav = favs.has(r.id); const slotId = 'room-img-' + r.id; const bookable = available && fitsGuests; const nextAvail = !available && datesSelected ? nextAvailableDate(r, search.checkIn, nights, bookings) : null; return (
bookable && onSelect(r)} >
{datesSelected && ( {bookable ? 'Còn trống' : !available ? 'Hết phòng' : `Tối đa ${r.cap} khách`} )} {!bookable && datesSelected && (
{!available ? '🚫 Hết phòng cho ngày này' : `👥 Phòng tối đa ${r.cap} khách`}
{nextAvail && (
Còn trống từ {formatDateShort(nextAvail)}
)}
)}
P.{r.number}

{r.name || ('Phòng ' + r.number)}

{r.type} · Tầng {r.floor}
{(4.6 + (parseInt(r.number) % 4) * 0.1).toFixed(1)}
{r.bed} {r.cap} khách
{r.features && (
{r.features.slice(0, 3).map((f, i) => ( {f} ))} {r.features.length > 3 && +{r.features.length - 3}}
)}
{VND(r.price)} /đêm
{nights > 1 && bookable && (
{VND(r.price * nights)} cho {nights} đêm
)}
); })}
); } // ─── Room detail ───────────────────────────────────────── function RoomDetail({ room, search, setSearch, onContinue, onBack }) { const nights = search.checkIn && search.checkOut ? Math.max(1, daysBetween(search.checkIn, search.checkOut)) : 2; const roomTotal = room.price * nights; const cleaning = 100000; const serviceFee = Math.round(roomTotal * 0.05); const total = roomTotal + cleaning + serviceFee; const canContinue = search.checkIn && search.checkOut && nights > 0; const rating = (4.6 + (parseInt(room.number) % 4) * 0.1).toFixed(1); return (
{[2, 3, 4, 5].map(i => (
))}
{room.type} · Tầng {room.floor} · Phòng {room.number}

{room.name || ('Phòng ' + room.number)}

{room.bed} Tối đa {room.cap} khách {rating} · 47 đánh giá

{room.name} là không gian {room.type === 'Suite' || room.type.startsWith('Suite') ? 'sang trọng nhất' : room.type === 'Deluxe' ? 'rộng rãi' : 'ấm cúng'} của Windy Hill — {room.features?.find(f => f.startsWith('View')) ? ` đón ánh sáng tự nhiên qua cửa sổ lớn hướng ${room.features.find(f => f.startsWith('View')).replace('View ', '')}, ` : ' '} {room.features?.includes('Lò sưởi củi') || room.features?.includes('Lò sưởi điện') ? 'có lò sưởi để ấm áp những đêm Đà Lạt se lạnh, ' : ''} cùng đầy đủ tiện nghi cho kỳ nghỉ thư thái. Mỗi sáng, bạn sẽ thức dậy bên tiếng chim và mùi thông sau mưa.

Tiện nghi phòng

{(room.features || []).map((f, i) => (
{amenityIcon(f)} {f}
))}
Bữa sáng homemade
📶 Wi-Fi tốc độ cao
🅿️ Bãi đỗ xe miễn phí

Chính sách

  • Nhận phòng từ 14:00 · Trả phòng trước 12:00
  • Hủy miễn phí trong vòng 48 giờ trước check-in
  • Không hút thuốc trong phòng · Có khu hút thuốc riêng
  • Cho phép thú cưng nhỏ (báo trước · phụ thu 150.000đ/đêm)
{/* Sticky booking card */}
); } // ─── Checkout ───────────────────────────────────────────── function Checkout({ room, search, guestInfo, setGuestInfo, payment, setPayment, onBack, onConfirm }) { const nights = Math.max(1, daysBetween(search.checkIn, search.checkOut)); const roomTotal = room.price * nights; const cleaning = 100000; const serviceFee = Math.round(roomTotal * 0.05); const total = roomTotal + cleaning + serviceFee; const canConfirm = guestInfo.name && guestInfo.phone && guestInfo.email && payment.method; return (

Xác nhận đặt phòng

Sắp xong rồi! Điền thông tin để chúng tôi giữ chỗ.
1 Chọn phòng
2 Thông tin & Thanh toán
3 Xác nhận

Thông tin khách

setGuestInfo(g => ({ ...g, name: e.target.value }))} />
setGuestInfo(g => ({ ...g, phone: e.target.value }))} />
setGuestInfo(g => ({ ...g, email: e.target.value }))} />
setGuestInfo(g => ({ ...g, idNo: e.target.value }))} />

Phương thức thanh toán

{[ { k: 'card', l: 'Thẻ ngân hàng', emoji: '💳' }, { k: 'transfer', l: 'Chuyển khoản', emoji: '🏦' }, { k: 'momo', l: 'Momo · ZaloPay', emoji: '📱' }, { k: 'pay_later', l: 'Trả tại chỗ', emoji: '💵' }, ].map(m => ( ))}
{/* Summary */}
); } // ─── Confirmation ───────────────────────────────────────── function Confirmation({ booking, room, search, total, guestInfo, onHome }) { const nights = Math.max(1, daysBetween(search.checkIn, search.checkOut)); return (

Cảm ơn {guestInfo.name.split(' ').pop()}! Phòng đã được giữ chỗ.

Chúng tôi đã gửi email xác nhận tới {guestInfo.email}.
Nhân viên sẽ liên hệ qua số {guestInfo.phone} trong 24h để xác nhận lại.

{booking.code}
Phòng{room.name || ('Phòng ' + room.number)} · {room.type}
Nhận phòng{formatDateVN(search.checkIn)} · 14:00
Trả phòng{formatDateVN(search.checkOut)} · 12:00
{nights} đêm · {search.guests} khách{VND(room.price)}/đêm
Tổng cộng{VND(total)}

Hẹn gặp bạn ở Windy Hill 🌲

); } // ─── About + Footer ────────────────────────────────────── function About() { return (
Về Windy Hill

Một góc Đà Lạt yên ả.

Chỉ 12 phòng, đội ngũ ít hơn 10 người. Chúng tôi tin vào sự đơn giản, chân thật và những bữa sáng tự nấu.
{[ { ico: '🌲', t: 'Trong rừng thông', d: '5 phút đi bộ tới đường mòn rừng thông Tuyền Lâm. Sáng dậy với mùi nhựa thông và tiếng chim.' }, { ico: '☕', t: 'Bữa sáng nhà nấu', d: 'Phở, bún, trứng ốp, bánh mì tự nướng — bữa sáng đổi món mỗi ngày, làm bằng nguyên liệu địa phương.' }, { ico: '🚗', t: '20 phút từ trung tâm', d: 'Đủ gần để đi chợ Đà Lạt buổi tối, đủ xa để nghe tiếng thông reo cả ngày.' }, ].map((b, i) => (
{b.ico}

{b.t}

{b.d}

))}
); } function Footer() { return ( ); } // ─── Main App ─────────────────────────────────────────── function BookingApp() { const data = window.HotelData; const [view, setView] = React.useState('home'); // 'home' | 'detail' | 'checkout' | 'confirm' const [selectedRoom, setSelectedRoom] = React.useState(null); const [search, setSearch] = React.useState(() => { // Default: today + 1 → today + 3. This overlaps existing demo bookings // so customer immediately sees the "Còn trống / Hết phòng" UI. const ci = window.HotelData.addDays(window.HotelData.today, 1); const co = window.HotelData.addDays(window.HotelData.today, 3); return { checkIn: ci, checkOut: co, guests: 2 }; }); const [guestInfo, setGuestInfo] = React.useState({ name: '', phone: '', email: '', idNo: '', note: '', }); const [payment, setPayment] = React.useState({ method: 'card' }); const [confirmedBooking, setConfirmedBooking] = React.useState(null); const [toast, setToast] = React.useState(null); // Scroll to top on view change React.useEffect(() => { window.scrollTo({ top: 0, behavior: 'instant' }); }, [view]); const handleSelect = (room) => { setSelectedRoom(room); setView('detail'); }; const handleSearchSubmit = () => { setToast('Đã tìm phòng cho ' + (search.checkIn ? formatDateShort(search.checkIn) : '?') + ' → ' + (search.checkOut ? formatDateShort(search.checkOut) : '?')); setTimeout(() => setToast(null), 2500); // Scroll to rooms section document.getElementById('rooms')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; const handleConfirm = () => { const code = 'WH' + Math.floor(2500 + Math.random() * 7499); const nights = Math.max(1, daysBetween(search.checkIn, search.checkOut)); const roomTotal = selectedRoom.price * nights; const cleaning = 100000; const serviceFee = Math.round(roomTotal * 0.05); const total = roomTotal + cleaning + serviceFee; setConfirmedBooking({ code, total }); setView('confirm'); }; return ( <>