403Webshell
Server IP : 173.209.174.21  /  Your IP : 216.73.216.89
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64
User : nodor ( 1000)
PHP Version : 8.3.6
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/grangestation/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/grangestation//script.js
const photos = [
  { src: "assets/building-wide.jpg", caption: "Original Grange Supply Co exterior and porch." },
  { src: "assets/interior1.jpg", caption: "Restored interior with open rafters, string lighting, and hardwood floors." },
  { src: "assets/tree-wagon-wheel.jpg", caption: "Shaded tree area with rustic wagon wheel detail." },
  { src: "assets/railroad-florals.jpg", caption: "Outdoor details near the railroad backdrop." },
  { src: "assets/dessert-wall.jpg", caption: "Warm wood wall and dessert table setup." },
  { src: "assets/bar-detail.jpg", caption: "Vintage serving details for receptions and private events." }
];

let siteConfig = getSiteConfig();
let galleryPhotos = activeGalleryPhotos();

const blockedDates = new Set(["2026-06-20", "2026-07-04", "2026-08-15"]);
const storageKey = "grangeStationRequests";
const confirmedStorageKey = "grangeStationConfirmedBookings";
const rateStorageKey = "grangeStationSubmissionLog";
const cropStorageKey = "grangeStationGalleryCrops";
const minimumSubmitSeconds = 8;
let currentPhoto = 0;
let selectedCropPhoto = 0;

const defaultCrops = [
  { x: 28, y: 50, zoom: 1, cols: 1, rows: 2, fit: "cover" },
  { x: 50, y: 60, zoom: 1, cols: 1, rows: 1, fit: "cover" },
  { x: 58, y: 52, zoom: 1, cols: 1, rows: 1, fit: "cover" },
  { x: 50, y: 62, zoom: 1, cols: 2, rows: 1, fit: "cover" },
  { x: 50, y: 64, zoom: 1, cols: 1, rows: 1, fit: "cover" },
  { x: 62, y: 58, zoom: 1, cols: 1, rows: 1, fit: "cover" }
];

const header = document.querySelector("[data-header]");
const menuToggle = document.querySelector("[data-menu-toggle]");
const nav = document.querySelector("[data-nav]");
const form = document.querySelector("[data-booking-form]");
const contactForm = document.querySelector("[data-contact-form]");
const estimate = document.querySelector("[data-estimate]");
const estimateNote = document.querySelector("[data-estimate-note]");
const estimateBreakdown = document.querySelector("[data-estimate-breakdown]");
const availability = document.querySelector("[data-availability]");
const confirmedSlots = document.querySelector("[data-confirmed-slots]");
const dateInput = document.querySelector("[data-date-input]");
const bookingCalendar = document.querySelector("[data-booking-calendar]");
const calendarMonthLabel = document.querySelector("[data-calendar-month]");
const calendarGrid = document.querySelector("[data-calendar-grid]");
const calendarDetails = document.querySelector("[data-calendar-details]");
const requestList = document.querySelector("[data-request-list]");
const toast = document.querySelector("[data-toast]");
const mailto = document.querySelector("[data-mailto]");
const lightbox = document.querySelector("[data-lightbox]");
const lightboxImage = document.querySelector("[data-lightbox-image]");
const lightboxCaption = document.querySelector("[data-lightbox-caption]");
const galleryThumbs = document.querySelector("[data-gallery-thumbs]");
const formStartedAt = document.querySelector("[data-form-started]");
const contactStartedAt = document.querySelector("[data-contact-started]");
const fullDayInput = document.querySelector("[data-full-day]");
const cropEditor = document.querySelector("[data-crop-editor]");
const cropPhotoSelect = document.querySelector("[data-crop-photo]");
const cropPreview = document.querySelector("[data-crop-preview]");
const cropX = document.querySelector("[data-crop-x]");
const cropY = document.querySelector("[data-crop-y]");
const cropZoom = document.querySelector("[data-crop-zoom]");
const cropCols = document.querySelector("[data-crop-cols]");
const cropRows = document.querySelector("[data-crop-rows]");
const cropFit = document.querySelector("[data-crop-fit]");
let calendarMonth = new Date();
calendarMonth.setDate(1);
let liveConfirmedBookings = null;
let liveAvailabilityBlocks = null;
let livePendingRequests = null;
let apiAvailable = false;

async function apiRequest(action, payload) {
  const options = payload
    ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }
    : {};
  const response = await fetch(`api/index.php?action=${encodeURIComponent(action)}`, options);
  const data = await response.json();
  if (!response.ok || !data.ok) throw new Error(data.detail || data.error || "Request failed.");
  return data;
}

async function loadLiveData() {
  try {
    const data = await apiRequest("bootstrap");
    apiAvailable = true;
    siteConfig = mergeConfig(data.settings || {});
    galleryPhotos = (siteConfig.gallery || []).filter((photo) => photo.enabled !== false);
    liveConfirmedBookings = data.confirmedBookings || [];
    liveAvailabilityBlocks = data.availabilityBlocks || [];
    livePendingRequests = data.pendingRequests || [];
  } catch (error) {
    apiAvailable = false;
  }
}

function showToast(message) {
  toast.textContent = message;
  toast.classList.add("visible");
  window.setTimeout(() => toast.classList.remove("visible"), 2800);
}

