Panda eating at Taiwan

2023 Ultimate Guide to Taiwanese Dining in Vancouver

As the moon festival approaches, it’s an excellent opportunity to explore the rich culinary tapestry of the Taiwanese cuisine in Vancouver.

For our dear guest, we’ve curated a list of popular and hidden Taiwanese restaurants in Vancouver and nearby —from comforting bowls of beef noodle soup to delectable desserts and modern hot pot experiences.

Table of Contents

Whether you’re yearning for traditional Moon Festival flavors or simply looking for a culinary adventure, these establishments offer something for everyone to enjoy.

Festive and Authentic (As listed in TripAdvisor)

RedBeef Noodle Kitchen

This image has an empty alt attribute; its file name is redbeef-noodle-kitchen-1024x576.webp

Photo by RedBeef Noodle Kitchen (via TripAdvisor)

Location: 1947 West 4th Avenue, Vancouver, BC V6J 1M7

RedBeef Noodle Kitchen stands out for its variety of Taiwanese beef noodle soups. Located in Vancouver city, it’s an ideal stop for anyone who enjoys a hearty bowl of noodles, especially during fall and winter. What sets the restaurant apart is its meticulous attention to detail in crafting their signature Taiwanese beef noodle soups. The restaurant features a modern interior, providing a comfortable space to relish your meal.

Corner 23

Photo by Corner 23 (via TripAdvisor)

Location: 4008 Cambie St, Vancouver, BC V5Z 2X8

For those living in Vancouver’s West Side, Corner 23 is the nearest option. Located in Cambie Village, Corner 23 is renowned for its Taiwanese-style bubble waffles and an extensive menu of traditional Taiwanese dishes.

Meetfresh

Photo by Meetfresh (via TripAdvisor)

Location: 1232 Robson St, Vancouver, BC V6E 1C1

Meetfresh is where dessert dreams come alive. The restaurant is a perfect venue for a casual dessert dates. It is Vancouver’s authentic Taiwanese desserts haven offering sweets like taro balls and herbal jelly. What makes it unique is its focus on both traditional and innovative desserts that provide a sweet journey through Taiwanese culture.

Wang’s Taiwan Beef Noodle House

Photo by Wang’s Noodle House (via TripAdvisor)

Location: 8390 Granville St, Vancouver, BC V6P 4Z7

At Wang’s Taiwan Beef Noodle House, you can expect traditional flavors and hearty meals. The restaurant specializes in Taiwanese beef noodle soup just like RedBeef, but with a more traditional and authentic tastes – combined with a “Taiwan-feel” and homey atmoshphere.

Tai One (By Bubbleworld)

Photo by Tai One (via TripAdvisor)

Location: 1947 W 4th Ave, Vancouver, BC V6J 1M7

Part of the Bubbleworld chain, Tai One elevates the experience by offering a broader range of classic Taiwanese dishes, often presented with a modern twist. What makes Tai One unique is it extends beyond just bubble tea – it includes a variety of soups, noodles, and other authentic Taiwanese delights. The dual identity makes the restaurant a destination for refreshments and indulging meals.

Taiwanese Restaurants & Takeaways Near Panda Pod

If you are hungry and want to eat inside Panda Pod, or looking for a nearby Taiwanese restaurant, the options below are great for the late night cravings.

Memory & Sweet Corner

Photo by Yelp

Location (Memory Corner): 4651 Garden City Rd #1110, Richmond, BC V6X 2K4

Location (Sweet Corner): 8080 Leslie Rd #130, Richmond, BC V6X 4A8

Memory Corner is a Richmond-based gem that serves traditional Taiwanese dishes. The restaurant is famous for their “Taiwanese Hot Pot,” a winter favorite among locals. It’s dessert counterpart Sweet Memory – serves traditional Taiwanese desserts offering a wide range of treats from fluffy buns to intricate cakes, often with a contemporary flair.

Joys Taiwanese Food

Photo by Joys Taiwanese Food (via Yelp)

Location: 4380 No 3 Road Richmond, BC V6X 2C3

Looking for a Taiwanese comfort food nearby? Joy’s Taiwanese Kitchen is a great choice for those who are craving. Located in a food court, it offers flavorful dishes such as braised pork rice and Taiwanese sausage. What sets Joy’s Taiwanese Kitchen apart is its commitment to traditional Taiwanese home cooking.

Uno Beef Noodle

Photo by Uno Beef Noodle (via Yelp)

Location: 135 – 2811 Number 3 Road Richmond, BC V6X 2B2

Uno Beef Noodle stands out for its dedication to perfecting a single dish: beef noodle soup. – crafting the dish using a traditional recipe refined over the years. The laid-back ambiance makes it an ideal spot for a relaxed meal, whether you’re grabbing lunch or settling in for dinner.

Pearl Castle Café

Photo by Pearl Castle Café (via Yelp)

Location: 3779 Sexsmith Road Suite 1128 Richmond, BC V6X 3Z9

Pearl Castle Café is a favorite among Richmond locals for its bubble tea and expansive menu of 100 dishes. It offers a diverse selection of Taiwanese staples such as noodle soups and popcorn chicken. What makes the restaurant popular is their bubble tea that features unique flavors like taro and lychee.

Boiling Point

Photo by Boiling Point (via Yelp)

Location: 4800 No 3 Road Suite 130 Richmond, BC V6X 3A6

Craving for a Taiwanese hot pot? The Boiling Point is the place to be. The restaurant offers the Taiwanese hot pot experience with its customizable options, a variety of broth and ingredient choices. The quality of the ingredients are served fresh with a variety of broths making Boiling Point a popular choice for most Vancouverites.

Our recommended highlights some of the well-regarded and popular Taiwanese restaurants in Vancouver and Richmond. From traditional to modern, the wide range of Taiwanese dishes available ensures that both our guest and locals can enjoy a wide range of mouth-watering options that celebrate the essence of Taiwanese cuisine.

  • September 5, 2023
  • Dining
  • Comments Off on 2023 Ultimate Guide to Taiwanese Dining in Vancouver

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