Panda skiing in Grouse Mountain Vancouver

Winter Wonderland at Whistler Blackcomb

Whistler Blackcomb converts into an enchanting winter wonderland during holiday season. Popular for its broad ski area, the resort also provides several activities, making it the perfect getaway for an unforgettable vacation. Whistler Village has appealing surroundings and joyful decorations, making it the heart of the celebration. It is adorned with bright lights and sparkling decors, making it a wonderful setting that will charm visitors of any age.

At the center of Whistler, the Village Walk, has changed into a winter wonderland with lights, decorations and a joyful ambiance. A walk in the village is a surreal experience, filled with happiness of the season.

 

Getting to Whistler from Panda Pod

Their bus tours available in downtown or online, but it’s highly recommended to rent a car rental from Panda POD (as an alternative YVR). The drive to Whistler is will take around 1.5-2 hours which depends on traffic and weather conditions. The drive is along the sea-sky highway (Highway 99), that is widely use for most BC locals..

 

 

Holiday Attractions

 

Whistler Blackcomb is one of the best skiing and snowboarding area in North America. With over 8,000 acres of snow slopes, the resort provides to all skill levels from beginners to experts. The resort and village are adorned with wonderful Christmas lights and decors that brings about a joyful atmosphere which is both magical and cozy.

 

Santa shows his presence in the village and on the slopes, which excites children to have fantastic opportunities for photo taking. Whistler Village has various holiday markets and shops, great for finding extraordinary gifts and souvenirs. The resort has several restaurants and eateries which has holiday menus that features traditional Christmas dishes and seasonal delicacies. Christmas carols and live music performances adds to the village’s festivities.

Whistler Blackcomb has several family activities, including ice skating, sledding, and the magical Whistler Vacation Experience, an indoor playground. The main attraction of the holidays is the Fire and Ice Show, a display of ski and snowboard tricks with fireworks and displays.

The resort has various New Year’s and New Year’s Eve celebrations, which includes family parties, fireworks, and a fire and ice show. Guests can take a gondola ride, snowshoeing, or just enjoy beautiful mountain views.

 

Pricing of Tickets

Prices may be more expensive during peak season, like winter skiing or summer hiking and mountain biking. Pre-booking transport can guarantee better deals. If you’re traveling with a group, look for discounts, most especially on transport services. For the most accurate prices, it is best to check the service providers or use online booking platforms to compare prices closer to your travel date. A Whistler trip is part of the experience, and the Sea-to-Sky Highway has some of the most spectacular views in British Columbia.

Spending the holidays at Whistler Blackcomb is a wonderful adventure. It is a place to enjoy and make happy memories. If you’re searching for thrilling winter sports, special activities, or a calm escape, Whistler Blackcomb is a perfect holiday destination. Know more about the best skiing resorts here.

  • December 16, 2023
  • Attractions
  • Comments Off on Winter Wonderland at Whistler Blackcomb

Tags