function getRequests() {
  return JSON.parse(localStorage.getItem(storageKey) || "[]");
}

function saveRequests(requests) {
  localStorage.setItem(storageKey, JSON.stringify(requests));
}

function defaultConfirmedBookings() {
  return [
    { id: "confirmed-2026-06-20", date: "2026-06-20", start: "14:00", end: "22:00", title: "Confirmed private event" },
    { id: "confirmed-2026-07-04", date: "2026-07-04", start: "10:00", end: "18:00", title: "Confirmed holiday booking" },
    { id: "confirmed-2026-08-15", date: "2026-08-15", start: "15:00", end: "23:00", title: "Confirmed wedding" }
  ];
}

function getConfirmedBookings() {
  if (Array.isArray(liveConfirmedBookings)) return liveConfirmedBookings;
  const saved = JSON.parse(localStorage.getItem(confirmedStorageKey) || "[]");
  return [...defaultConfirmedBookings(), ...saved].sort((a, b) => `${a.date} ${a.start}`.localeCompare(`${b.date} ${b.start}`));
}

function confirmedForDate(date) {
  return getConfirmedBookings().filter((booking) => booking.date === date);
}

function blocksForDate(date) {
  if (Array.isArray(liveAvailabilityBlocks)) return liveAvailabilityBlocks.filter((block) => block.date === date);
  return getAvailabilityBlocks().filter((block) => block.date === date);
}

function unavailableForDate(date) {
  return [
    ...confirmedForDate(date).map((booking) => ({ ...booking, label: booking.title || "Confirmed booking", kind: "confirmed" })),
    ...blocksForDate(date).map((block) => ({ ...block, label: block.title || "Unavailable", kind: "blocked" }))
  ];
}

function pendingForDate(date) {
  const source = Array.isArray(livePendingRequests) ? livePendingRequests : getRequests();
  return source.filter((request) => request.date === date);
}

function timeToMinutes(time) {
  const [hours, minutes] = time.split(":").map(Number);
  return hours * 60 + minutes;
}

function isFullDayItem(item) {
  if (!item?.start || !item?.end) return false;
  return timeToMinutes(item.start) <= 8 * 60 && timeToMinutes(item.end) >= 23 * 60;
}

function isFullDayUnavailable(items) {
  return items.some((item) => isFullDayItem(item));
}

function formatTime(time) {
  if (!time) return "time TBD";
  const [rawHours, rawMinutes = "00"] = String(time).split(":");
  const hours = Number(rawHours);
  if (Number.isNaN(hours)) return time;
  const suffix = hours >= 12 ? "PM" : "AM";
  const displayHours = hours % 12 || 12;
  return `${displayHours}:${String(rawMinutes).padStart(2, "0")} ${suffix}`;
}

function formatTimeRange(start, end) {
  return `${formatTime(start)}-${formatTime(end)}`;
}

function formatPendingTime(item) {
  const start = item.start || item.time;
  const end = item.end || item.endTime;
  return end ? formatTimeRange(start, end) : formatTime(start);
}

function overlapsConfirmedBooking(date, startTime) {
  if (!date || !startTime) return false;
  const endTime = form?.elements?.endTime?.value || "";
  const requestedStart = timeToMinutes(startTime);
  const requestedEnd = endTime ? timeToMinutes(endTime) : requestedStart + 180;
  return unavailableForDate(date).some((item) => {
    const unavailableStart = timeToMinutes(item.start);
    const unavailableEnd = timeToMinutes(item.end);
    return requestedStart < unavailableEnd && requestedEnd > unavailableStart;
  });
}

function renderConfirmedSlots(date) {
  const unavailableItems = unavailableForDate(date);
  if (!date || !unavailableItems.length) {
    confirmedSlots.hidden = true;
    confirmedSlots.innerHTML = "";
    return;
  }
  confirmedSlots.hidden = false;
  confirmedSlots.innerHTML = `
    <strong>Unavailable times on ${escapeHtml(formatDate(date))}</strong>
    ${unavailableItems.map((item) => `<span>${escapeHtml(formatTimeRange(item.start, item.end))} ${escapeHtml(item.label)}</span>`).join("")}
  `;
}

function getSubmissionLog() {
  return JSON.parse(localStorage.getItem(rateStorageKey) || "[]");
}

function saveSubmissionLog(log) {
  localStorage.setItem(rateStorageKey, JSON.stringify(log));
}

function getCropSettings() {
  return JSON.parse(localStorage.getItem(cropStorageKey) || "{}");
}

function saveCropSettings(settings) {
  localStorage.setItem(cropStorageKey, JSON.stringify(settings));
}

function cropFor(index) {
  return { ...defaultCrops[index], ...(getCropSettings()[index] || {}) };
}

function applyCropToImage(index, crop) {
  const tile = document.querySelector(`[data-photo="${index}"]`);
  const image = document.querySelector(`[data-photo="${index}"] img`);
  if (!tile || !image) return;
  tile.style.gridColumn = `span ${crop.cols || 1}`;
  tile.style.gridRow = `span ${crop.rows || 1}`;
  image.style.setProperty("--crop-x", `${crop.x}%`);
  image.style.setProperty("--crop-y", `${crop.y}%`);
  image.style.setProperty("--crop-zoom", crop.zoom);
  image.style.setProperty("--crop-fit", crop.fit || "cover");
}

