Booking Details
Choose your accommodation to begin Select a unit above to see photos, capacity and pricing — then pick your guests and dates.
Who’s coming?
Adults Age 13+
Children Age 0–12
Please add at least one guest
From Select a date To
Please select check-in and check-out dates
PLEASE SELECT ACCOMMODATION TYPE
Accommodation Summary
Calculating...
Booking Details
grid — gets // an immediate, confident confirmation of what they're booking. function updateStayHero() { const sel = document.getElementById('innAccommodation'); const hero = document.getElementById('innStayHero'); if (!sel || !hero) return; const opt = sel.options[sel.selectedIndex]; const emptyEl = document.getElementById('innStayHeroEmpty'); const infoEl = document.getElementById('innStayHeroInfo'); const imgEl = document.getElementById('innStayHeroImg'); const fbEl = document.getElementById('innStayHeroFallback'); const initEl = document.getElementById('innStayHeroInitials'); const priceEl = document.getElementById('innStayHeroPrice'); const nameEl = document.getElementById('innStayHeroName'); const catEl = document.getElementById('innStayHeroCat'); const specsEl = document.getElementById('innStayHeroSpecs'); if (!opt || sel.value === 'none') { hero.classList.add('is-empty'); emptyEl.hidden = false; infoEl.hidden = true; return; } hero.classList.remove('is-empty'); emptyEl.hidden = true; infoEl.hidden = false; const name = (opt.textContent || '').trim(); const img = opt.getAttribute('data-image') || ''; const cat = opt.getAttribute('data-category') || ''; const occ = parseInt(opt.getAttribute('data-max-occ') || '0', 10) || 0; const mAd = parseInt(opt.getAttribute('data-max-adults') || '0', 10) || 0; const mCh = parseInt(opt.getAttribute('data-max-children') || '0', 10) || 0; function showFallback() { imgEl.hidden = true; imgEl.removeAttribute('src'); fbEl.style.display = ''; const initials = name.split(/\s+/).filter(Boolean).slice(0, 2) .map(function (w) { return w.charAt(0); }).join('').toUpperCase(); initEl.textContent = initials || '★'; } if (img) { fbEl.style.display = 'none'; imgEl.onerror = showFallback; imgEl.alt = name; imgEl.src = img; imgEl.hidden = false; } else { showFallback(); } // Price is fetched live for the current party size. priceEl.hidden = true; nameEl.textContent = name; if (cat) { catEl.textContent = cat; catEl.hidden = false; } else { catEl.hidden = true; } const specs = []; if (occ) specs.push('\u{1F465} Sleeps ' + occ + ''); if (mAd) specs.push('\u{1F6CF}️ ' + mAd + ' adults' + (mCh ? ', ' + mCh + ' children' : '') + ''); specsEl.innerHTML = specs.join(''); refreshHeroFromPrice(); } function updateStepRail() { const rStay = document.getElementById('innRailStay'); const rGuests = document.getElementById('innRailGuests'); const rReview = document.getElementById('innRailReview'); if (!rStay || !rGuests || !rReview) return; const hasStay = state.accommodation !== 'none' && state.unitTypeId !== 0; const hasDates = !!(state.checkIn && state.checkOut); [rStay, rGuests, rReview].forEach(function (el) { el.classList.remove('is-active', 'is-done'); }); if (!hasStay) { rStay.classList.add('is-active'); } else if (!hasDates) { rStay.classList.add('is-done'); rGuests.classList.add('is-active'); } else { rStay.classList.add('is-done'); rGuests.classList.add('is-done'); rReview.classList.add('is-active'); } } // ==================== FETCH AVAILABILITY ==================== function fetchAvailability() { // Unit Type IDs can be negative, so only reject if exactly 0 (not selected) if (state.unitTypeId === 0) { state.availableDates = []; state.availabilityLoaded = false; return; } const formData = new FormData(); formData.append('action', 'inn_get_availability'); formData.append('nonce', innNonce); formData.append('unit_type_id', state.unitTypeId); fetch(ajaxUrl, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { state.availableDates = data.data.availableDates || []; state.availabilityLoaded = true; innRenderCalendars(); } else { state.availableDates = []; state.availabilityLoaded = true; } }) .catch(err => { console.error('Error fetching availability:', err); state.availableDates = []; state.availabilityLoaded = true; }); } // ==================== FETCH RATE ==================== function fetchRate() { // Unit Type IDs can be negative, so only reject if exactly 0 (not selected) if (state.unitTypeId === 0 || !state.checkIn || !state.checkOut) { state.rateData = null; updateCostSummary(); return; } const formData = new FormData(); formData.append('action', 'inn_get_rate'); formData.append('nonce', innNonce); formData.append('unit_type_id', state.unitTypeId); formData.append('arrival', formatDateForApi(state.checkIn)); formData.append('departure', formatDateForApi(state.checkOut)); formData.append('adults', state.adults); formData.append('children', state.children); document.getElementById('innCostDetails').innerHTML = '
Calculating rate...
'; document.getElementById('innCostSummary').classList.add('show'); fetch(ajaxUrl, { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { state.rateData = data.data; } else { state.rateData = { error: data.data || 'Unknown error fetching rate' }; } updateCostSummary(); }) .catch(err => { console.error('Error fetching rate:', err); state.rateData = null; updateCostSummary(); }); } function formatDateForApi(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return year + '-' + month + '-' + day; } function updateCostSummary() { const summaryEl = document.getElementById('innCostSummary'); const detailsEl = document.getElementById('innCostDetails'); updateStepRail(); if (!state.checkIn || !state.checkOut || state.accommodation === 'none') { summaryEl.classList.remove('show'); // Also clear calendar total if dates are not complete if (!state.checkIn || !state.checkOut) { document.getElementById('innCalendarTotal').textContent = 'Total ZAR 0'; } return; } summaryEl.classList.add('show'); if (!state.rateData) { detailsEl.innerHTML = '
Calculating rate...
'; // Update calendar total to show calculating document.getElementById('innCalendarTotal').textContent = 'Calculating...'; return; } // Handle potential error in rate data if (state.rateData.error) { detailsEl.innerHTML = '
Rate unavailable: ' + state.rateData.error + '
'; document.getElementById('innCalendarTotal').textContent = 'Error calculating rate'; return; } const nights = Math.ceil((state.checkOut - state.checkIn) / (1000 * 60 * 60 * 24)); const totalGuests = state.adults + state.children; let html = ''; html += '
Guests' + state.adults + ' Adults, ' + state.children + ' Children
'; html += '
Check-in' + state.checkIn.toLocaleDateString('en-ZA') + '
'; html += '
Check-out' + state.checkOut.toLocaleDateString('en-ZA') + '
'; html += '
Nights' + nights + '
'; html += '
Total AccommodationR ' + state.rateData.formattedTotal + '
'; detailsEl.innerHTML = html; // Also update the calendar total if (state.rateData && state.rateData.formattedTotal) { document.getElementById('innCalendarTotal').textContent = 'Total ZAR ' + state.rateData.formattedTotal; } } // ==================== ACCOMMODATION ==================== document.getElementById('innAccommodation').addEventListener('change', function() { const selected = this.options[this.selectedIndex]; state.accommodation = this.value; state.unitTypeId = this.value !== 'none' ? parseInt(this.value) : 0; if (this.value !== 'none') { state.maxAdults = parseInt(selected.dataset.maxAdults) || 10; state.maxChildren = parseInt(selected.dataset.maxChildren) || 5; state.maxGuests = parseInt(selected.dataset.maxGuests) || 10; // Fetch availability for the selected accommodation fetchAvailability(); } else { state.maxAdults = 10; state.maxChildren = 5; state.maxGuests = 10; state.availableDates = []; state.availabilityLoaded = false; } // Adjust current counts if they exceed new max if (state.adults > state.maxAdults) { state.adults = state.maxAdults; document.getElementById('innAdultsCount').value = state.adults; } if (state.children > state.maxChildren) { state.children = state.maxChildren; document.getElementById('innChildrenCount').value = state.children; } // Ensure total guests don't exceed max while ((state.adults + state.children) > state.maxGuests && state.children > 0) { state.children--; document.getElementById('innChildrenCount').value = state.children; } while ((state.adults + state.children) > state.maxGuests && state.adults > 1) { state.adults--; document.getElementById('innAdultsCount').value = state.adults; } // Reset dates when accommodation changes state.checkIn = null; state.checkOut = null; state.selectingCheckout = false; state.rateData = null; innUpdateDateDisplays(); // Update tabs state updateTabsState(); updateStayHero(); updateCostSummary(); updateStepRail(); }); // ==================== GUEST COUNTER ==================== window.innUpdateCounter = function(type, delta) { if (type === 'adults') { const newVal = state.adults + delta; if (newVal >= 1 && newVal <= state.maxAdults && (newVal + state.children) <= state.maxGuests) { state.adults = newVal; document.getElementById('innAdultsCount').value = newVal; fetchRate(); refreshHeroFromPrice(); } } else if (type === 'children') { const newVal = state.children + delta; if (newVal >= 0 && newVal <= state.maxChildren && (state.adults + newVal) <= state.maxGuests) { state.children = newVal; document.getElementById('innChildrenCount').value = newVal; fetchRate(); } } innUpdateGuestsDisplay(); }; function innUpdateGuestsDisplay() { // Reflect min/max limits on the inline counter buttons so the // controls communicate their own bounds. const aMinus = document.getElementById('innAdultsMinus'); const aPlus = document.getElementById('innAdultsPlus'); const cMinus = document.getElementById('innChildrenMinus'); const cPlus = document.getElementById('innChildrenPlus'); const total = state.adults + state.children; if (aMinus) aMinus.disabled = state.adults <= 1; if (aPlus) aPlus.disabled = state.adults >= state.maxAdults || total >= state.maxGuests; if (cMinus) cMinus.disabled = state.children <= 0; if (cPlus) cPlus.disabled = state.children >= state.maxChildren || total >= state.maxGuests; // Update info message const infoMsg = document.getElementById('innInfoMsg'); if (state.accommodation === 'none') { infoMsg.textContent = 'PLEASE SELECT ACCOMMODATION TYPE'; } else if (total > 0) { if (!state.checkIn || !state.checkOut) { infoMsg.textContent = 'PLEASE SELECT CHECK-IN AND CHECK-OUT DATES'; } else { infoMsg.textContent = ''; } } else { infoMsg.textContent = 'PLEASE SELECT THE NUMBER OF GUESTS'; } } // Re-quote the hero's "from" price for the current adult count. The // grid/initial badge is computed at 1 adult server-side; this keeps the // hero honest once the guest picks their party size. let innFromPriceReq = 0; function refreshHeroFromPrice() { const priceEl = document.getElementById('innStayHeroPrice'); if (!priceEl || state.unitTypeId === 0) return; const reqId = ++innFromPriceReq; const fd = new FormData(); fd.append('action', 'inn_get_from_price'); fd.append('nonce', innNonce); fd.append('unit_type_id', state.unitTypeId); fd.append('adults', state.adults); fetch(ajaxUrl, { method: 'POST', body: fd }) .then(function(r){ return r.json(); }) .then(function(data){ if (reqId !== innFromPriceReq) return; // a newer request superseded this one if (data && data.success && data.data && data.data.formatted) { priceEl.innerHTML = 'from ' + data.data.formatted + ' /night'; priceEl.hidden = false; } }) .catch(function(){ /* leave the existing badge in place */ }); } // ==================== CALENDAR ==================== window.innRenderCalendars = function() { const month1 = new Date(state.currentMonth); const month2 = new Date(state.currentMonth); month2.setMonth(month2.getMonth() + 1); document.getElementById('innMonth1').innerHTML = innRenderMonth(month1, true); document.getElementById('innMonth2').innerHTML = innRenderMonth(month2, false); innUpdateRangeHint(); }; // Check if a date is available function isDateAvailable(dateStr) { // If accommodation is not selected, allow all dates // Unit Type IDs can be negative, so only check for exactly 0 (not selected) if (state.accommodation === 'none' || state.unitTypeId === 0) return true; // If availability not loaded yet, don't allow selection (safer) if (!state.availabilityLoaded) return false; // If no available dates at all, nothing is available if (state.availableDates.length === 0) return false; return state.availableDates.includes(dateStr); } // Find the first unavailable date after check-in function getMaxCheckoutDate() { if (!state.checkIn) return null; const checkInStr = formatDateForApi(state.checkIn); let currentDate = new Date(state.checkIn); currentDate.setDate(currentDate.getDate() + 1); // Start from day after check-in // Look up to 365 days ahead for (let i = 0; i < 365; i++) { const dateStr = formatDateForApi(currentDate); if (!isDateAvailable(dateStr)) { // Return the day before the first unavailable date return currentDate; } currentDate.setDate(currentDate.getDate() + 1); } return null; // No restriction found } function innRenderMonth(date, showPrevNav) { const year = date.getFullYear(); const month = date.getMonth(); const monthName = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDayOfWeek = firstDay.getDay(); const daysInMonth = lastDay.getDate(); const today = new Date(); today.setHours(0, 0, 0, 0); // Get max checkout date if we're selecting checkout const maxCheckout = state.selectingCheckout ? getMaxCheckoutDate() : null; let html = '
'; if (showPrevNav) { html += ''; } else { html += ''; } html += '' + monthName.toUpperCase() + ''; if (!showPrevNav) { html += ''; } else { html += ''; } html += '
'; html += '
'; ['S', 'M', 'T', 'W', 'T', 'F', 'S'].forEach(d => { html += '
' + d + '
'; }); html += '
'; html += '
'; // Empty cells for (let i = 0; i < startDayOfWeek; i++) { html += '
'; } // Days for (let d = 1; d <= daysInMonth; d++) { const dayDate = new Date(year, month, d); dayDate.setHours(0, 0, 0, 0); const isPast = dayDate < today; const dateStr = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0'); // Check if date is available const available = isDateAvailable(dateStr); let classes = 'inn-day'; let isDisabled = false; if (isPast) { classes += ' disabled'; isDisabled = true; } else if (!available) { // Unavailable dates are always marked as unavailable and disabled classes += ' unavailable'; isDisabled = true; } else if (state.selectingCheckout) { // For checkout selection, check additional constraints if (state.checkIn && dayDate <= state.checkIn) { // Can't checkout on or before check-in classes += ' disabled'; isDisabled = true; } else if (maxCheckout && dayDate >= maxCheckout) { // Everything from the first unavailable night onward is // off-limits — you can't book across a gap. classes += ' unavailable'; isDisabled = true; } else if (state.checkIn && dayDate > state.checkIn) { // A reachable, contiguous check-out date: give it extra // visual weight so valid end dates stand out. classes += ' checkout-option'; } } // Check if selected if (state.checkIn && dayDate.getTime() === state.checkIn.getTime()) { classes += ' check-in selected'; } else if (state.checkOut && dayDate.getTime() === state.checkOut.getTime()) { classes += ' check-out selected'; } else if (state.checkIn && state.checkOut && dayDate > state.checkIn && dayDate < state.checkOut) { classes += ' in-range'; } if (isDisabled) { html += '
' + d + '
'; } else { html += '
' + d + '
'; } } html += '
'; return html; } window.innNavigateMonth = function(delta) { state.currentMonth.setMonth(state.currentMonth.getMonth() + delta); innRenderCalendars(); }; window.innSelectDate = function(dateStr) { const date = new Date(dateStr + 'T00:00:00'); const today = new Date(); today.setHours(0, 0, 0, 0); if (date < today) return; // Check availability for check-in selection if (!state.selectingCheckout && !isDateAvailable(dateStr)) { return; } if (!state.checkIn || (state.checkIn && state.checkOut)) { // Start fresh selection - this is check-in // Must be available for check-in if (!isDateAvailable(dateStr)) { return; } state.checkIn = date; state.checkOut = null; state.selectingCheckout = true; // Keep calendar open for checkout selection } else if (state.selectingCheckout) { if (date <= state.checkIn) { // If selected date is before or equal to check-in, make it new check-in // Must be available for check-in if (!isDateAvailable(dateStr)) { return; } state.checkIn = date; state.checkOut = null; // Keep selectingCheckout = true } else { // Valid checkout date selected - check availability if (!isDateAvailable(dateStr)) { return; } state.checkOut = date; state.selectingCheckout = false; // Fetch rate now that both dates are selected fetchRate(); } } innRenderCalendars(); innUpdateDateDisplays(); innUpdateRangeHint(); }; // Top-of-calendar cue: "From ___" before any pick, "From to ___" // once the check-in is set, and the full range once both are chosen. // The active segment (the one we're waiting on) is highlighted. function innUpdateRangeHint() { const fromVal = document.getElementById('innRangeFromVal'); const toVal = document.getElementById('innRangeToVal'); const fromSeg = document.getElementById('innRangeFrom'); const toSeg = document.getElementById('innRangeTo'); if (!fromVal || !toVal) return; const fmt = function(dt){ return dt.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); }; fromVal.textContent = state.checkIn ? fmt(state.checkIn) : 'Select a date'; toVal.textContent = state.checkOut ? fmt(state.checkOut) : '—'; // Which field are we waiting on? const waitingForCheckout = !!state.checkIn && !state.checkOut; if (fromSeg) fromSeg.classList.toggle('is-active', !state.checkIn); if (toSeg) toSeg.classList.toggle('is-active', waitingForCheckout); if (fromSeg) fromSeg.classList.toggle('is-filled', !!state.checkIn); if (toSeg) toSeg.classList.toggle('is-filled', !!state.checkOut); } function innUpdateDateDisplays() { const checkinVal = document.getElementById('innCheckinValue'); const checkoutVal = document.getElementById('innCheckoutValue'); if (state.checkIn) { checkinVal.textContent = state.checkIn.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }); } else { checkinVal.textContent = 'Select date'; } if (state.checkOut) { checkoutVal.textContent = state.checkOut.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }); } else { checkoutVal.textContent = state.checkIn ? 'Choose check-out' : 'Select date'; } // Update calendar total - use real rate data if available, otherwise show calculating message if (state.checkIn && state.checkOut) { if (state.rateData && state.rateData.formattedTotal) { // Use the real rate from the API document.getElementById('innCalendarTotal').textContent = 'Total ZAR ' + state.rateData.formattedTotal; } else { // Show calculating message while waiting for rate API response document.getElementById('innCalendarTotal').textContent = 'Calculating...'; } } else { document.getElementById('innCalendarTotal').textContent = 'Total ZAR 0'; } innUpdateGuestsDisplay(); } window.innResetDates = function() { state.checkIn = null; state.checkOut = null; state.selectingCheckout = false; state.rateData = null; innRenderCalendars(); innUpdateDateDisplays(); innUpdateRangeHint(); updateCostSummary(); }; window.innConfirmDates = function() { if (!state.checkIn || !state.checkOut) { document.getElementById('innDatesError').classList.add('show'); return; } document.getElementById('innDatesError').classList.remove('show'); document.getElementById('innCalendarPopup').classList.remove('show'); document.getElementById('innCheckinTab').classList.remove('active'); document.getElementById('innCheckoutTab').classList.remove('active'); }; // ==================== STEP NAVIGATION ==================== window.innGoToStep2 = function() { const isAccommodationNone = state.accommodation === 'none'; // Validation if (!isAccommodationNone) { // Check guests if (state.adults + state.children < 1) { document.getElementById('innGuestsError').classList.add('show'); document.getElementById('innGuestsInline').scrollIntoView({ behavior: 'smooth', block: 'center' }); innShowToast('Please select at least one guest', 'warning'); return; } // Check dates if (!state.checkIn || !state.checkOut) { document.getElementById('innDatesError').classList.add('show'); state.preventOutsideClose = true; innTogglePopup('calendar', true); innShowToast('Please select check-in and check-out dates first', 'warning'); return; } } else { // For treatments only, require at least 1 guest if (state.adults + state.children < 1) { innShowToast('Please select at least one guest for treatments', 'warning'); return; } } // Initialize guests array based on count const totalGuests = Math.max(state.adults + state.children, 1); state.guests = []; for (let i = 0; i < totalGuests; i++) { state.guests.push({ name: '', surname: '', type: i < state.adults ? 'adult' : 'child', treatments: [] }); } // Show step 2 document.getElementById('innStep1').classList.add('hidden'); document.getElementById('innStep2').classList.add('show'); state.currentGuestIndex = 0; innRenderGuestForm(); }; window.innGoToStep1 = function() { document.getElementById('innStep1').classList.remove('hidden'); document.getElementById('innStep2').classList.remove('show'); }; // ==================== GUEST CAROUSEL ==================== window.innNavigateGuest = function(delta) { innSaveGuestData(); const newIndex = state.currentGuestIndex + delta; if (newIndex >= 0 && newIndex < state.guests.length) { state.currentGuestIndex = newIndex; innRenderGuestForm(); } }; function innRenderGuestForm() { const guest = state.guests[state.currentGuestIndex]; const isLast = state.currentGuestIndex === state.guests.length - 1; const isFirst = state.currentGuestIndex === 0; const totalGuests = state.guests.length; // Check both total guest limit and adult limit (new guests are added as adults) const canAddMore = state.guests.length < state.maxGuests && state.adults < state.maxAdults; // Update title with format "Guest X / Y Info" document.getElementById('innGuestTitle').textContent = 'Guest ' + (state.currentGuestIndex + 1) + ' / ' + totalGuests + ' Info'; // Update navigation buttons document.getElementById('innPrevGuest').disabled = isFirst; const nextBtn = document.getElementById('innNextGuest'); if (isLast && canAddMore) { // Change to add guest button appearance nextBtn.textContent = '+'; nextBtn.disabled = false; nextBtn.onclick = innAddGuest; } else { nextBtn.textContent = '›'; nextBtn.disabled = isLast && !canAddMore; nextBtn.onclick = function() { innNavigateGuest(1); }; } nextBtn.classList.toggle('inn-nav-btn-add', isLast && canAddMore); // Enable/disable add guest button const addGuestLink = document.getElementById('innAddGuestLink'); addGuestLink.disabled = !canAddMore; addGuestLink.classList.toggle('disabled', !canAddMore); // Fill form document.getElementById('innGuestName').value = guest.name || ''; document.getElementById('innGuestSurname').value = guest.surname || ''; // Update type toggle document.getElementById('innAdultRadio').classList.toggle('selected', guest.type === 'adult'); document.getElementById('innChildRadio').classList.toggle('selected', guest.type === 'child'); // Render treatments innRenderGuestTreatments(); // Show/hide remove button const removeBtn = document.getElementById('innRemoveGuestBtn'); removeBtn.style.display = (state.guests.length > 1) ? 'block' : 'none'; } window.innSaveGuestData = function() { const guest = state.guests[state.currentGuestIndex]; guest.name = document.getElementById('innGuestName').value; guest.surname = document.getElementById('innGuestSurname').value; }; window.innSetGuestType = function(type) { state.guests[state.currentGuestIndex].type = type; document.getElementById('innAdultRadio').classList.toggle('selected', type === 'adult'); document.getElementById('innChildRadio').classList.toggle('selected', type === 'child'); // Update counts let adults = 0, children = 0; state.guests.forEach(g => { if (g.type === 'adult') adults++; else children++; }); state.adults = adults; state.children = children; // Update add guest button state based on new adult/child counts const canAddMore = state.guests.length < state.maxGuests && state.adults < state.maxAdults; const addGuestLink = document.getElementById('innAddGuestLink'); addGuestLink.disabled = !canAddMore; addGuestLink.classList.toggle('disabled', !canAddMore); // Update navigation button if on last guest const isLast = state.currentGuestIndex === state.guests.length - 1; const nextBtn = document.getElementById('innNextGuest'); if (isLast && canAddMore) { nextBtn.textContent = '+'; nextBtn.disabled = false; nextBtn.onclick = innAddGuest; nextBtn.classList.add('inn-nav-btn-add'); } else if (isLast && !canAddMore) { nextBtn.textContent = '›'; nextBtn.disabled = true; nextBtn.onclick = function() { innNavigateGuest(1); }; nextBtn.classList.remove('inn-nav-btn-add'); } }; window.innAddGuest = function() { // Check both total guest limit and adult limit (new guests are added as adults) if (state.guests.length >= state.maxGuests) { innShowToast('This accommodation allows up to ' + state.maxGuests + ' guests only', 'warning'); return; } if (state.adults >= state.maxAdults) { innShowToast('Maximum of ' + state.maxAdults + ' adults allowed for this accommodation', 'warning'); return; } innSaveGuestData(); state.guests.push({ name: '', surname: '', type: 'adult', treatments: [] }); state.adults++; state.currentGuestIndex = state.guests.length - 1; innRenderGuestForm(); }; window.innRemoveGuest = function() { if (state.guests.length > 1) { const removed = state.guests.splice(state.currentGuestIndex, 1)[0]; if (removed.type === 'adult') state.adults--; else state.children--; if (state.currentGuestIndex >= state.guests.length) { state.currentGuestIndex = state.guests.length - 1; } innRenderGuestForm(); } }; // ==================== TREATMENTS ==================== window.innFilterTreatments = function() { const search = document.getElementById('innTreatmentSearch').value.toLowerCase(); const category = document.getElementById('innCategoryFilter').value; document.querySelectorAll('.inn-treatment-row').forEach(row => { const name = row.dataset.name.toLowerCase(); const cat = row.dataset.category; const matchesSearch = name.includes(search); const matchesCategory = !category || cat === category; row.style.display = (matchesSearch && matchesCategory) ? 'flex' : 'none'; }); }; window.innAddTreatmentToGuest = function(btn) { const row = btn.closest('.inn-treatment-row'); const treatment = { id: row.dataset.id, name: row.dataset.name, price: parseFloat(row.dataset.price) }; state.guests[state.currentGuestIndex].treatments.push(treatment); innRenderGuestTreatments(); }; window.innAddTreatmentToAll = function(btn) { const row = btn.closest('.inn-treatment-row'); const treatment = { id: row.dataset.id, name: row.dataset.name, price: parseFloat(row.dataset.price) }; state.guests.forEach(guest => { guest.treatments.push({...treatment}); }); innRenderGuestTreatments(); }; function innRenderGuestTreatments() { const container = document.getElementById('innGuestTreatmentsList'); const treatments = state.guests[state.currentGuestIndex].treatments; if (treatments.length === 0) { container.innerHTML = '

