← back to Malden House

dashboard.js

273 lines

// dashboard.js — rental list, inquiry inbox, photo library
(function () {
  const $ = sel => document.querySelector(sel);
  const $$ = sel => document.querySelectorAll(sel);

  function load() {
    try { return JSON.parse(localStorage.getItem("htg.rentals") || "{}"); }
    catch (e) { return {}; }
  }
  function save(all) {
    localStorage.setItem("htg.rentals", JSON.stringify(all));
  }
  function rentalsArr() {
    const all = load();
    return Object.values(all).sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
  }
  function escapeHtml(s) {
    return String(s ?? "")
      .replace(/&/g, "&")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }
  const escapeAttr = escapeHtml;
  function formatMoney(v) {
    return v === null || v === undefined || v === "" ? "—" : escapeHtml(v.toLocaleString());
  }

  // ------------ Rentals list ------------
  function renderRentals() {
    const all = load();
    const list = rentalsArr();
    const grid = $("#dash-grid");
    const empty = $("#dash-empty");
    if (!list.length) {
      grid.innerHTML = "";
      empty.hidden = false;
      return;
    }
    empty.hidden = true;
    grid.innerHTML = list.map(r => {
      const inq = (r.inquiries || []).length;
      const firstPhoto = (r.photos && r.photos[0]) || null;
      const sat = firstPhoto || `https://maps.google.com/maps?q=${r.address.lat},${r.address.lng}&t=k&z=18&output=embed`;
      const n0 = r.nearest && r.nearest[0];
      const street = r.address.street || "";
      const cityLine = [r.address.city, r.address.state, r.address.zip].filter(Boolean).join(", ");
      const chip = n0 ? n0.mi.toFixed(1) + " mi · " + (n0.headline || n0.sports) : "";
      return `
        <article class="dash-card" data-slug="${escapeAttr(r.slug)}">
          <div class="dash-card-img">
            ${firstPhoto
              ? `<img src="${escapeAttr(firstPhoto)}" alt="${escapeAttr(street)}">`
              : `<iframe src="${escapeAttr(sat)}" loading="lazy" title="Aerial"></iframe>`}
            <span class="dash-chip">${escapeHtml(chip)}</span>
          </div>
          <div class="dash-card-body">
            <div class="dash-card-addr">${escapeHtml(street)}</div>
            <div class="dash-card-city">${escapeHtml(cityLine)}</div>
            <div class="dash-card-row">
              <span><b>$${formatMoney(r.price && r.price.mid)}</b><em>/ night</em></span>
              <span><b>${(r.photos || []).length}</b><em>photos</em></span>
              <span><b>${inq}</b><em>${inq === 1 ? "inquiry" : "inquiries"}</em></span>
            </div>
            <div class="dash-card-actions">
              <a href="rental.html?id=${encodeURIComponent(r.slug)}" target="_blank" rel="noopener noreferrer">Open →</a>
              <button class="dash-edit" data-slug="${escapeAttr(r.slug)}">Edit</button>
            </div>
            <div class="dash-card-host">Hosted by ${escapeHtml(r.host)}</div>
          </div>
        </article>`;
    }).join("");

    $$(".dash-edit").forEach(b => b.addEventListener("click", e => {
      e.stopPropagation();
      openEditModal(e.currentTarget.getAttribute("data-slug"));
    }));
    renderStats(list);
  }

  function renderStats(list) {
    const inq = list.reduce((n, r) => n + ((r.inquiries || []).length), 0);
    const pics = list.reduce((n, r) => n + ((r.photos || []).length), 0);
    const avg = list.length ? Math.round(list.reduce((n, r) => n + (r.price ? r.price.mid : 0), 0) / list.length) : 0;
    $("#dash-stats").innerHTML = `
      <div><b>${list.length}</b><span>rental${list.length === 1 ? "" : "s"}</span></div>
      <div><b>${inq}</b><span>inquir${inq === 1 ? "y" : "ies"}</span></div>
      <div><b>${pics}</b><span>photos</span></div>
      <div><b>$${avg.toLocaleString()}</b><span>avg nightly</span></div>
    `;
    $("#dash-hello").textContent = list.length
      ? `Welcome back, ${list[0].host.split(" ")[0]}.`
      : `Welcome.`;
    $("#dash-summary").textContent = list.length
      ? `You have ${list.length} rental${list.length === 1 ? "" : "s"} live, ${inq} pending inquir${inq === 1 ? "y" : "ies"}.`
      : `Create a rental from the landing page to get started.`;
  }

  // ------------ Edit modal ------------
  function openEditModal(slug) {
    const all = load();
    const r = all[slug]; if (!r) return;
    const modal = $("#edit-modal");
    const f = $("#edit-form");
    f.host.value = r.host || "";
    f.email.value = r.email || "";
    f.phone.value = r.phone || "";
    f.setAttribute("data-slug", slug);
    modal.hidden = false;
  }
  function closeEditModal() { $("#edit-modal").hidden = true; }

  $("#edit-close").addEventListener("click", closeEditModal);
  $("#edit-modal").addEventListener("click", e => { if (e.target === e.currentTarget) closeEditModal(); });
  $("#edit-form").addEventListener("submit", e => {
    e.preventDefault();
    const slug = e.currentTarget.getAttribute("data-slug");
    const all = load();
    if (!all[slug]) return;
    all[slug].host = e.currentTarget.host.value;
    all[slug].email = e.currentTarget.email.value;
    all[slug].phone = e.currentTarget.phone.value;
    save(all);
    closeEditModal();
    renderRentals();
    populatePhotoPicker();
  });
  $("#edit-delete").addEventListener("click", () => {
    const slug = $("#edit-form").getAttribute("data-slug");
    if (!slug) return;
    if (!confirm("Delete this rental and all its inquiries? This cannot be undone.")) return;
    const all = load();
    delete all[slug];
    save(all);
    closeEditModal();
    renderRentals();
    renderInbox();
    populatePhotoPicker();
  });

  // ------------ Inquiry inbox ------------
  function renderInbox() {
    const list = rentalsArr();
    const flat = [];
    list.forEach(r => (r.inquiries || []).forEach(i => flat.push({ ...i, _slug: r.slug, _street: r.address.street })));
    flat.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
    const inbox = $("#dash-inbox");
    const empty = $("#dash-inbox-empty");
    if (!flat.length) { inbox.innerHTML = ""; empty.hidden = false; return; }
    empty.hidden = true;
    inbox.innerHTML = flat.map(m => `
      <li>
        <div class="inq-top">
          <div>
            <span class="inq-name">${escapeHtml((m.firstName || "") + " " + (m.lastName || ""))}</span>
            <span class="inq-email">${escapeHtml(m.email || "")}</span>
          </div>
          <div class="inq-meta">
            <span class="inq-at">${escapeHtml(fmtDate(m.createdAt))}</span>
            <span class="inq-for">for <a href="rental.html?id=${encodeURIComponent(m._slug)}" target="_blank" rel="noopener noreferrer">${escapeHtml(m._street)}</a></span>
          </div>
        </div>
        <div class="inq-dates">
          ${escapeHtml(m.checkIn || "?")} → ${escapeHtml(m.checkOut || "?")} · ${escapeHtml(m.guests || "?")} guests
          ${m.events && m.events.length ? ` · attending: ${escapeHtml(m.events.join(", "))}` : ""}
        </div>
        ${m.message ? `<p class="inq-msg">${escapeHtml(m.message)}</p>` : ""}
        <div class="inq-actions">
          <a href="mailto:${encodeURIComponent(m.email || "")}?subject=Re: Your Victory Stays inquiry for ${encodeURIComponent(m._street || "")}">Reply</a>
          <button data-slug="${escapeAttr(m._slug)}" data-id="${escapeAttr(m.id)}" class="inq-delete">Delete</button>
        </div>
      </li>
    `).join("");
    $$(".inq-delete").forEach(b => b.addEventListener("click", () => {
      const slug = b.getAttribute("data-slug"), id = b.getAttribute("data-id");
      const all = load();
      if (!all[slug]) return;
      all[slug].inquiries = (all[slug].inquiries || []).filter(i => i.id !== id);
      save(all);
      renderInbox();
      renderRentals();
    }));
  }
  function fmtDate(iso) { try { return new Date(iso).toLocaleString(); } catch (e) { return iso || ""; } }

  // ------------ Photo library ------------
  function populatePhotoPicker() {
    const sel = $("#photo-rental");
    const list = rentalsArr();
    if (!list.length) {
      sel.innerHTML = `<option value="">— no rentals yet —</option>`;
      $("#photo-grid").innerHTML = "";
      return;
    }
    sel.innerHTML = list.map(r => `<option value="${escapeAttr(r.slug)}">${escapeHtml(r.address.street)} — ${escapeHtml(r.address.city)}</option>`).join("");
    renderPhotos();
  }

  function renderPhotos() {
    const slug = $("#photo-rental").value;
    if (!slug) { $("#photo-grid").innerHTML = ""; return; }
    const all = load();
    const r = all[slug]; if (!r) return;
    const photos = r.photos || [];
    $("#photo-grid").innerHTML = photos.map((src, i) => `
      <figure class="photo-cell">
        <img src="${escapeAttr(src)}" alt="photo ${i + 1}">
        <figcaption>
          <span>#${i + 1}${i === 0 ? " · cover" : ""}</span>
          <button data-i="${i}" class="photo-del">×</button>
        </figcaption>
      </figure>
    `).join("");
    $$(".photo-del").forEach(b => b.addEventListener("click", () => {
      const i = +b.getAttribute("data-i");
      const all2 = load();
      all2[slug].photos.splice(i, 1);
      save(all2);
      renderPhotos();
      renderRentals();
    }));
  }

  $("#photo-btn").addEventListener("click", () => $("#photo-input").click());
  $("#photo-rental").addEventListener("change", renderPhotos);
  $("#photo-input").addEventListener("change", async e => {
    const slug = $("#photo-rental").value;
    if (!slug) { alert("Pick a rental first."); return; }
    const files = Array.from(e.target.files || []);
    if (!files.length) return;
    const all = load();
    if (!all[slug].photos) all[slug].photos = [];
    for (const f of files.slice(0, 10 - all[slug].photos.length)) {
      const compressed = await compressImage(f, 1200, 0.82);
      all[slug].photos.push(compressed);
    }
    save(all);
    renderPhotos();
    renderRentals();
    e.target.value = "";
  });

  async function compressImage(file, maxW, quality) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      const reader = new FileReader();
      reader.onload = () => {
        img.onload = () => {
          const scale = Math.min(1, maxW / img.width);
          const w = Math.round(img.width * scale);
          const h = Math.round(img.height * scale);
          const cv = document.createElement("canvas");
          cv.width = w; cv.height = h;
          cv.getContext("2d").drawImage(img, 0, 0, w, h);
          resolve(cv.toDataURL("image/jpeg", quality));
        };
        img.onerror = reject;
        img.src = reader.result;
      };
      reader.onerror = reject;
      reader.readAsDataURL(file);
    });
  }

  // ------------ Init ------------
  document.addEventListener("DOMContentLoaded", () => {
    renderRentals();
    renderInbox();
    populatePhotoPicker();
  });
})();