function applySavedCrops() {
  photos.forEach((_, index) => applyCropToImage(index, cropFor(index)));
}

function updateCropPreview(crop) {
  cropPreview.src = photos[selectedCropPhoto].src;
  cropPreview.alt = photos[selectedCropPhoto].caption;
  cropPreview.parentElement.style.setProperty("--preview-cols", crop.cols || 1);
  cropPreview.parentElement.style.setProperty("--preview-rows", crop.rows || 1);
  cropPreview.style.setProperty("--preview-x", `${crop.x}%`);
  cropPreview.style.setProperty("--preview-y", `${crop.y}%`);
  cropPreview.style.setProperty("--preview-zoom", crop.zoom);
  cropPreview.style.setProperty("--preview-fit", crop.fit || "cover");
}

function loadCropEditor(index) {
  selectedCropPhoto = Number(index);
  const crop = cropFor(selectedCropPhoto);
  cropPhotoSelect.value = String(selectedCropPhoto);
  cropX.value = crop.x;
  cropY.value = crop.y;
  cropZoom.value = crop.zoom;
  cropCols.value = crop.cols || 1;
  cropRows.value = crop.rows || 1;
  cropFit.value = crop.fit || "cover";
  updateCropPreview(crop);
}

function currentEditorCrop() {
  return {
    x: Number(cropX.value),
    y: Number(cropY.value),
    zoom: Number(cropZoom.value),
    cols: Number(cropCols.value),
    rows: Number(cropRows.value),
    fit: cropFit.value
  };
}

function previewEditorCrop() {
  const crop = currentEditorCrop();
  if (crop.fit === "cover" && crop.zoom < 1) {
    crop.zoom = 1;
    cropZoom.value = 1;
  }
  applyCropToImage(selectedCropPhoto, crop);
  updateCropPreview(crop);
}

function setupCropEditorMode() {
  const params = new URLSearchParams(window.location.search);
  const enabled = params.get("edit") === "gallery";
  cropEditor.hidden = !enabled;
}

