← back to Designer Portfolio Pages
public/builder.js
229 lines
// Designer Mood Board Builder
// All user-visible content is escaped via escapeHtml() before DOM insertion
const els = {
q: document.getElementById("patternQuery"),
searchBtn: document.getElementById("searchBtn"),
results: document.getElementById("results"),
selected: document.getElementById("selected"),
selectedCount: document.getElementById("selectedCount"),
emptyHint: document.getElementById("emptyHint"),
createBtn: document.getElementById("createBtn"),
share: document.getElementById("share"),
title: document.getElementById("title"),
contactName: document.getElementById("contactName"),
business: document.getElementById("business"),
email: document.getElementById("email"),
phone: document.getElementById("phone"),
website: document.getElementById("website"),
password: document.getElementById("password"),
generateAiIntro: document.getElementById("generateAiIntro")
};
let selected = new Map(); // id -> pattern
function escapeHtml(s) {
const div = document.createElement("div");
div.textContent = String(s);
return div.innerHTML;
}
function createPatternCard(p, mode) {
const div = document.createElement("div");
const isSelected = selected.has(p.id);
div.className = "pattern" + (isSelected && mode === "results" ? " in-board" : "");
if (mode === "selected") div.className = "pattern in-board";
const img = document.createElement("img");
img.src = p.imageUrl;
img.alt = p.name;
img.className = "square";
img.loading = "lazy";
div.appendChild(img);
const nameDiv = document.createElement("div");
nameDiv.className = "patternName";
nameDiv.textContent = p.name;
div.appendChild(nameDiv);
const tagsDiv = document.createElement("div");
tagsDiv.className = "tags";
tagsDiv.textContent = (p.tags || []).slice(0, 5).join(" · ");
div.appendChild(tagsDiv);
const btn = document.createElement("button");
if (mode === "results" && isSelected) {
btn.className = "btn remove-mood";
btn.textContent = "Remove";
btn.addEventListener("click", () => {
selected.delete(p.id);
refreshSelected();
renderResults(lastResults);
});
} else if (mode === "results") {
btn.className = "btn add-mood";
btn.textContent = "Add to Mood Board";
btn.addEventListener("click", () => {
selected.set(p.id, p);
refreshSelected();
renderResults(lastResults);
});
} else {
btn.className = "btn remove-mood";
btn.textContent = "Remove";
btn.addEventListener("click", () => {
selected.delete(p.id);
refreshSelected();
renderResults(lastResults);
});
}
div.appendChild(btn);
return div;
}
function renderResults(patterns) {
els.results.textContent = "";
if (!patterns.length) {
const msg = document.createElement("div");
msg.className = "muted";
msg.textContent = "No patterns found. Try a different search term.";
els.results.appendChild(msg);
return;
}
patterns.forEach(p => {
els.results.appendChild(createPatternCard(p, "results"));
});
}
let lastResults = [];
function refreshSelected() {
const items = Array.from(selected.values());
els.selectedCount.textContent = items.length ? "(" + items.length + ")" : "";
els.emptyHint.style.display = items.length ? "none" : "block";
els.createBtn.disabled = items.length === 0;
els.selected.textContent = "";
items.forEach(p => {
els.selected.appendChild(createPatternCard(p, "selected"));
});
}
async function search() {
const q = (els.q.value || "").trim();
els.results.textContent = "";
const loading = document.createElement("div");
loading.className = "muted";
loading.textContent = "Searching...";
els.results.appendChild(loading);
try {
const res = await fetch("/api/patterns/search?q=" + encodeURIComponent(q));
const data = await res.json();
if (!res.ok) {
els.results.textContent = "";
const err = document.createElement("div");
err.className = "error";
err.textContent = data.error || "Search failed";
els.results.appendChild(err);
return;
}
lastResults = data.patterns || [];
renderResults(lastResults);
} catch (err) {
els.results.textContent = "";
const errDiv = document.createElement("div");
errDiv.className = "error";
errDiv.textContent = "Network error: " + err.message;
els.results.appendChild(errDiv);
}
}
async function createPortfolio() {
els.share.textContent = "";
els.createBtn.disabled = true;
els.createBtn.textContent = "Creating...";
const patternIds = Array.from(selected.keys());
const payload = {
title: (els.title.value || "").trim(),
contact: {
name: (els.contactName.value || "").trim(),
business: (els.business.value || "").trim(),
email: (els.email.value || "").trim(),
phone: (els.phone.value || "").trim(),
website: (els.website.value || "").trim()
},
password: (els.password.value || "").trim(),
patternIds,
generateAiIntro: Boolean(els.generateAiIntro.checked)
};
try {
const res = await fetch("/api/portfolios", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) {
const errDiv = document.createElement("div");
errDiv.className = "error";
errDiv.textContent = data.error || "Create failed";
els.share.appendChild(errDiv);
return;
}
const box = document.createElement("div");
box.className = "shareBox";
box.textContent = data.shareUrl;
els.share.appendChild(box);
const actions = document.createElement("div");
actions.style.cssText = "margin-top:10px;display:flex;gap:8px;align-items:center";
const copyBtn = document.createElement("button");
copyBtn.className = "btn primary";
copyBtn.textContent = "Copy Link";
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(data.shareUrl).then(() => {
copyBtn.textContent = "Copied!";
setTimeout(() => { copyBtn.textContent = "Copy Link"; }, 2000);
});
});
actions.appendChild(copyBtn);
const openLink = document.createElement("a");
openLink.className = "btn";
openLink.href = data.shareUrl;
openLink.target = "_blank";
openLink.rel = "noopener noreferrer";
openLink.textContent = "Open Mood Board";
actions.appendChild(openLink);
els.share.appendChild(actions);
} catch (err) {
const errDiv = document.createElement("div");
errDiv.className = "error";
errDiv.textContent = "Network error: " + err.message;
els.share.appendChild(errDiv);
} finally {
els.createBtn.disabled = selected.size === 0;
els.createBtn.textContent = "Create Mood Board Link";
}
}
els.searchBtn.addEventListener("click", search);
els.q.addEventListener("keydown", (e) => { if (e.key === "Enter") search(); });
els.createBtn.addEventListener("click", createPortfolio);
// Load all mock patterns on page load
search();
refreshSelected();