(function ppc5LoadWhenReady() { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", ppc5LoadWhenReady, { once: true }); return; } (function () { "use strict"; const currentScript = document.currentScript; const widgetRoots = Array.prototype.slice.call(document.querySelectorAll("#ppc5-app")); const root = (currentScript && currentScript.closest("#ppc5-app")) || widgetRoots.find(element => element.dataset.initialized !== "true") || null; if (!root || root.dataset.initialized === "true") return; root.dataset.initialized = "true"; /* ================================================================ PUBLIC DEMO CONFIGURATION — EDIT TEMPORARY DISPLAY VALUES HERE. Set DEMO_MODE to false only after Panda Pod's backend is ready. NEVER add credentials, private promo codes, API keys, or webhooks. ================================================================ */ const DEMO_MODE = true; const DEMO_CONFIG = { timezone: "America/Vancouver", sameDayBookingCutoff: "18:00", defaultDailyCrewAllocation: 5, offerEnabled: true, ratePeriods: [ { validFrom: "2026-08-15", validTo: "2026-09-30", currency: "CAD", taxDisplay: "tax included", lowerPodRate: 57, upperPodRate: 55 } ], allocationOverrides: { "2026-08-22": 0, "2026-08-23": 8, "2026-08-24": 5 }, demoCrewBooked: { "2026-08-24": 5 } }; const API = { config: "/api/v1/crew/public-config", availability: "/api/v1/crew/availability", reservations: "/api/v1/crew/reservations" }; const ROOM_TYPES = Object.freeze({ lower: { roomTypeID: "169530", label: "Lower Pod" }, upper: { roomTypeID: "169527", label: "Upper Pod" } }); const WHATSAPP = "https://wa.me/message/BQOHZPK5JFQCP1?src=qr"; let config = null; let currentStep = 1; let availability = null; let reservationComplete = false; let crewIdPreviewUrls = []; let cardAuthorizationOpened = false; const $ = (selector, context = root) => context.querySelector(selector); const $$ = (selector, context = root) => Array.from(context.querySelectorAll(selector)); async function getCrewPublicConfig() { if (DEMO_MODE) return JSON.parse(JSON.stringify(DEMO_CONFIG)); const response = await fetch(API.config, { credentials: "same-origin", headers: { Accept: "application/json" } }); if (!response.ok) throw new Error("Public crew offer is temporarily unavailable."); return response.json(); } function getVancouverTime() { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: (config && config.timezone) || "America/Vancouver", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }).formatToParts(new Date()); return Object.fromEntries(parts.filter(part => part.type !== "literal").map(part => [part.type, part.value])); } function getVancouverDate() { const time = getVancouverTime(); return `${time.year}-${time.month}-${time.day}`; } function getSameDayCutoffState() { const time = getVancouverTime(); const [cutoffHour, cutoffMinute] = String((config && config.sameDayBookingCutoff) || "18:00").split(":").map(Number); const currentMinutes = Number(time.hour) * 60 + Number(time.minute); return { date: `${time.year}-${time.month}-${time.day}`, closed: currentMinutes >= cutoffHour * 60 + cutoffMinute }; } function addDays(value, count) { const date = new Date(value + "T12:00:00Z"); date.setUTCDate(date.getUTCDate() + count); return date.toISOString().slice(0, 10); } function dateRange(start, end) { const dates = []; const cursor = new Date(start + "T12:00:00Z"); const stop = new Date(end + "T12:00:00Z"); while (cursor < stop) { dates.push(cursor.toISOString().slice(0, 10)); cursor.setUTCDate(cursor.getUTCDate() + 1); } return dates; } function findRatePeriod(start, end) { return config.ratePeriods.find(period => start >= period.validFrom && end <= addDays(period.validTo, 1)) || null; } function formatDate(value) { return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" }).format(new Date(value + "T12:00:00Z")); } function formatTime(value) { if (!value) return "—"; const [hours, minutes] = value.split(":").map(Number); return `${hours % 12 || 12}:${String(minutes).padStart(2, "0")} ${hours >= 12 ? "PM" : "AM"}`; } function sanitizePhone(value) { const leadingPlus = String(value).startsWith("+"); const allowed = String(value).replace(/[^0-9+()]/g, "").replace(/\+/g, ""); return `${leadingPlus ? "+" : ""}${allowed}`; } function phoneDigits(value) { const digits = String(value).replace(/\D/g, ""); return digits.length === 11 && digits.startsWith("1") ? digits.slice(1) : digits; } function formatPhone(value) { const digits = phoneDigits(value); return digits.length === 10 ? `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}` : String(value); } function updateEtaValue() { const hourSelect = $("#ppc5-eta-hour"); const minuteSelect = $("#ppc5-eta-minute"); const selectedPeriod = $("input[name=etaPeriod]:checked"); const period = selectedPeriod ? selectedPeriod.value : "AM"; const eta = $("#ppc5-eta"); if (!hourSelect.value || !minuteSelect.value) { eta.value = ""; return; } let hour = Number(hourSelect.value) % 12; if (period === "PM") hour += 12; eta.value = `${String(hour).padStart(2, "0")}:${minuteSelect.value}`; hourSelect.removeAttribute("aria-invalid"); minuteSelect.removeAttribute("aria-invalid"); $("#ppc5-eta-error").textContent = ""; } function money(value, currency = "CAD") { return new Intl.NumberFormat("en-CA", { style: "currency", currency, minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(value); } function selectedPod() { const input = $("input[name=podType]:checked"); return input ? { type: input.value, roomTypeID: input.dataset.roomId, label: ROOM_TYPES[input.value].label } : null; } async function checkCrewAvailability({ checkInDate, checkOutDate, roomTypeID }) { if (!DEMO_MODE) { const query = new URLSearchParams({ checkInDate, checkOutDate, roomTypeID }); const response = await fetch(`${API.availability}?${query}`, { credentials: "same-origin", headers: { Accept: "application/json" } }); if (!response.ok) throw new Error("We could not confirm crew availability."); return response.json(); } await new Promise(resolve => window.setTimeout(resolve, 350)); const podType = Object.keys(ROOM_TYPES).find(key => ROOM_TYPES[key].roomTypeID === roomTypeID); const nights = dateRange(checkInDate, checkOutDate).map(date => { const allocation = Object.prototype.hasOwnProperty.call(config.allocationOverrides, date) ? config.allocationOverrides[date] : config.defaultDailyCrewAllocation; const booked = config.demoCrewBooked[date] || (Number(date.slice(-2)) % 3); let rooms = 6 - (Number(date.slice(-2)) % 4); if ((podType === "lower" && date.endsWith("27")) || (podType === "upper" && date.endsWith("28"))) rooms = 0; return { date, crewAllocation: allocation, crewBooked: booked, crewRemaining: Math.max(0, allocation - booked), cloudbedsRoomsAvailable: rooms }; }); return { available: nights.every(night => night.crewAllocation > 0 && night.crewRemaining > 0 && night.cloudbedsRoomsAvailable > 0), selectedRoomTypeID: roomTypeID, selectedPodType: podType, nights }; } async function createCrewReservation(payload) { if (DEMO_MODE) { await new Promise(resolve => window.setTimeout(resolve, 550)); const cost = calculateCosts(); return { success: true, reservationNumber: `PP-DEMO-${Date.now().toString().slice(-6)}`, paymentStatus: "PENDING", amountDue: cost.total, currency: cost.currency }; } const response = await fetch(API.reservations, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) throw new Error("The reservation could not be created. No booking was made."); return response.json(); } function setError(element, message) { element.setAttribute("aria-invalid", message ? "true" : "false"); const field = element.closest(".ppc5-field"); const holder = field ? field.querySelector(".ppc5-error") : null; if (holder) holder.textContent = message || ""; } function clearErrors(scope = root) { $$("[aria-invalid='true']", scope).forEach(element => element.removeAttribute("aria-invalid")); $$(".ppc5-error", scope).forEach(element => { element.textContent = ""; }); } function focusFirstInvalid(scope) { const element = $("[aria-invalid='true']", scope) || $$(".ppc5-error", scope).find(item => item.textContent); if (!element) return false; element.scrollIntoView({ behavior: "smooth", block: "center" }); const fieldset = element.closest("fieldset"); const focusTarget = element.matches("input,select,button") ? element : (fieldset ? fieldset.querySelector("input") : null) || element; if (typeof focusTarget.focus === "function") focusTarget.focus({ preventScroll: true }); return true; } function validateStep(step) { const panel = $(`.ppc5-panel[data-step="${step}"]`); clearErrors(panel); if (step === 1) { const checkIn = $("#ppc5-checkin"); const checkOut = $("#ppc5-checkout"); const cutoff = getSameDayCutoffState(); const today = cutoff.date; if (!checkIn.value) setError(checkIn, "Choose a check-in date."); else if (checkIn.value < today) setError(checkIn, "Check-in cannot be in the past."); else if (checkIn.value === today && cutoff.closed) { setError(checkIn, "Same-day booking is closed after 6:00 PM. Please contact us on WhatsApp."); showSameDayClosedMessage(); } if (!checkOut.value) setError(checkOut, "Choose a check-out date."); else if (checkIn.value && checkOut.value <= checkIn.value) setError(checkOut, "Check-out must be after check-in."); if (!selectedPod()) { $("#ppc5-pod-error").textContent = "Choose either a Lower Pod or Upper Pod."; $$("input[name=podType]").forEach(input => input.setAttribute("aria-invalid", "true")); } } else if (step === 2) { $$("input[required],select[required]", panel).filter(element => element.type !== "radio").forEach(element => { if (!element.value.trim()) setError(element, "This field is required."); else if (element.type === "email" && !/^\S+@\S+\.\S+$/.test(element.value)) setError(element, "Enter a valid email address."); else if (element.name === "phone" && (!/^\+?[0-9()]+$/.test(element.value) || phoneDigits(element.value).length !== 10)) setError(element, "Enter a 10-digit phone number using numbers and optional + or parentheses."); }); if (!$("input[name=returningCrew]:checked")) { $("#ppc5-returning-error").textContent = "Please select one option."; $$("input[name=returningCrew]").forEach(input => input.setAttribute("aria-invalid", "true")); } else if ($("input[name=returningCrew]:checked").value === "false" && !$("#ppc5-crew-id-file").files.length) { setError($("#ppc5-crew-id-file"), "Select one or more clear RAIC or crew ID images."); } } else if (step === 3) { updateEtaValue(); if (!$("#ppc5-eta").value) { $("#ppc5-eta-error").textContent = "Choose an arrival hour and 15-minute interval."; if (!$("#ppc5-eta-hour").value) $("#ppc5-eta-hour").setAttribute("aria-invalid", "true"); if (!$("#ppc5-eta-minute").value) $("#ppc5-eta-minute").setAttribute("aria-invalid", "true"); } if (!$("input[name=earlyRequested]:checked")) { $("#ppc5-early-error").textContent = "Please select one early check-in option."; $$("input[name=earlyRequested]").forEach(input => input.setAttribute("aria-invalid", "true")); } if (!$("input[name=lateRequested]:checked")) { $("#ppc5-late-error").textContent = "Please select one late checkout option."; $$("input[name=lateRequested]").forEach(input => input.setAttribute("aria-invalid", "true")); } } else if (step === 4) { if (!cardAuthorizationOpened) { $("#ppc5-card-error").textContent = "Open the secure authorization page and complete both card forms first."; $("#ppc5-card-authorization-link").setAttribute("aria-invalid", "true"); } else if (!$("#ppc5-card-complete").checked) { $("#ppc5-card-error").textContent = "Confirm that you completed both secure credit card forms."; $("#ppc5-card-complete").setAttribute("aria-invalid", "true"); } } else if (step === 5 && !$("input[name=policyAccepted]").checked) { $("#ppc5-policy-error").textContent = "You must accept the crew booking terms and hotel policies."; $("input[name=policyAccepted]").setAttribute("aria-invalid", "true"); } return !focusFirstInvalid(panel); } function showStep(step) { currentStep = step; $$(".ppc5-panel").forEach(panel => { const active = Number(panel.dataset.step) === step; panel.classList.toggle("is-active", active); panel.hidden = !active; }); $$(".ppc5-step-tab").forEach((tab, index) => { const number = index + 1; tab.classList.toggle("is-active", number === step); tab.classList.toggle("is-done", number < step); tab.toggleAttribute("disabled", number > step); tab.removeAttribute("aria-current"); if (number === step) tab.setAttribute("aria-current", "step"); }); $("#ppc5-offer").classList.toggle("ppc5-hidden", step !== 1); if (step === 3) updateRequestCharges(); if (step === 4) prepareCardAuthorization(); if (step === 5) renderReview(); const heading = $(`.ppc5-panel[data-step="${step}"] h2`); heading.setAttribute("tabindex", "-1"); heading.focus(); root.scrollIntoView({ behavior: "smooth", block: "start" }); } function getFormData() { return Object.fromEntries(new FormData($("#ppc5-form")).entries()); } function prepareCardAuthorization() { const authorizationUrl = new URL("https://pandapodhotels.com/crewcc/"); authorizationUrl.searchParams.set("checkInDate", $("#ppc5-checkin").value); authorizationUrl.searchParams.set("source", "crew-booking"); $("#ppc5-card-authorization-link").href = authorizationUrl.toString(); } function calculateCosts() { const data = getFormData(); const pod = selectedPod(); const rate = pod && data.checkInDate && data.checkOutDate ? findRatePeriod(data.checkInDate, data.checkOutDate) : null; if (!rate) return { nightly: 0, nights: 0, stay: 0, early: 0, late: 0, total: 0, currency: "CAD", taxDisplay: "" }; const nightly = pod.type === "lower" ? rate.lowerPodRate : rate.upperPodRate; const nights = dateRange(data.checkInDate, data.checkOutDate).length; const stay = nightly * nights; const early = data.earlyRequested === "full_day" ? nightly : data.earlyRequested === "half_day" ? nightly / 2 : 0; const late = data.lateRequested === "full_day" ? nightly : data.lateRequested === "half_day" ? nightly / 2 : 0; return { nightly, nights, stay, early, late, total: stay + early + late, currency: rate.currency, taxDisplay: rate.taxDisplay }; } function updateRequestCharges() { const cost = calculateCosts(); const data = getFormData(); const early = $("#ppc5-early-charge"); const late = $("#ppc5-late-charge"); early.classList.toggle("ppc5-hidden", !data.earlyRequested || data.earlyRequested === "none"); late.classList.toggle("ppc5-hidden", !data.lateRequested || data.lateRequested === "none"); if (!early.classList.contains("ppc5-hidden")) early.textContent = `Estimated early check-in charge: ${money(cost.early, cost.currency)} CAD`; if (!late.classList.contains("ppc5-hidden")) late.textContent = `Estimated late checkout charge: ${money(cost.late, cost.currency)} CAD`; } function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, character => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ }[character])); } function renderReview() { const data = getFormData(); const pod = selectedPod(); const cost = calculateCosts(); const earlyLabel = data.earlyRequested === "full_day" ? "Before 11:00 AM — full-day rate" : data.earlyRequested === "half_day" ? "After 11:00 AM — half-day rate" : "Not requested"; const lateLabel = data.lateRequested === "full_day" ? "After 3:00 PM — full-day rate" : data.lateRequested === "half_day" ? "By 3:00 PM — half-day rate" : "Not requested"; const rows = [ ["Check-in", formatDate(data.checkInDate)], ["Check-out", formatDate(data.checkOutDate)], ["Number of nights", cost.nights], ["Pod type", pod.label], ["Nightly crew rate", `${money(cost.nightly, cost.currency)} CAD / night (${cost.taxDisplay})`], ["Stay subtotal", `${money(cost.stay, cost.currency)} CAD`], ["Guest", `${data.firstName} ${data.lastName}`], ["Email", data.email], ["Phone", formatPhone(data.phone)], ["Airline / Company", data.airline], ["Crew role", $("select[name=crewRole] option:checked").textContent], ["ETA", formatTime(data.eta)], ["Early check-in", `${earlyLabel} — ${money(cost.early, cost.currency)} CAD`], ["Late checkout", `${lateLabel} — ${money(cost.late, cost.currency)} CAD`], ["Damage deposit authorization", data.cardAuthorizationComplete === "on" ? "Completed by guest" : "Not completed"], ["Estimated total", `${money(cost.total, cost.currency)} CAD (${cost.taxDisplay})`] ]; $("#ppc5-review").innerHTML = rows.map(([key, value]) => `
${escapeHtml(key)}${escapeHtml(value)}
`).join(""); } function resultCard(title, message, success = false) { return `

${escapeHtml(title)}

${escapeHtml(message)}

${success ? "" : `Contact Us on WhatsApp`}`; } function showSameDayClosedMessage() { const box = $("#ppc5-availability-result"); box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Same-Day Booking Closed", "It is after 6:00 PM in Vancouver. Please contact us on WhatsApp, mention that you are a crew member, and ask for Front Desk."); box.focus(); } function availabilityFailure(result) { if (result.nights.some(night => night.crewAllocation === 0)) return ["Crew Offer Unavailable for Selected Date", "Crew self-booking is not available for one or more selected dates. Please contact us on WhatsApp for alternate availability."]; if (result.nights.some(night => night.crewRemaining === 0)) return ["Crew Allocation Reached", "The crew allocation for one or more selected dates has been reached. Please contact us and we will check whether additional pods can be offered."]; return ["Crew Availability Is Not Available", "Crew availability is not available for one or more selected dates. Please contact us and we will check other options."]; } async function runAvailability(advance) { if (!validateStep(1)) return false; const data = getFormData(); const pod = selectedPod(); const box = $("#ppc5-availability-result"); const button = $("#ppc5-check-availability"); availability = null; box.classList.add("ppc5-hidden"); if (!config) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Crew Offer Is Still Loading", "Please wait a moment, then check availability again."); box.focus(); return false; } if (!config.offerEnabled) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Crew Offer Unavailable", "Crew self-booking is currently unavailable. Please contact us on WhatsApp."); box.focus(); return false; } if (!findRatePeriod(data.checkInDate, data.checkOutDate)) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Crew Offer Unavailable for Selected Date", "The crew offer does not cover all selected stay dates. Please contact us for alternate availability."); box.focus(); return false; } button.disabled = true; button.textContent = "Checking…"; try { const result = await checkCrewAvailability({ checkInDate: data.checkInDate, checkOutDate: data.checkOutDate, roomTypeID: pod.roomTypeID }); availability = result; if (result.available) { const minimum = Math.min(...result.nights.map(night => Math.min(night.crewRemaining, night.cloudbedsRoomsAvailable))); box.className = "ppc5-result ppc5-result--success"; box.innerHTML = resultCard("Crew Availability Confirmed", `${pod.label} is available for all ${result.nights.length} night${result.nights.length === 1 ? "" : "s"}. At least ${minimum} crew pod${minimum === 1 ? "" : "s"} remain per selected night.`, true); $("#ppc5-live").textContent = "Crew availability confirmed for every selected night."; if (advance) showStep(2); } else { const [title, message] = availabilityFailure(result); box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard(title, message); box.focus(); } return result.available; } catch (error) { box.className = "ppc5-result ppc5-result--error"; box.innerHTML = resultCard("Availability Check Unavailable", "We could not safely confirm availability. No booking was made. Please retry or contact us on WhatsApp."); box.focus(); return false; } finally { button.disabled = false; button.textContent = "Check Availability"; } } function buildPayload() { const data = getFormData(); const pod = selectedPod(); return { checkInDate: data.checkInDate, checkOutDate: data.checkOutDate, roomTypeID: pod.roomTypeID, podType: pod.type, guest: { firstName: data.firstName.trim(), lastName: data.lastName.trim(), email: data.email.trim(), phone: data.phone.trim(), airline: data.airline.trim(), crewRole: data.crewRole, returningCrew: data.returningCrew === "true" }, arrival: { eta: data.eta, earlyCheckInRequested: data.earlyRequested !== "none", earlyCheckInOption: data.earlyRequested, earlyCheckInTime: null, lateCheckOutRequested: data.lateRequested !== "none", lateCheckOutOption: data.lateRequested, lateCheckOutTime: null }, damageDepositAuthorizationConfirmed: data.cardAuthorizationComplete === "on", policyAccepted: true }; } async function submitBooking(event) { event.preventDefault(); if (!validateStep(5)) return; // Recheck immediately before creation in case the guest crossed 6:00 PM during the form. const cutoff = getSameDayCutoffState(); if ($("#ppc5-checkin").value === cutoff.date && cutoff.closed) { showStep(1); setError($("#ppc5-checkin"), "Same-day booking is closed after 6:00 PM. Please contact us on WhatsApp."); showSameDayClosedMessage(); return; } const button = $("#ppc5-create"); const errorBox = $("#ppc5-submit-error"); errorBox.classList.add("ppc5-hidden"); button.disabled = true; button.textContent = "Creating Reservation…"; try { // FAIL CLOSED: first-time crew images require an approved private upload API. // Do not remove this guard until the backend securely stores the image and // returns an opaque upload token that can be linked to the reservation. const returningCrewSelection = $("input[name=returningCrew]:checked"); if (!DEMO_MODE && returningCrewSelection && returningCrewSelection.value === "false") { throw new Error("Secure RAIC/Crew ID image storage is not connected. No image or reservation was sent."); } if (!(await runAvailability(false))) throw new Error("Availability changed. Please return to Step 1 and review the message."); const payload = buildPayload(); const response = await createCrewReservation(payload); if (!response || !response.success || !response.reservationNumber) throw new Error("The reservation could not be created."); const pod = selectedPod(); const rows = [ ["Reservation number", response.reservationNumber], ["Check-in", formatDate(payload.checkInDate)], ["Check-out", formatDate(payload.checkOutDate)], ["Pod type", pod.label], ["Amount due", response.amountDue != null ? `${money(response.amountDue, response.currency || "CAD")} CAD` : "Provided by Panda Pod payment request"], ["Payment status", response.paymentStatus || "PENDING"] ]; $("#ppc5-success-summary").innerHTML = rows.map(([key, value]) => `
${escapeHtml(key)}${escapeHtml(value)}
`).join(""); reservationComplete = true; $("#ppc5-booking-ui").hidden = true; $("#ppc5-booking-ui").classList.add("ppc5-hidden"); $("#ppc5-success").hidden = false; $("#ppc5-success").classList.remove("ppc5-hidden"); $("#ppc5-success").focus(); root.scrollIntoView({ behavior: "smooth", block: "start" }); } catch (error) { showStep(5); errorBox.innerHTML = resultCard("Reservation Not Created", error.message || "We could not safely create the reservation. Please retry or contact us on WhatsApp."); errorBox.classList.remove("ppc5-hidden"); errorBox.focus(); } finally { button.disabled = false; button.textContent = "Create Reservation"; } } function invalidateAvailability() { availability = null; cardAuthorizationOpened = false; $("#ppc5-card-complete").checked = false; $("#ppc5-availability-result").classList.add("ppc5-hidden"); } function clearCrewIdSelection() { const input = $("#ppc5-crew-id-file"); input.value = ""; input.removeAttribute("aria-invalid"); $("#ppc5-upload-error").textContent = ""; $("#ppc5-upload-status").classList.add("ppc5-hidden"); crewIdPreviewUrls.forEach(url => URL.revokeObjectURL(url)); crewIdPreviewUrls = []; $("#ppc5-upload-previews").innerHTML = ""; } function handleCrewIdSelection(event) { const input = event.currentTarget; const files = Array.from(input.files || []); clearErrors($("#ppc5-crew-id-upload")); if (!files.length) { clearCrewIdSelection(); return; } const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/heic", "image/heif", ""]; const invalidType = files.find(file => !allowedTypes.includes(file.type) || !/\.(jpe?g|png|webp|heic|heif)$/i.test(file.name)); if (invalidType) { clearCrewIdSelection(); setError(input, `${invalidType.name} is not a supported JPG, PNG, WEBP, HEIC, or HEIF image.`); return; } const oversized = files.find(file => file.size > 10 * 1024 * 1024); if (oversized) { clearCrewIdSelection(); setError(input, `${oversized.name} must be 10 MB or smaller.`); return; } crewIdPreviewUrls.forEach(url => URL.revokeObjectURL(url)); crewIdPreviewUrls = []; const previews = $("#ppc5-upload-previews"); previews.innerHTML = ""; files.forEach(file => { const url = URL.createObjectURL(file); crewIdPreviewUrls.push(url); const item = document.createElement("div"); item.className = "ppc5-upload-preview-item"; const image = document.createElement("img"); image.className = "ppc5-upload-preview"; image.src = url; image.alt = "Selected crew ID preview"; const details = document.createElement("div"); details.className = "ppc5-upload-file"; const name = document.createElement("strong"); name.textContent = file.name; const size = document.createElement("small"); size.textContent = `${(file.size / 1024 / 1024).toFixed(1)} MB · selected locally`; details.append(name, size); item.append(image, details); previews.appendChild(item); }); $("#ppc5-upload-status").classList.remove("ppc5-hidden"); $("#ppc5-live").textContent = `${files.length} crew ID image${files.length === 1 ? "" : "s"} selected.`; } function wireEvents() { $("#ppc5-check-availability").addEventListener("click", () => runAvailability(true)); $("#ppc5-form").addEventListener("submit", submitBooking); $("#ppc5-phone").addEventListener("input", event => { event.target.value = sanitizePhone(event.target.value); }); $$('[data-date-field]').forEach(field => field.addEventListener("click", event => { const input = $("input[type=date]", field); if (event.target === input) { const editableTextWidth = Math.min(160, Math.max(110, input.clientWidth * .45)); if (event.offsetX <= editableTextWidth) return; event.preventDefault(); try { if (typeof input.showPicker === "function") input.showPicker(); else input.focus(); } catch (error) { input.focus(); } return; } event.preventDefault(); try { if (typeof input.showPicker === "function") input.showPicker(); else { input.focus(); input.click(); } } catch (error) { input.focus(); } })); $$("[data-next]").forEach(button => button.addEventListener("click", () => { if (validateStep(currentStep)) showStep(Number(button.dataset.next)); })); $$("[data-back]").forEach(button => button.addEventListener("click", () => showStep(Number(button.dataset.back)))); $$("[data-go-step]").forEach(button => button.addEventListener("click", () => { if (!button.disabled) showStep(Number(button.dataset.goStep)); })); $$("input[name=returningCrew]").forEach(input => input.addEventListener("change", () => { const firstTime = input.value === "false"; $("#ppc5-first-time-note").classList.toggle("ppc5-hidden", !firstTime); $("#ppc5-crew-id-upload").classList.toggle("ppc5-hidden", !firstTime); $("#ppc5-crew-id-file").toggleAttribute("required", firstTime); if (!firstTime) clearCrewIdSelection(); })); $("#ppc5-crew-id-file").addEventListener("change", handleCrewIdSelection); $("#ppc5-upload-remove").addEventListener("click", clearCrewIdSelection); $("#ppc5-card-authorization-link").addEventListener("click", () => { cardAuthorizationOpened = true; $("#ppc5-card-authorization-link").removeAttribute("aria-invalid"); $("#ppc5-card-error").textContent = ""; $("#ppc5-live").textContent = "Secure card authorization opened in a new tab. Complete both forms, then return here."; }); $("#ppc5-card-complete").addEventListener("change", () => { if ($("#ppc5-card-complete").checked) { $("#ppc5-card-complete").removeAttribute("aria-invalid"); $("#ppc5-card-error").textContent = ""; } }); $("#ppc5-eta-hour").addEventListener("change", updateEtaValue); $("#ppc5-eta-minute").addEventListener("change", updateEtaValue); $$("input[name=etaPeriod]").forEach(input => input.addEventListener("change", updateEtaValue)); $$("input[name=earlyRequested],input[name=lateRequested]").forEach(input => input.addEventListener("change", updateRequestCharges)); $$("input[name=podType],#ppc5-checkin,#ppc5-checkout").forEach(input => input.addEventListener("change", invalidateAvailability)); $("#ppc5-new-booking").addEventListener("click", () => window.location.reload()); } async function init() { wireEvents(); try { config = await getCrewPublicConfig(); const period = config.ratePeriods[0]; if (period) { $("#ppc5-rate-validity").textContent = `Valid ${formatDate(period.validFrom)} – ${formatDate(period.validTo)}`; $("#ppc5-lower-rate").textContent = money(period.lowerPodRate, period.currency); $("#ppc5-upper-rate").textContent = money(period.upperPodRate, period.currency); $("#ppc5-lower-option-rate").textContent = `${money(period.lowerPodRate, period.currency)} CAD / night`; $("#ppc5-upper-option-rate").textContent = `${money(period.upperPodRate, period.currency)} CAD / night`; $("#ppc5-tax-display").textContent = `Displayed rates: ${period.taxDisplay}. Rates are subject to availability.`; } const today = getVancouverDate(); $("#ppc5-checkin").min = today; $("#ppc5-checkout").min = addDays(today, 1); } catch (error) { $("#ppc5-offer").innerHTML = `

Crew booking is temporarily unavailable

Please retry or contact us on WhatsApp, mention that you are a crew member, and ask for Front Desk.

`; $("#ppc5-form").hidden = true; } } init(); }()); }());
Panda Pod Hotel
Concierge Panda
Panda Pod Hotel Assistant