function createId() {
  if (window.crypto && typeof window.crypto.randomUUID === "function") {
    return window.crypto.randomUUID();
  }
  return `request-${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function formatDate(dateString) {
  if (!dateString) return "Date not selected";
  const date = new Date(`${dateString}T12:00:00`);
  return date.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" });
}

function monthLabel(date) {
  return date.toLocaleDateString(undefined, { month: "long", year: "numeric" });
}

function toDateValue(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 selectedDateValue() {
  return form.elements.date.value;
}

function calendarDetailsHtml(dateValue) {
  const unavailableItems = unavailableForDate(dateValue);
  const pendingItems = pendingForDate(dateValue);
  if (!dateValue) return "Select a date to see confirmed bookings.";
  if (!unavailableItems.length && !pendingItems.length) {
    return `<strong>${escapeHtml(formatDate(dateValue))}</strong><span>No confirmed bookings yet. This date can be requested.</span>`;
  }
  if (!unavailableItems.length) {
    return `
      <strong>${escapeHtml(formatDate(dateValue))}</strong>
      ${pendingItems.map((item) => `<span class="pending-note">${escapeHtml(formatPendingTime(item))} pending request</span>`).join("")}
    `;
  }
  return `
    <strong>${escapeHtml(formatDate(dateValue))}</strong>
    ${unavailableItems.map((item) => `<span>${escapeHtml(formatTimeRange(item.start, item.end))} ${escapeHtml(item.label)}</span>`).join("")}
  `;
}

function renderBookingCalendar() {
  const selected = selectedDateValue();
  const todayValue = toDateValue(new Date());
  const year = calendarMonth.getFullYear();
  const month = calendarMonth.getMonth();
  const firstDay = new Date(year, month, 1);
  const totalDays = new Date(year, month + 1, 0).getDate();
  const cells = [];

  calendarMonthLabel.textContent = monthLabel(calendarMonth);
  for (let i = 0; i < firstDay.getDay(); i++) {
    cells.push('<button class="calendar-day blank" type="button" tabindex="-1"></button>');
  }
  for (let day = 1; day <= totalDays; day++) {
    const date = new Date(year, month, day);
    const value = toDateValue(date);
    const unavailableItems = unavailableForDate(value);
    const pendingItems = pendingForDate(value);
    const classes = ["calendar-day"];
    if (value < todayValue) classes.push("past");
    if (unavailableItems.length) classes.push(isFullDayUnavailable(unavailableItems) ? "has-confirmed" : "has-partial");
    if (pendingItems.length && !unavailableItems.length) classes.push("pending");
    if (value === selected) classes.push("selected");
    cells.push(`
      <button class="${classes.join(" ")}" type="button" data-calendar-date="${value}">
        <strong>${day}</strong>
        ${pendingItems.length && !unavailableItems.length ? "<small>Pending</small>" : ""}
      </button>
    `);
  }
  calendarGrid.innerHTML = cells.join("");
  calendarDetails.innerHTML = calendarDetailsHtml(selected);
}

function setCalendarMonthFromDate(dateValue) {
  const base = dateValue ? new Date(`${dateValue}T12:00:00`) : new Date();
  calendarMonth = new Date(base.getFullYear(), base.getMonth(), 1);
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function formatMoney(value) {
  return `$${Number(value || 0).toLocaleString()}`;
}

function formatPriceRange(low, high) {
  const lowValue = Number(low || 0);
  const highValue = Number(high || 0);
  if (!lowValue && !highValue) return "Quote";
  if (lowValue === highValue) return formatMoney(lowValue);
  return `${formatMoney(lowValue)} - ${formatMoney(highValue)}`;
}

function addonPriceRange(addon) {
  const low = Number(addon.priceLow ?? 0);
  const high = Number(addon.priceHigh ?? 0);
  const price = low || high ? formatPriceRange(low, high) : "Free";
  return addon.priceUnit && price !== "Free" ? `${price} ${addon.priceUnit}` : price;
}

function packageIncludedItems(pkg) {
  if (Array.isArray(pkg.included) && pkg.included.length) return pkg.included;
  return String(pkg.includes || "")
    .split(/\n|;/)
    .map((item) => item.trim())
    .filter(Boolean);
}

function activePackages() {
  const configured = (siteConfig.packages || []).filter((pkg) => pkg.enabled !== false);
  if (configured.length) return configured;
  return (defaultSiteConfig.packages || []).filter((pkg) => pkg.enabled !== false);
}

function activeAddons() {
  const defaults = defaultSiteConfig.addons || [];
  const configured = (siteConfig.addons || []).filter((addon) => addon.enabled !== false);
  if (!configured.length) return defaults.filter((addon) => addon.enabled !== false);
  return configured.map((addon) => {
    const fallback = defaults.find((item) => (item.value || item.label) === (addon.value || addon.label)) || {};
    return { ...fallback, ...addon };
  });
}

function packageEstimateRange(pkg, dateValue) {
  if (!pkg) return { low: 0, high: 0 };
  const low = Number(pkg.priceLow || 0);
  const high = Number(pkg.priceHigh || 0);
  if (!/simple/i.test(pkg.name || "")) return { low, high };
  if (!dateValue) return { low, high };
  const date = new Date(`${dateValue}T12:00:00`);
  const amount = date.getDay() === 6 ? high : low;
  return { low: amount, high: amount };
}

function packageDisplayPrice(pkg) {
  if (/simple/i.test(pkg.name || "")) return "$1,500 Fri/Sun, $1,800 Sat";
  return formatPriceRange(pkg.priceLow, pkg.priceHigh);
}

function applyThemeSettings() {
  const colors = siteConfig.colors || {};
  Object.entries({
    ink: "--ink",
    paper: "--paper",
    cream: "--cream",
    barn: "--barn",
    sage: "--sage",
    gold: "--gold"
  }).forEach(([key, property]) => {
    if (colors[key]) document.documentElement.style.setProperty(property, colors[key]);
  });
}

function applyBrandingSettings() {
  const branding = siteConfig.branding || {};
  const venueName = branding.venueName || "Grange Station";
  const tagline = branding.tagline || "Wilson Creek, WA";
  const logoImage = branding.logoImage || "";
  const heroImage = branding.heroImage || "assets/grange-station.png";
  const socialImage = branding.socialImage || heroImage;

  document.querySelectorAll(".brand-mark").forEach((mark) => {
    mark.innerHTML = logoImage ? `<img src="${escapeHtml(logoImage)}" alt="">` : "GS";
    mark.classList.toggle("has-logo", Boolean(logoImage));
  });
  document.querySelectorAll(".site-header, .gallery-header").forEach((item) => {
    item.classList.toggle("has-logo", Boolean(logoImage));
  });
  document.querySelectorAll(".brand strong").forEach((item) => item.textContent = venueName);
  document.querySelectorAll(".brand small").forEach((item) => item.textContent = tagline);
  const hero = document.querySelector(".hero > img");
  if (hero) {
    hero.src = heroImage;
    hero.alt = `${venueName} venue exterior.`;
  }
  document.querySelectorAll("meta[property='og:image'], meta[name='twitter:image']").forEach((meta) => {
    meta.setAttribute("content", socialImage.startsWith("http") ? socialImage : `https://www.grangestation.com/${socialImage}`);
  });
}

