← 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, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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();
});
})();