← 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();