function renderManagedContent() {
  applyThemeSettings();
  applyBrandingSettings();

  const activePackageList = activePackages();
  const packageGrid = document.querySelector("[data-package-grid]");
  if (packageGrid) {
    packageGrid.innerHTML = activePackageList.map((pkg) => {
      const includedItems = packageIncludedItems(pkg);
      const investment = `${formatPriceRange(pkg.priceLow, pkg.priceHigh)}${pkg.investmentNote ? ` <small>(${escapeHtml(pkg.investmentNote)})</small>` : ""}`;
      return `
        <article>
          <span>${escapeHtml(pkg.subtitle || "Package")}</span>
          <h3>${escapeHtml(pkg.name)}</h3>
          <p>${escapeHtml(pkg.description || "")}</p>
          <dl>
            <div><dt>Investment</dt><dd><strong>${investment}</strong></dd></div>
            <div><dt>Time Block</dt><dd>${escapeHtml(pkg.timeBlock || "")}</dd></div>
            <div>
              <dt>What's Included</dt>
              <dd>${includedItems.length ? `<ul>${includedItems.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : "Details coming soon."}</dd>
            </div>
            ${pkg.blueprint ? `<div><dt>The Blueprint</dt><dd>${escapeHtml(pkg.blueprint)}</dd></div>` : ""}
          </dl>
        </article>
      `;
    }).join("");
  }

  const packageSelect = document.querySelector("[data-package-select]");
  if (packageSelect) {
    const selected = packageSelect.value;
    packageSelect.innerHTML = `<option value="">Not sure yet</option>${activePackageList.map((pkg) => `<option value="${escapeHtml(pkg.name)}">${escapeHtml(pkg.name)} - ${escapeHtml(packageDisplayPrice(pkg))}</option>`).join("")}`;
    packageSelect.value = selected;
  }

  const addonFieldset = document.querySelector("fieldset");
  if (addonFieldset) {
    addonFieldset.innerHTML = `
      <legend>Optional add-ons</legend>
      ${activeAddons().map((addon) => `
        <label><input type="checkbox" name="addons" value="${escapeHtml(addon.value)}"> ${escapeHtml(addon.label)} <small>${escapeHtml(addonPriceRange(addon))}</small></label>
      `).join("")}
    `;
  }

  const vendorGrid = document.querySelector(".vendor-grid");
  if (vendorGrid) {
    vendorGrid.innerHTML = (siteConfig.vendors || []).filter((vendor) => vendor.enabled !== false).map((vendor) => `
      <article>
        <span>${escapeHtml(vendor.category)}</span>
        <h3>${escapeHtml(vendor.title)}</h3>
        <p>${escapeHtml(vendor.description)}</p>
      </article>
    `).join("");
  }

  const faqGrid = document.querySelector(".faq-grid");
  if (faqGrid) {
    faqGrid.innerHTML = (siteConfig.faqs || []).filter((faq) => faq.enabled !== false).map((faq) => `
      <details>
        <summary>${escapeHtml(faq.question)}</summary>
        <p>${escapeHtml(faq.answer)}</p>
      </details>
    `).join("");
  }
}

function publicRequestData(request) {
  const { confirmEmail, ...publicRequest } = request;
  return publicRequest;
}

function collectFormData() {
  const data = new FormData(form);
  return {
    id: createId(),
    createdAt: new Date().toISOString(),
    requestType: data.get("requestType"),
    name: data.get("name").trim(),
    email: data.get("email").trim(),
    phone: data.get("phone").trim(),
    confirmEmail: data.get("confirmEmail").trim(),
    date: data.get("date"),
    time: data.get("time"),
    endTime: data.get("endTime"),
    fullDay: data.get("fullDay") === "1",
    guests: Number(data.get("guests") || 0),
    package: data.get("package"),
    eventType: data.get("eventType"),
    space: data.get("space"),
    vendorHelp: data.get("vendorHelp"),
    addons: data.getAll("addons"),
    message: data.get("message").trim(),
    humanCheck: data.get("humanCheck").trim(),
    website: data.get("website").trim(),
    formStartedAt: Number(data.get("formStartedAt") || 0)
  };
}

function collectContactData() {
  const data = new FormData(contactForm);
  return {
    name: data.get("name").trim(),
    email: data.get("email").trim(),
    phone: data.get("phone").trim(),
    reason: data.get("reason"),
    message: data.get("message").trim(),
    humanCheck: data.get("humanCheck").trim(),
    website: data.get("website").trim(),
    contactStartedAt: Number(data.get("contactStartedAt") || 0)
  };
}

function validateRequest(request) {
  const now = Date.now();
  const secondsToSubmit = (now - request.formStartedAt) / 1000;
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const phoneDigits = request.phone.replace(/\D/g, "");
  const messageHasLinks = /https?:\/\/|www\.|\.ru\b|\.cn\b/i.test(request.message);
  const recentLog = getSubmissionLog().filter((time) => now - time < 60 * 60 * 1000);
  const duplicate = getRequests().some((item) =>
    item.email.toLowerCase() === request.email.toLowerCase() &&
    item.date === request.date &&
    item.requestType === request.requestType
  );

  if (request.website) return "Your request could not be accepted.";
  if (secondsToSubmit < minimumSubmitSeconds) return "Please take a moment to review the form before submitting.";
  if (!request.name || !request.email || !request.phone || !request.date || !request.time || !request.endTime) return "Please complete the required contact and event fields.";
  if (!emailPattern.test(request.email)) return "Please enter a valid email address.";
  if (request.email.toLowerCase() !== request.confirmEmail.toLowerCase()) return "The email confirmation does not match.";
  if (phoneDigits.length < 10) return "Please enter a phone number with at least 10 digits.";
  if (!request.fullDay && timeToMinutes(request.endTime) <= timeToMinutes(request.time)) return "Please choose an end time after the start time.";
  if (Number(request.humanCheck) !== 7) return "Please answer the human check before submitting.";
  if (messageHasLinks) return "Please remove links from the message field and submit again.";
  if (duplicate) return "A matching request is already saved for that date and email.";
  if (recentLog.length >= 3) return "Too many requests were submitted from this browser recently. Please try again later.";

  saveSubmissionLog([...recentLog, now]);
  return "";
}

function validateContactMessage(message) {
  const now = Date.now();
  const secondsToSubmit = (now - message.contactStartedAt) / 1000;
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const messageHasLinks = /https?:\/\/|www\.|\.ru\b|\.cn\b/i.test(message.message);

  if (message.website) return "Your message could not be accepted.";
  if (secondsToSubmit < minimumSubmitSeconds) return "Please take a moment to review the message before submitting.";
  if (!message.name || !message.email || !message.message) return "Please complete your name, email, and message.";
  if (!emailPattern.test(message.email)) return "Please enter a valid email address.";
  if (Number(message.humanCheck) !== 9) return "Please answer the human check before submitting.";
  if (messageHasLinks) return "Please remove links from the message field and submit again.";
  return "";
}

function resetAntiBotFields() {
  formStartedAt.value = String(Date.now());
  form.elements.humanCheck.value = "";
  form.elements.website.value = "";
  form.elements.confirmEmail.value = "";
  if (contactForm && contactStartedAt) {
    contactStartedAt.value = String(Date.now());
    contactForm.elements.humanCheck.value = "";
    contactForm.elements.website.value = "";
  }
}

function estimateRange() {
  const fields = form.elements;
  const requestType = fields.requestType.value;
  const guests = Number(fields.guests.value || 0);
  const date = fields.date.value ? new Date(`${fields.date.value}T12:00:00`) : null;
  const isWeekend = date ? [5, 6].includes(date.getDay()) : true;
  const selectedAddons = [...form.querySelectorAll("input[name='addons']:checked")].map((input) => input.value);
  const addonOptions = activeAddons();

  if (requestType === "Private Viewing") {
    estimate.textContent = "Complimentary";
    estimateNote.textContent = "Private viewings can be scheduled before selecting an event date.";
    if (estimateBreakdown) estimateBreakdown.textContent = "";
    return;
  }

  const pricing = siteConfig.pricing;
  const selectedPackage = activePackages().find((pkg) => pkg.name === fields.package?.value);
  let low = 0;
  let high = 0;
  const breakdown = [];

  if (selectedPackage) {
    const packageRange = packageEstimateRange(selectedPackage, fields.date.value);
    low = packageRange.low;
    high = packageRange.high;
    breakdown.push(`${selectedPackage.name}: ${formatPriceRange(low, high)}`);
  } else {
    low = isWeekend ? Number(pricing.weekendLow) : Number(pricing.weekdayLow);
    high = isWeekend ? Number(pricing.weekendHigh) : Number(pricing.weekdayHigh);
    breakdown.push(`${isWeekend ? "Weekend" : "Weekday"} base: ${formatPriceRange(low, high)}`);
  }

  if (!selectedPackage && guests > 100) {
    low += Number(pricing.over100Low);
    high += Number(pricing.over100High);
    breakdown.push(`Over 100 guests: ${formatPriceRange(pricing.over100Low, pricing.over100High)}`);
  }

  selectedAddons.forEach((addonValue) => {
    const option = addonOptions.find((addon) => (addon.value || addon.label) === addonValue);
    if (!option) return;
    const addonLow = Number(option.priceLow ?? 0);
    const addonHigh = Number(option.priceHigh ?? 0);
    low += addonLow;
    high += addonHigh;
    breakdown.push(`${option.label || addonValue}: ${addonPriceRange(option)}`);
  });

  estimate.textContent = formatPriceRange(low, high);
  estimateNote.textContent = selectedPackage
    ? "Package estimate with selected add-ons. Final rental/vendor details are confirmed by quote."
    : `${isWeekend ? "Weekend" : "Weekday"} estimate with selected add-ons. Final rental/vendor details are confirmed by quote.`;
  if (estimateBreakdown) estimateBreakdown.textContent = breakdown.join(" + ");
}

function updateAvailability() {
  const date = form.elements.date.value;
  const time = form.elements.time.value;
  renderConfirmedSlots(date);
  renderBookingCalendar();
  availability.className = "availability";
  if (!date) {
    availability.textContent = "Choose a date to check availability.";
    return;
  }
  if (overlapsConfirmedBooking(date, time)) {
    availability.textContent = `${formatDate(date)} has a confirmed booking at that time. Please choose another time or ask about waitlist options.`;
    availability.classList.add("bad");
    return;
  }
  const confirmedCount = confirmedForDate(date).length;
  availability.textContent = confirmedCount
    ? `${formatDate(date)} has unavailable times listed below, but other times may be requested.`
    : `${formatDate(date)} can be requested. Final confirmation happens after venue review.`;
  availability.classList.add("good");
}

function updateMailto() {
  if (!mailto) return;
  const request = collectFormData();
  const subject = encodeURIComponent(`${request.requestType}: ${request.date || "Date TBD"} at Grange Station`);
  const body = encodeURIComponent([
    `Request type: ${request.requestType}`,
    `Name: ${request.name}`,
    `Email: ${request.email}`,
    `Phone: ${request.phone}`,
    `Date: ${formatDate(request.date)}`,
    `Time: ${request.fullDay ? "Full day" : `${formatTime(request.time)}-${formatTime(request.endTime)}`}`,
    `Guest count: ${request.guests}`,
    `Package: ${request.package || "Not sure yet"}`,
    `Event type: ${request.eventType}`,
    `Space: ${request.space}`,
    `Preferred vendor help: ${request.vendorHelp}`,
    `Additions: ${request.addons.join(", ") || "None"}`,
    "",
    request.message || "No additional notes yet."
  ].join("\n"));
  mailto.href = `mailto:events@grangestation.com?subject=${subject}&body=${body}`;
}

function renderRequests() {
  if (!requestList) return;
  const requests = getRequests();
  if (!requests.length) {
    requestList.innerHTML = '<div class="empty">No saved requests yet. New reservation and viewing requests will appear here.</div>';
    return;
  }

  requestList.innerHTML = requests.map((request) => `
    <article class="request-card">
      <div>
        <strong>${escapeHtml(request.requestType)}: ${escapeHtml(request.name)}</strong>
        <p>${escapeHtml(formatDate(request.date))} at ${escapeHtml(formatTime(request.time))}${request.endTime ? ` to ${escapeHtml(formatTime(request.endTime))}` : ""} - ${escapeHtml(request.guests || "guest count TBD")} guests - ${escapeHtml(request.eventType)}</p>
        <p>${escapeHtml(request.package || "Package not selected")}</p>
        <p>${escapeHtml(request.vendorHelp || "No vendor help selected")}</p>
        <p>${escapeHtml(request.email)} - ${escapeHtml(request.phone)}</p>
      </div>
      <button type="button" data-delete-request="${escapeHtml(request.id)}">Remove</button>
    </article>
  `).join("");
}

function exportCsv() {
  const requests = getRequests();
  if (!requests.length) {
    showToast("No requests to export yet.");
    return;
  }
  const headers = ["createdAt", "requestType", "name", "email", "phone", "date", "time", "endTime", "guests", "package", "eventType", "space", "vendorHelp", "addons", "message"];
  const rows = requests.map((request) => headers.map((key) => {
    const value = Array.isArray(request[key]) ? request[key].join("; ") : request[key] || "";
    return `"${String(value).replaceAll('"', '""')}"`;
  }).join(","));
  const blob = new Blob([[headers.join(","), ...rows].join("\n")], { type: "text/csv" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = "grange-station-requests.csv";
  link.click();
  URL.revokeObjectURL(url);
  showToast("CSV exported.");
}

function openPhoto(index) {
  if (!lightbox || !lightboxImage || !lightboxCaption) return;
  currentPhoto = (index + galleryPhotos.length) % galleryPhotos.length;
  lightboxImage.src = galleryPhotos[currentPhoto].src;
  lightboxImage.alt = galleryPhotos[currentPhoto].caption;
  lightboxCaption.textContent = galleryPhotos[currentPhoto].caption;
  renderGalleryThumbs();
  if (!lightbox.open) lightbox.showModal();
}

function renderGalleryThumbs() {
  if (!galleryThumbs) return;
  galleryThumbs.innerHTML = galleryPhotos.map((photo, index) => `
    <button type="button" class="${index === currentPhoto ? "active" : ""}" data-gallery-thumb="${index}" aria-label="Open gallery photo ${index + 1}">
      <img src="${photo.src}" alt="">
    </button>
  `).join("");
}

window.addEventListener("scroll", () => {
  header.classList.toggle("scrolled", window.scrollY > 20);
});

menuToggle.addEventListener("click", () => {
  const open = nav.classList.toggle("open");
  menuToggle.setAttribute("aria-expanded", String(open));
});

nav.addEventListener("click", () => {
  nav.classList.remove("open");
  menuToggle.setAttribute("aria-expanded", "false");
});

document.querySelectorAll("[data-request-type]").forEach((button) => {
  button.addEventListener("click", () => {
    document.querySelectorAll("[data-request-type]").forEach((item) => item.classList.remove("active"));
    button.classList.add("active");
    form.elements.requestType.value = button.dataset.requestType;
    if (button.dataset.requestType === "Private Viewing") {
      form.elements.eventType.value = "Other";
      form.elements.space.value = "Viewing only";
      form.elements.vendorHelp.value = "No vendor help needed yet";
      form.elements.guests.value = 2;
    }
    estimateRange();
    updateAvailability();
    updateMailto();
  });
});

document.querySelector("[data-viewing-link]").addEventListener("click", () => {
  document.querySelector("[data-request-type='Private Viewing']").click();
});

document.querySelector("[data-calendar-prev]").addEventListener("click", () => {
  calendarMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() - 1, 1);
  renderBookingCalendar();
});

document.querySelector("[data-calendar-next]").addEventListener("click", () => {
  calendarMonth = new Date(calendarMonth.getFullYear(), calendarMonth.getMonth() + 1, 1);
  renderBookingCalendar();
});

calendarGrid.addEventListener("click", (event) => {
  const button = event.target.closest("[data-calendar-date]");
  if (!button) return;
  dateInput.value = button.dataset.calendarDate;
  renderBookingCalendar();
  estimateRange();
  updateAvailability();
  updateMailto();
});

dateInput.addEventListener("change", () => {
  setCalendarMonthFromDate(dateInput.value);
  renderBookingCalendar();
});

dateInput.addEventListener("focus", () => {
  bookingCalendar.scrollIntoView({ block: "nearest", behavior: "smooth" });
});

form.addEventListener("input", () => {
  estimateRange();
  updateAvailability();
  updateMailto();
});

form.addEventListener("change", () => {
  estimateRange();
  updateAvailability();
  updateMailto();
});

if (fullDayInput) {
  fullDayInput.addEventListener("change", () => {
    form.elements.time.value = fullDayInput.checked ? "08:00" : (form.elements.time.value || "16:00");
    form.elements.endTime.value = fullDayInput.checked ? "23:00" : (form.elements.endTime.value || "22:00");
    form.elements.time.readOnly = fullDayInput.checked;
    form.elements.endTime.readOnly = fullDayInput.checked;
    estimateRange();
    updateAvailability();
    updateMailto();
  });
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const request = collectFormData();
  const validationError = validateRequest(request);
  if (validationError) {
    showToast(validationError);
    return;
  }
  if (overlapsConfirmedBooking(request.date, request.time)) {
    showToast("That time overlaps a confirmed booking. Please choose another time.");
    return;
  }
  if (apiAvailable) {
    try {
      const result = await apiRequest("submit_request", publicRequestData(request));
      await loadLiveData();
      showToast(result.emailed ? "Request submitted and emailed to Grange Station." : "Request submitted. Email delivery is not configured on the server yet.");
    } catch (error) {
      showToast("The server could not save that request. Please try again or email events@grangestation.com.");
      return;
    }
  } else {
    const requests = [publicRequestData(request), ...getRequests()];
    saveRequests(requests);
    renderRequests();
  }
  form.reset();
  form.elements.requestType.value = "Event Reservation";
  form.elements.time.readOnly = false;
  form.elements.endTime.readOnly = false;
  resetAntiBotFields();
  document.querySelectorAll("[data-request-type]").forEach((item) => item.classList.toggle("active", item.dataset.requestType === "Event Reservation"));
  estimateRange();
  updateAvailability();
  updateMailto();
  if (!apiAvailable) showToast("Request saved in the booking desk.");
});

