`; resultCard.innerHTML = ""; topMatchesEl.innerHTML = ""; return; } // Accept both long + short query params let thermal = parseIntSafe(getParam("thermal"), null); let support = parseIntSafe(getParam("support"), null); let pressure = parseIntSafe(getParam("pressure"), null); let sensory = parseIntSafe(getParam("sensory"), null); if (thermal === null) thermal = parseIntSafe(getParam("t"), 50); if (support === null) support = parseIntSafe(getParam("s"), 50); if (pressure === null) pressure = parseIntSafe(getParam("p"), 50); if (sensory === null) sensory = parseIntSafe(getParam("se"), 50); const profile = { thermal: clamp(thermal ?? 50, 0, 99), support: clamp(support ?? 50, 0, 99), pressure: clamp(pressure ?? 50, 0, 99), sensory: clamp(sensory ?? 50, 0, 99) }; const type = (getParam("type") || "bed").toLowerCase(); const typeLabel = type === "body" ? "Body Pillow" : (type === "travel" ? "Travel Pillow" : "Standard Bed Pillow"); const THEME = { primary: getComputedStyle(document.documentElement).getPropertyValue('--pcq-primary').trim() || "#04334B", textMain: getComputedStyle(document.documentElement).getPropertyValue('--pcq-text-main').trim() || "#041018", textMute: getComputedStyle(document.documentElement).getPropertyValue('--pcq-text-muted').trim() || "#6b7280", cThermal: getComputedStyle(document.documentElement).getPropertyValue('--c-thermal').trim() || "#f6b6b6", cSupport: getComputedStyle(document.documentElement).getPropertyValue('--c-support').trim() || "#b9e3d0", cPressure:getComputedStyle(document.documentElement).getPropertyValue('--c-pressure').trim() || "#9cc9d6", cSensory: getComputedStyle(document.documentElement).getPropertyValue('--c-sensory').trim() || "#d3c2ef", stroke: "#97A6B6" }; function buildScoreFinderUrl(p){ const base = "https://www.findtheperfectpillow.com/perfect-pillow-score-finder/"; const qs = new URLSearchParams(); qs.set("t", String(p.thermal)); qs.set("s", String(p.support)); qs.set("p", String(p.pressure)); qs.set("se", String(p.sensory)); qs.set("code", `${p.thermal} | ${p.support} | ${p.pressure} | ${p.sensory}`); return base + "?" + qs.toString(); } function renderScoreBlock(p){ const wrap = document.createElement("div"); const grid = document.createElement("div"); grid.className = "score-wrap"; const items = [ { label:"Thermal", value:p.thermal, color:THEME.cThermal }, { label:"Support", value:p.support, color:THEME.cSupport }, { label:"Pressure", value:p.pressure, color:THEME.cPressure }, { label:"Sensory", value:p.sensory, color:THEME.cSensory } ]; items.forEach(it=>{ const chip = document.createElement("div"); chip.className = "score-chip"; chip.innerHTML = `
${it.value}
${it.label}
`; grid.appendChild(chip); }); const code = `${p.thermal} | ${p.support} | ${p.pressure} | ${p.sensory}`; const codeRow = document.createElement("div"); codeRow.className = "code-row"; codeRow.innerHTML = `
Selected type: ${escapeHtml(typeLabel)} ${escapeHtml(code)} Copy code Download PNG `; const finderRow = document.createElement("div"); finderRow.className = "finder-row"; finderRow.innerHTML = ` Compare this code in the Score Finder `; wrap.appendChild(grid); wrap.appendChild(codeRow); wrap.appendChild(finderRow); return wrap; } scoreBlock.innerHTML = ""; scoreBlock.appendChild(renderScoreBlock(profile)); // Card generator: fixed output size (1000x560), no giant retina scaling. function buildCardCanvas(p){ const W = 1000, H = 560; const canvas = document.createElement("canvas"); canvas.width = W; canvas.height = H; const ctx = canvas.getContext("2d"); ctx.fillStyle = "#ffffff"; ctx.fillRect(0, 0, W, H); ctx.fillStyle = THEME.primary; ctx.font = "900 52px system-ui, -apple-system, Segoe UI, sans-serif"; ctx.fillText("Your Perfect Pillow Score™", 60, 85); ctx.fillStyle = THEME.textMute; ctx.font = "700 24px system-ui, -apple-system, Segoe UI, sans-serif"; ctx.fillText("FindThePerfectPillow.com", 60, 120); const circles = [ { label:"THERMAL", value:p.thermal, color:THEME.cThermal }, { label:"SUPPORT", value:p.support, color:THEME.cSupport }, { label:"PRESSURE", value:p.pressure, color:THEME.cPressure }, { label:"SENSORY", value:p.sensory, color:THEME.cSensory } ]; const cx = [160, 390, 610, 840]; const cy = 300; const r = 88; circles.forEach((it, i) => { ctx.beginPath(); ctx.arc(cx[i], cy, r, 0, Math.PI*2); ctx.fillStyle = it.color; ctx.fill(); ctx.lineWidth = 5; ctx.strokeStyle = THEME.stroke; ctx.stroke(); ctx.fillStyle = THEME.textMain; ctx.font = "900 54px system-ui, -apple-system, Segoe UI, sans-serif"; const val = String(it.value); const tw = ctx.measureText(val).width; ctx.fillText(val, cx[i] - tw/2, cy + 18); ctx.fillStyle = THEME.textMain; ctx.font = "900 18px system-ui, -apple-system, Segoe UI, sans-serif"; const lab = it.label; const lw = ctx.measureText(lab).width; ctx.fillText(lab, cx[i] - lw/2, cy + 140); }); const code = `${p.thermal} | ${p.support} | ${p.pressure} | ${p.sensory}`; ctx.fillStyle = THEME.primary; ctx.font = "900 26px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace"; ctx.fillText("CODE:", 60, 510); ctx.fillStyle = THEME.textMain; ctx.font = "900 26px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace"; ctx.fillText(" " + code, 140, 510); return canvas; } function downloadCanvasPNG(canvas, filename){ const a = document.createElement("a"); a.download = filename; a.href = canvas.toDataURL("image/png"); document.body.appendChild(a); a.click(); a.remove(); } document.addEventListener("click", async (e) => { if (e.target && e.target.id === "ftpp-copy") { const codeEl = document.getElementById("ftpp-code"); const status = document.getElementById("ftpp-copy-status"); try{ await navigator.clipboard.writeText(codeEl.textContent.trim()); status.textContent = "Copied."; setTimeout(()=>status.textContent="", 1400); }catch(err){ status.textContent = "Copy blocked by browser."; setTimeout(()=>status.textContent="", 1600); } } if (e.target && e.target.id === "ftpp-download-png") { const canvas = buildCardCanvas(profile); downloadCanvasPNG(canvas, "perfect-pillow-score.png"); } }); function buildPrimaryHTML(p){ const codeStr = p.code || `${p.thermal} | ${p.support} | ${p.pressure} | ${p.sensory}`; const viewUrl = p.view_url || (p.links && (p.links.discount || p.links.brandsite || p.links.amazon || p.links.macys || p.links.nordstrom)) || ""; // Only show fields if present const metaPairs = [ ["Strength", p.strength], ["Best For", p.best_for], ["Why It Wins", p.why_wins], ["Tradeoff", p.tradeoff], ["Misuse Risk", p.misuse_risk], ["Material", p.material], ["Cover", p.cover], ["Shape", p.shape], ["Firmness", p.firmness], ["Loft", p.loft], ["Loft (in)", p.loft_in], ["Adjustable Loft", p.adjustable_loft], ["Size", p.size_name], ].filter(x => x[1] && String(x[1]).trim() !== ""); const metaHtml = metaPairs.map(([k,v]) => `
`).join(""); const ctAs = []; if (viewUrl) ctAs.push(`View this pillow`); ctAs.push(`Compare this code in the Score Finder`); return `
${escapeHtml(p.pillow || "Your Best Match")}
Score: ${escapeHtml(codeStr)} • Match: ${escapeHtml(String(p.match ?? 0))}%
${metaHtml}
${ctAs.join("")}
`; } function buildAltCard(p){ const codeStr = p.code || `${p.thermal} | ${p.support} | ${p.pressure} | ${p.sensory}`; const viewUrl = p.view_url || (p.links && (p.links.discount || p.links.brandsite || p.links.amazon || p.links.macys || p.links.nordstrom)) || ""; return `
${escapeHtml(p.pillow || "")}
${escapeHtml(String(p.match ?? 0))}% match
Score: ${escapeHtml(codeStr)}
${viewUrl ? `
View this pillow
` : ""}
`; } async function sendEmailIfNeeded(){ if (emailOptin !== "1" || !emailAddr) return; emailTitle.style.display = "block"; emailStatus.style.display = "block"; emailStatus.textContent = "Sending your score…"; try{ const payload = { email: emailAddr, thermal: profile.thermal, support: profile.support, pressure: profile.pressure, sensory: profile.sensory, type: type, aud: aud }; const res = await fetch(EMAIL_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), credentials: "same-origin" }); const data = await res.json(); if (data && data.ok) { emailStatus.textContent = data.sent ? "Email sent. Check your inbox." : (data.message || "Email queued."); } else { emailStatus.textContent = "Email wasn’t sent. (Mail setup may be blocking it.)"; } }catch(e){ emailStatus.textContent = "Email wasn’t sent. (Mail setup may be blocking it.)"; } } async function loadMatches(){ topMatchesEl.innerHTML = "
Loading matches…
"; const params = new URLSearchParams(); params.set("thermal", String(profile.thermal)); params.set("support", String(profile.support)); params.set("pressure", String(profile.pressure)); params.set("sensory", String(profile.sensory)); params.set("limit", String(DEFAULT_LIMIT)); params.set("type", type); if (aud) params.set("audience", aud); try{ const res = await fetch(API_ENDPOINT + "?" + params.toString(), { method:"GET", credentials:"same-origin", cache:"no-store" }); const data = await res.json(); if(!data || data.ok !== true){ resultCard.innerHTML = `
Couldn’t load your matches
${escapeHtml(data?.error || "Please refresh and try again.")}
`; topMatchesEl.innerHTML = ""; return; } const primary = data.primary || (data.results && data.results[0]) || null; if(!primary){ resultCard.innerHTML = `
No matches found
The audience/type filter may not have entries yet.
`; topMatchesEl.innerHTML = ""; return; } resultCard.innerHTML = buildPrimaryHTML(primary); // Merge: in-type results + smart alternatives (no duplicates) const listA = Array.isArray(data.results) ? data.results : []; const listB = Array.isArray(data.smart_alternatives) ? data.smart_alternatives : []; const seen = new Set([primary.pillow]); const merged = []; [...listA, ...listB].forEach(x=>{ if (!x || !x.pillow) return; if (seen.has(x.pillow)) return; seen.add(x.pillow); merged.push(x); }); topMatchesEl.innerHTML = ""; if (!merged.length) { topMatchesEl.innerHTML = `
No alternates available (yet)
Your strongest-fit match is clear. Alternates will appear automatically as the database grows.
`; return; } merged.slice(0, DEFAULT_LIMIT - 1).forEach(p=>{ topMatchesEl.insertAdjacentHTML("beforeend", buildAltCard(p)); }); subcopy.textContent = "Here’s your score + your strongest-fit match from the database."; // Send email after a successful load (so user gets a valid link + code) sendEmailIfNeeded(); }catch(e){ resultCard.innerHTML = `
Couldn’t load your matches
We couldn’t reach the matcher service. Refresh and try again.
`; topMatchesEl.innerHTML = ""; } } loadMatches(); });