← back to Pattern Vault

public/index.html

98 lines

<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>The Pattern Vault — license original designs</title>
<style>
:root{--bg:#0d0b10;--card:#17141d;--ink:#efe9f7;--mut:#9a8fb0;--gold:#d4af6a;--line:#2a2436;--cols:4}
*{box-sizing:border-box;margin:0}
body{background:var(--bg);color:var(--ink);font:15px/1.55 "Avenir Next",system-ui,sans-serif}
header{padding:56px 6vw 28px;text-align:center;border-bottom:1px solid var(--line)}
h1{font-size:clamp(28px,4.5vw,52px);font-weight:600;letter-spacing:.12em;text-transform:uppercase}
h1 em{color:var(--gold);font-style:normal}
.tag{color:var(--mut);font-style:italic;margin-top:8px}
.badge{display:inline-block;margin-top:14px;padding:4px 14px;border:1px solid var(--gold);border-radius:99px;color:var(--gold);font-size:12px;letter-spacing:.08em}
.controls{display:flex;gap:14px;align-items:center;justify-content:center;padding:18px 6vw;flex-wrap:wrap}
select,input[type=range]{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:18px;padding:10px 6vw 60px}
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;overflow:hidden;transition:transform .15s}
.card:hover{transform:translateY(-3px);border-color:var(--gold)}
.card{position:relative}
.card.orig{border-color:#d4af6a55}
.origtag{position:absolute;top:8px;left:8px;background:#0d0a16e0;border:1px solid var(--gold);color:var(--gold);font-size:9.5px;letter-spacing:.09em;font-weight:600;padding:3px 7px;border-radius:99px}
.roomtag{position:absolute;bottom:8px;right:8px;background:#0d0a16d0;border:1px solid var(--line);color:var(--mut);font-size:9.5px;padding:3px 7px;border-radius:99px;pointer-events:none}
.card:hover .roomtag{color:var(--gold);border-color:var(--gold)}
#inhouseBand b{color:var(--gold)}
.card img{width:100%;aspect-ratio:1;object-fit:cover;display:block;background:#221d2c}
.pad{padding:12px 14px 16px}
.t{font-weight:600;font-size:14px}
.s{color:var(--mut);font-size:12.5px;margin:5px 0 10px;min-height:34px}
.tiers{display:flex;gap:6px;flex-wrap:wrap}
.tiers button{background:transparent;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:5px 9px;font-size:12px;cursor:pointer}
.tiers button:hover{border-color:var(--gold);color:var(--gold)}
.price{color:var(--gold)}
dialog{background:var(--card);color:var(--ink);border:1px solid var(--gold);border-radius:14px;padding:26px;max-width:420px}
dialog::backdrop{background:#000a}
dialog input{width:100%;margin:10px 0;background:var(--bg);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px}
dialog button{background:var(--gold);border:0;border-radius:8px;padding:10px 18px;font-weight:600;cursor:pointer}
.note{color:var(--mut);font-size:12px;margin-top:10px}
footer{border-top:1px solid var(--line);color:var(--mut);text-align:center;padding:26px;font-size:13px}
footer a{color:var(--gold)}
</style></head><body>
<header>
  <h1>The <em>Pattern</em> Vault</h1>
  <div class="tag">Original seamless wallpaper &amp; surface patterns — hand-designed, print-ready, licensed by the roll or exclusive</div>
  <div class="badge" id="modeBadge">loading…</div>
  <div style="margin-top:14px"><a href="/trends" style="color:var(--gold);text-decoration:none;font-size:13px;letter-spacing:.06em;border-bottom:1px solid var(--line);padding-bottom:2px">Trend Board →</a> &nbsp; <a href="/handoff" style="color:var(--gold);text-decoration:none;font-size:13px;letter-spacing:.06em;border-bottom:1px solid var(--line);padding-bottom:2px">Handoff →</a></div>
</header>
<div class="controls">
  <label>Sort <select id="sortSel">
    <option value="newest">Newest</option><option value="title">Title A→Z</option>
    <option value="category">Style</option><option value="color">Color</option>
  </select></label>
  <label>Density <input type="range" id="density" min="2" max="6" value="4"></label>
</div>
<div id="inhouseBand" style="text-align:center;color:var(--mut);font-size:13px;padding:0 6vw 6px"></div>
<div class="grid" id="grid"></div>
<footer>Printed wallpaper of these designs → <a href="https://wallpapersback.com" rel="noopener noreferrer">wallpapersback.com</a> · Every design is an original from our studio pipeline, seam-verified and print-clean at 150 DPI. · <a href="/setup.html">setup guide</a></footer>
<dialog id="dlg"><div id="dlgTitle" class="t"></div><div class="s" id="dlgTier"></div>
  <input id="email" type="email" placeholder="your email">
  <button id="goBtn">Continue</button><div class="note" id="dlgNote"></div></dialog>
<script>
let items=[],cfg={};
const grid=document.getElementById('grid'),dlg=document.getElementById('dlg');
const sortSel=document.getElementById('sortSel'),density=document.getElementById('density');
sortSel.value=localStorage.pvSort||'newest';density.value=localStorage.pvCols||'4';
document.documentElement.style.setProperty('--cols',density.value);
sortSel.onchange=()=>{localStorage.pvSort=sortSel.value;render()};
density.oninput=()=>{localStorage.pvCols=density.value;document.documentElement.style.setProperty('--cols',density.value)};
function sorted(){const s=sortSel.value,a=[...items];
  if(s==='title')a.sort((x,y)=>x.title.localeCompare(y.title));
  else if(s==='category')a.sort((x,y)=>(x.category||'').localeCompare(y.category||''));
  else if(s==='color')a.sort((x,y)=>(x.dominantHex||'').localeCompare(y.dominantHex||''));
  else a.sort((x,y)=>(y.createdAt||'').localeCompare(x.createdAt||''));return a}
function render(){const n=items.filter(x=>x.category==='in-house-original').length;
  const band=document.getElementById('inhouseBand');
  if(band)band.innerHTML=n?`<b>✦ ${n} In-House Originals</b> — designed in our own hand from live marketplace trend research, every one settlement-cleared. The rest are licensed Wallpaper's Back designs.`:'';
  grid.innerHTML=sorted().map(d=>{const orig=d.category==='in-house-original';
  const hov=d.roomImg?` onmouseenter="this.querySelector('img').src='${d.roomImg}'" onmouseleave="this.querySelector('img').src='${d.img}'"`:'';
  return `<div class="card${orig?' orig':''}"${hov}><img loading="lazy" src="${d.img}" alt="${d.title}">${orig?'<span class="origtag">✦ IN-HOUSE ORIGINAL</span>':''}${d.roomImg?'<span class="roomtag">🛋 see in a room</span>':''}<div class="pad">
  <div class="t">${d.title}</div><div class="s">${d.styleLine||''}</div>
  <div class="tiers">${(d.licenseTiers||[]).map(t=>`<button onclick='pick(${JSON.stringify(d.id)},${JSON.stringify(t.tier)})'>${t.label||t.tier} <span class="price">$${t.priceUsd??'—'}</span></button>`).join('')}</div>
  </div></div>`}).join('')}
let sel={};
function pick(id,tier){const d=items.find(x=>String(x.id)===String(id));sel={id,tier};
  document.getElementById('dlgTitle').textContent=d.title;
  document.getElementById('dlgTier').textContent=(d.licenseTiers.find(t=>t.tier===tier)||{}).label+' license';
  document.getElementById('dlgNote').textContent=cfg.note||'';dlg.showModal()}
document.getElementById('goBtn').onclick=async()=>{
  try{
    const r=await fetch('/api/license/checkout',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({designId:sel.id,tier:sel.tier,email:document.getElementById('email').value})});
    const j=await r.json();
    if(j.url)location=j.url;else{document.getElementById('dlgNote').textContent=j.note||j.error;setTimeout(()=>dlg.close(),1800)}
  }catch(e){document.getElementById('dlgNote').textContent='Network error — try again'}};
(async()=>{cfg=await(await fetch('/api/config')).json();
  document.getElementById('modeBadge').textContent=cfg.mode==='test'?'TEST MODE — no real charges':'Preview — inquiries only until checkout opens';
  items=(await(await fetch('/api/catalog')).json()).items;render()})();
</script></body></html>