if (contactForm) {
  contactForm.addEventListener("submit", async (event) => {
    event.preventDefault();
    const message = collectContactData();
    const validationError = validateContactMessage(message);
    if (validationError) {
      showToast(validationError);
      return;
    }
    try {
      const result = await apiRequest("submit_contact", message);
      contactForm.reset();
      if (contactStartedAt) contactStartedAt.value = String(Date.now());
      showToast(result.emailed ? "Message emailed to Grange Station." : "Message saved. Email delivery is not configured on the server yet.");
    } catch (error) {
      showToast(error.message || "The server could not send that message. Please email events@grangestation.com.");
    }
  });
}

if (requestList) {
  requestList.addEventListener("click", (event) => {
    const button = event.target.closest("[data-delete-request]");
    if (!button) return;
    saveRequests(getRequests().filter((request) => request.id !== button.dataset.deleteRequest));
    renderRequests();
    showToast("Request removed.");
  });
}

const exportButton = document.querySelector("[data-export]");
if (exportButton) exportButton.addEventListener("click", exportCsv);
const clearButton = document.querySelector("[data-clear]");
if (clearButton) {
  clearButton.addEventListener("click", () => {
    if (!getRequests().length) return;
    saveRequests([]);
    renderRequests();
    showToast("Saved requests cleared.");
  });
}