No treatments added yet

'; return; } let html = ''; treatments.forEach((t, idx) => { html += '
'; html += '
'; html += '' + t.name + ''; html += 'R' + (t.price / 100).toFixed(2) + ''; html += '
'; html += ''; html += '
'; }); container.innerHTML = html; } window.innRemoveTreatment = function(idx) { state.guests[state.currentGuestIndex].treatments.splice(idx, 1); innRenderGuestTreatments(); }; // ==================== DRAG AND DROP ==================== let draggedTreatmentData = null; window.innDragStart = function(event) { const row = event.target.closest('.inn-treatment-row'); if (!row) return; row.classList.add('dragging'); // Store treatment data draggedTreatmentData = { id: row.dataset.id, name: row.dataset.name, price: parseFloat(row.dataset.price) }; // Set drag data (required for Firefox) event.dataTransfer.setData('text/plain', JSON.stringify(draggedTreatmentData)); event.dataTransfer.effectAllowed = 'copy'; }; window.innDragEnd = function(event) { const row = event.target.closest('.inn-treatment-row'); if (row) { row.classList.remove('dragging'); } draggedTreatmentData = null; }; window.innDragOver = function(event) { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; const dropZone = document.getElementById('innGuestTreatmentsList'); dropZone.classList.add('drag-over'); }; window.innDragLeave = function(event) { const dropZone = document.getElementById('innGuestTreatmentsList'); // Only remove class if we're actually leaving the drop zone if (!dropZone.contains(event.relatedTarget)) { dropZone.classList.remove('drag-over'); } }; window.innDrop = function(event) { event.preventDefault(); const dropZone = document.getElementById('innGuestTreatmentsList'); dropZone.classList.remove('drag-over'); // Use stored data or parse from dataTransfer let treatment = draggedTreatmentData; if (!treatment) { try { treatment = JSON.parse(event.dataTransfer.getData('text/plain')); } catch (e) { return; } } if (treatment && treatment.id && treatment.name) { // Add treatment to current guest state.guests[state.currentGuestIndex].treatments.push({ id: treatment.id, name: treatment.name, price: treatment.price }); innRenderGuestTreatments(); innShowToast(treatment.name + ' added to Guest ' + (state.currentGuestIndex + 1), 'success', 2000); } }; // ==================== CHECKOUT ==================== window.innProceedToCheckout = function() { innSaveGuestData(); // Validate all guests have name and surname for (let i = 0; i < state.guests.length; i++) { const guest = state.guests[i]; if (!guest.name || !guest.name.trim()) { // Switch to this guest state.currentGuestIndex = i; innRenderGuestForm(); // Focus on name field and add error styling const nameField = document.getElementById('innGuestName'); nameField.classList.add('inn-input-error'); nameField.focus(); // Remove error styling after user starts typing nameField.addEventListener('input', function() { this.classList.remove('inn-input-error'); }, { once: true }); innShowToast('All guests must have a name and surname filled in', 'error'); return; } if (!guest.surname || !guest.surname.trim()) { // Switch to this guest state.currentGuestIndex = i; innRenderGuestForm(); // Focus on surname field and add error styling const surnameField = document.getElementById('innGuestSurname'); surnameField.classList.add('inn-input-error'); surnameField.focus(); // Remove error styling after user starts typing surnameField.addEventListener('input', function() { this.classList.remove('inn-input-error'); }, { once: true }); innShowToast('All guests must have a name and surname filled in', 'error'); return; } } // Aggregate treatments for cart const treatmentCounts = {}; state.guests.forEach(guest => { guest.treatments.forEach(t => { if (!treatmentCounts[t.id]) { treatmentCounts[t.id] = { id: t.id, name: t.name, price: t.price, quantity: 0 }; } treatmentCounts[t.id].quantity++; }); }); // Build cart data const cartData = { accommodation: state.accommodation, unitTypeId: state.unitTypeId, checkIn: state.checkIn ? state.checkIn.toISOString().split('T')[0] : null, checkOut: state.checkOut ? state.checkOut.toISOString().split('T')[0] : null, adults: state.adults, children: state.children, guests: state.guests, treatments: Object.values(treatmentCounts), rateData: state.rateData }; // Also keep a copy client-side in case the user lands back on the // booking page (eg. via the WC "← Return to shop" link) so the // selection can be restored from localStorage later. try { localStorage.setItem('innBookingCart', JSON.stringify(cartData)); } catch (e) {} // Disable the button while the request is in flight so a double // click can't add the booking twice. const nextBtn = document.querySelector('.inn-btn-checkout'); if (nextBtn) { nextBtn.disabled = true; nextBtn.dataset.origLabel = nextBtn.textContent; nextBtn.textContent = 'Adding to cart…'; } const restoreBtn = function() { if (nextBtn) { nextBtn.disabled = false; nextBtn.textContent = nextBtn.dataset.origLabel || 'NEXT'; } }; const formData = new FormData(); formData.append('action', 'inn_add_booking_to_cart'); formData.append('nonce', innNonce); formData.append('booking', JSON.stringify(cartData)); fetch(ajaxUrl, { method: 'POST', body: formData, credentials: 'same-origin' }) .then(r => r.json()) .then(data => { if (!data || !data.success) { restoreBtn(); const msg = (data && data.data) ? data.data : 'Could not add the booking to the cart.'; innShowToast(msg, 'error'); return; } if (data.data.notice) { innShowToast(data.data.notice, 'warning', 6000); } const target = data.data.checkout_url || data.data.cart_url; if (!target) { restoreBtn(); innShowToast('Cart was updated but no checkout URL was returned.', 'error'); return; } window.location.href = target; }) .catch(err => { restoreBtn(); innShowToast('Network error: ' + (err && err.message ? err.message : err), 'error'); }); }; // ==================== INITIALIZATION ==================== // Initialize with tabs disabled (no accommodation selected) updateTabsState(); updateStayHero(); updateStepRail(); innRenderCalendars(); // Deep-link support: when the dropdown is pre-selected server-side // from a unit-id query string, fire the change handler so // availability and occupancy limits load. var innAccEl = document.getElementById('innAccommodation'); if (innAccEl && innAccEl.value && innAccEl.value !== 'none') { innAccEl.dispatchEvent(new Event('change', { bubbles: true })); } })();

Our Hydro House

Our private house is nestled on the lush slopes of the mountain, offering a tranquil escape from the hustle and bustle of everyday life. Adjacent to the main Hydro, this secluded retreat provides the ideal setting for up to six guests to unwind and recharge. The spacious interior is tastefully decorated, blending modern comforts with rustic charm. Guests can enjoy breathtaking views of the valley and vineyards from the comfort of the cozy living room or while lounging on the expansive deck. Whether sipping a delicious drink by the fireplace or soaking in the hot tub under a starlit sky, our house offers a luxurious and peaceful sanctuary for those seeking a memorable getaway.

What you will find in our Hydro House:

  • 3 bedrooms with king-size double beds
  • 3.5 en-suite bathrooms
  • Private hot tub
  • Separate Lounge with
  • Indoor wood-burning fireplace
  • Dining Room
  • Fully equipped, self-catering kitchen with gas stove and electric oven, refrigerator, freezer, microwave and blender
  • Dishwasher
  • Laundry with washer and dryer
  • Solar
  • Air conditioning, heating and cooling
  • Wi-Fi
  • HDTV smartTV with Netflix

Our Suites

Our suites offer a luxurious experience with a spacious bedroom, a well-appointed bathroom with a shower, and a private sitting room for added comfort and relaxation. These suites are designed to provide guests with a premium stay, combining sophisticated design and modern amenities for an indulgent accommodation experience.

Our suites offer a tranquil retreat for guests looking to unwind in style.

What you will find in our Suites:

  • Vineyard wing
  • Extra-length king-size bed
  • Separate Lounge
  • Smart TV
  • Wi-Fi
  • Airconditioning
  • Electric blankets
  • Kettle, bar fridge and herbal teas
  • Hair dryers
  • En-suite bathroom – with bath and shower
  • Balconies or patios with pool loungers

Our Executive Rooms

Our Executive Rooms offer a superior option for our guests, with a spacious, yet cozy and welcoming environment. These rooms include a stylish bedroom with a convenient open-plan bathroom and a balcony area for relaxation.

Our executive rooms provide a touch of elegance and comfort for a more elevated stay.

What you will find in our Executive Rooms:

  • Orchard wing
  • Extra-length beds
  • Smart TV
  • Wi-Fi
  • Airconditioning
  • Electric blankets
  • Kettle, bar fridge and herbal teas
  • Hair dryers
  • En-suite bathroom – 6 out of the 8 are open-plan, half with bath and shower
  • Balconies or patios with pool loungers

Our Standard Rooms

Our standard room is the perfect choice for budget-conscious guests looking for comfortable and affordable accommodation. Equipped with all the essential amenities, including a cozy bed, a private bathroom and a poolside outdoor space.

Our standard room provides a relaxing space to unwind.

What you will find in our Standard Rooms:

  • Vineyard and Orchard wings
  • King, Queen or 3/4 size beds
  • Flatscreen TV
  • Wi-Fi
  • Airconditioning
  • Electric blankets
  • Hair dryers
  • En-suite bathroom
  • Balconies or patios with pool loungers

Our Budget Rooms

Our budget room provides everything you need for a productive and relaxing wellness experience. This room is the perfect choice for budget-conscious guests looking for comfortable and affordable accommodation. Equipped with all the essential amenities, including a cozy bed, a private bathroom and a poolside outdoor space.

Our budget room provides a relaxing space to unwind.

What you will find in our Budget Rooms:

  • Vineyard wing and Main building next to Eco-Pool
  • 3/4 size bed
  • Flatscreen TV
  • Wi-Fi
  • Airconditioning
  • Electric blankets
  • Hair dryers
  • Balconies or patios with pool loungers

Freshness on Your Plate

We believe in promoting healthy eating practices from the ground up. Our commitment to fostering a culture of wellness extends beyond the confines of our kitchen to The Hydro produce garden.

Here, we cultivate a vibrant assortment of vegetables, herbs, spices and flowers, creating a living classroom for our guests to explore and learn from. By immersing themselves in this hands-on experience, our guests gain a deeper understanding of the connection between the food they consume, their overall health and the well-being of the ecosystem.

Our holistic approach inspires appreciation for oneself and the planet.

What's Good for You

From the Hydro’s beginning, we have understood that food has healing properties beyond just providing nourishment. Each bite we take could potentially be a form of medicine, supplying our bodies with essential nutrients that can benefit various bodily functions.

Packed with antioxidants, vitamins, and minerals, nutrient-rich foods have the ability to decrease inflammation, enhance the immune system and potentially lower the chances of developing chronic illnesses.

 

By embracing the concept of food as medicine, we nourish our bodies and improve our health, vitality and longevity.

 

Seasonal Freshness

Freshness plays a key role in maximising the nutritional benefits of whole foods. When we consume fruits and vegetables that are in season, we enjoy their peak flavour, reap optimum vitamins, minerals and antioxidants.

The vibrant colours and fragrant aromas of seasonal produce are a testament to their nutrient density and medicinal value.

Eating a variety of in-season whole foods allows us to nourish our bodies with a diverse array of nutrients that support our immune systems, promote healthy digestion, and protect us against chronic diseases.

Our Origins

The Hydro was founded by Cleto Saporetti in 1972 on High Rustenberg farm as a Natural Healing resort based on the ‘Nature Cure’ philosophy, emphasizing self-healing in the right environment.Boris Chaitow implemented a strict regime, including controlled fasting and a mainly raw vegetarian foods diet. Guests received daily massages, hydrotherapy, and osteopathic manipulation, along with exercise and relaxation.

They left feeling lighter, energized and revitalized.

The Hydro attracted guests from South Africa and Europe, who often booked in advance to be with their friends. Returning guests felt like part of the family, showing that some things at The Hydro have never changed.

Soothe Your Soul

At The Hydro, we know that good health leads to increased energy and vitality. It allows you to fully engage with your life and pursue your passions, leading to a more fulfilling existence.

Good health improves your ability to connect with others and form meaningful relationships, fostering positive interactions that enhance your sense of belonging.

With wellness comes a greater appreciation for life’s blessings, promoting gratitude and mindfulness.

Being healthy allows you to savour each experience and find joy in simple pleasures, providing contentment and harmony.

This holistic level of wellness is the ultimate achievement for mind, body and soul.

Put Your Mind At Rest

The prevalence of stress-related health challenges continues to rise at an alarming rate. In today’s fast-paced society, it is evident that mental well-being is not only essential for individual health and happiness, but crucial for building resilience and creating thriving, compassionate communities.

At The Hydro, we offer a range of effective programmes and treatments to address mental stress and fatigue.

Our approach focuses on promoting overall mental wellness and equipping you with the tools you need to manage stress effectively and achieve a balanced and fulfilling life.

 

Re-Calibrate Your Body

At The Hydro, we know that a healthy body thrives on a harmonious balance of stress management, nutrition and exercise. We believe that this holistic approach to wellness is key to achieving optimal health and vitality.

Our team offers a variety of treatments and therapies to nourish your body and mind, along with nutrient-rich meals. We provide indoor and outdoor exercise facilities, including yoga, Pilates, swimming, and hiking, to help you maintain an active lifestyle. Engaging in physical activities can boost your energy levels, improve your mood and enhance your overall well-being.


Let us help you recalibrate your body and embark on a journey towards a healthier, happier you.

 

Practice Mindfulness

Rooted in ancient meditation techniques, Mindfulness has evolved into a transformative mental practice that invites you to slow down, embrace the present moment with open-hearted awareness and free yourself from judgment and distractions. This process serves as a powerful tool for you to cultivates mental clarity, emotional balance, nurture inner peace and enhance your quality of life.

Our unique location at The Hydro naturally facilitates a natural quieting of the mind, reducing stress, increasing focus and promoting self-awareness.

We offer various scheduled workshops and sessions for experiencing Mindfulness first-hand, including Guided Meditation and Mindfulness classes.