document.querySelector("[data-gallery-grid]").addEventListener("click", (event) => {
  const button = event.target.closest("[data-photo]");
  if (!button) return;
  if (cropEditor && !cropEditor.hidden) {
    loadCropEditor(Number(button.dataset.photo || 0));
    return;
  }
  window.location.href = `gallery.html?photo=${button.dataset.galleryIndex || 0}`;
});
if (galleryThumbs) {
  galleryThumbs.addEventListener("click", (event) => {
    const button = event.target.closest("[data-gallery-thumb]");
    if (button) openPhoto(Number(button.dataset.galleryThumb));
  });
}

cropPhotoSelect.addEventListener("change", () => loadCropEditor(cropPhotoSelect.value));
[cropX, cropY, cropZoom, cropCols, cropRows, cropFit].forEach((control) => {
  control.addEventListener("input", previewEditorCrop);
  control.addEventListener("change", previewEditorCrop);
});
document.querySelector("[data-save-crop]").addEventListener("click", () => {
  const settings = getCropSettings();
  settings[selectedCropPhoto] = currentEditorCrop();
  saveCropSettings(settings);
  applySavedCrops();
  showToast("Gallery crop saved.");
});
document.querySelector("[data-reset-crop]").addEventListener("click", () => {
  const settings = getCropSettings();
  delete settings[selectedCropPhoto];
  saveCropSettings(settings);
  loadCropEditor(selectedCropPhoto);
  applySavedCrops();
  showToast("Photo crop reset.");
});
document.querySelector("[data-reset-all-crops]").addEventListener("click", () => {
  saveCropSettings({});
  applySavedCrops();
  loadCropEditor(selectedCropPhoto);
  showToast("All gallery crops reset.");
});
if (lightbox) {
  document.querySelector("[data-close-gallery]").addEventListener("click", () => lightbox.close());
  document.querySelector("[data-prev-photo]").addEventListener("click", () => openPhoto(currentPhoto - 1));
  document.querySelector("[data-next-photo]").addEventListener("click", () => openPhoto(currentPhoto + 1));
  lightbox.addEventListener("click", (event) => {
    if (event.target === lightbox) lightbox.close();
  });
}

setCalendarMonthFromDate(dateInput.value);
bookingCalendar.hidden = false;
function initializePage() {
  renderManagedContent();
  estimateRange();
  updateAvailability();
  updateMailto();
  renderRequests();
  resetAntiBotFields();
  applySavedCrops();
  setupCropEditorMode();
  loadCropEditor(0);
}

loadLiveData().finally(initializePage);

Youez - 2016 - github.com/yon3zu
LinuXploit