← back to Quadrille House Site

public/product.html

207 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Designer Wallcoverings</title>
<meta name="twitter:card" content="summary_large_image">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
</head>
<body>

<a class="corner ul wordmark" href="https://www.designerwallcoverings.com">Designer Wallcoverings</a>
<nav class="corner ur">
  <a href="/">The House</a>
  <a href="https://www.designerwallcoverings.com/collections/all" target="_blank" rel="noopener">All Wallcoverings</a>
  <a href="https://www.designerwallcoverings.com/pages/trade" target="_blank" rel="noopener">Trade</a>
</nav>

<main class="pdp" id="pdp"><div class="crumbs">Loading…</div></main>

<!-- QUOTE / MEMO-SAMPLE MODAL (held brands) -->
<div class="qmodal" id="qmodal" hidden>
  <div class="qcard">
    <button class="qx" id="qx" aria-label="Close">×</button>
    <div class="eyebrow">To the Trade</div>
    <h3 id="qtitle">Request a memo sample &amp; quote</h3>
    <p class="qsub" id="qsub"></p>
    <form id="qform">
      <div class="qrow"><label>Name<input name="name" required></label><label>Company<input name="company"></label></div>
      <label>Email<input type="email" name="email" required></label>
      <label>Message <span class="opt">(quantity, project, ship-to)</span><textarea name="message" rows="3"></textarea></label>
      <button class="cta" type="submit">Send request</button>
      <div class="qnote">Sold to the trade. Our team follows up with memo-sample availability and pricing — no public price is shown.</div>
    </form>
    <div class="qdone" id="qdone" hidden></div>
  </div>
</div>

<a class="shopbar" href="https://www.designerwallcoverings.com" target="_blank" rel="noopener">Shop at <b>Designer Wallcoverings</b> →</a>

<script>
const handle=location.pathname.split('/').pop();
const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
function specRow(k,v){return v?`<div class="row"><span class="k">${k}</span><span class="v">${esc(v)}</span></div>`:'';}
let PROD=null, CFG=null;

(async()=>{
  const [cfg,r]=await Promise.all([fetch(location.origin+'/api/config').then(x=>x.json()).catch(()=>({})), fetch(location.origin+'/api/product/'+handle)]);
  CFG=cfg;
  if(cfg.slug) document.body.dataset.line=cfg.slug;
  if(cfg.palette){const rs=document.documentElement.style;['bg','paper','ink','accent','gold','line','taupe'].forEach(k=>{if(cfg.palette[k])rs.setProperty('--'+k,cfg.palette[k]);});}
  document.querySelector('.corner.ul').setAttribute('href', cfg.houseUrl || 'https://www.designerwallcoverings.com');
  if(!r.ok){document.getElementById('pdp').innerHTML='<div class="crumbs"><a href="/">← Back</a></div><h1 style="font-family:var(--serif)">Not found</h1>';return;}
  const p=await r.json(); PROD=p;
  const BRAND=p.brand;
  document.title=`${p.display_name||p.color||p.title} · ${BRAND} | Designer Wallcoverings`;
  const md=document.querySelector('meta[name=description]')||document.head.appendChild(Object.assign(document.createElement('meta'),{name:'description'}));
  md.content=`${p.display_name||p.title} — ${BRAND}, curated by Designer Wallcoverings.`;
  const imgs=(p.images&&p.images.length)?p.images:[p.swatch].filter(Boolean);
  const main=imgs[0];

  const isLive = p.cta_mode==='live' && p.store_url;
  const isSample = p.cta_mode==='sample';
  const samplePrice = (p.sample_price!=null) ? Number(p.sample_price).toFixed(2) : '4.25';
  const ctaMain = isLive
    ? `<a class="cta" href="${esc(p.store_url)}" target="_blank" rel="noopener">View &amp; order on Designer Wallcoverings →</a>`
    : isSample
    ? `<div class="price-sample">Sample — $${samplePrice}</div><button class="cta" id="reqBtn" type="button">Order a sample — $${samplePrice} →</button>`
    : `<button class="cta" id="reqBtn" type="button">Request memo sample &amp; quote →</button>`;
  const ctaGhost = isLive
    ? `<a class="cta ghost" href="${esc(p.store_url)}" target="_blank" rel="noopener">Order a sample</a>`
    : isSample
    ? `<div class="trade-note">To the trade · hand-screened to order · sample ships before you specify</div>`
    : `<div class="trade-note">To the trade · hand-screened to order</div>`;

  document.getElementById('pdp').innerHTML=`
    <div class="crumbs"><a href="https://www.designerwallcoverings.com">Designer Wallcoverings</a> &nbsp;/&nbsp; <a href="/">The Quadrille House</a> &nbsp;/&nbsp; <a href="/">${esc(BRAND)}</a> &nbsp;/&nbsp; ${esc(p.display_name||p.color||p.title)}</div>
    <div class="pdp-top">
      <div class="gallery">
        <div class="main" id="main" style="background-image:url('${main}')"></div>
        ${imgs.length>1?`<div class="thumbs">${imgs.map((s,i)=>`<div class="t ${i===0?'on':''}" data-s="${s}" style="background-image:url('${s}')"></div>`).join('')}</div>`:''}
      </div>
      <div class="rail">
        <div class="ser">${esc(BRAND)}${p.display_eyebrow?' · '+esc(p.display_eyebrow):''}</div>
        <h1>${esc(p.display_name||p.color||p.title)}</h1>
        <div class="colr">${esc(p.sku)}${p.composition?' · '+esc(p.composition):''}</div>
        ${ctaMain}
        ${ctaGhost}
        <div class="share" id="share" data-url="${esc(isLive?p.store_url:location.href)}" data-title="${esc((p.display_name||p.color||p.title)+' · '+BRAND+' | Designer Wallcoverings')}" data-img="${esc(main)}"></div>
        <div class="spec">
          ${specRow('Item code',p.sku)}
          ${specRow('Pattern',p.series)}
          ${specRow('Colorway',p.color)}
          ${specRow('Composition',p.composition)}
          ${specRow('Width',p.width)}
          ${specRow('Repeat',p.repeat)}
          ${specRow('Match',p.match_type)}
          ${specRow('Finish',p.finish)}
          ${specRow('Application',p.application)}
          ${specRow('Coverage',p.coverage)}
        </div>
      </div>
    </div>
    <div class="story">
      <div><h3>The pattern</h3><div>${p.body_html||'<p>'+esc(p.title)+' — a hand-screened design from '+esc(BRAND)+', part of the Quadrille house.</p>'}</div></div>
      <div><h3>Specifications</h3><p>${[p.composition,p.width&&('Width '+p.width),p.repeat&&('Repeat '+p.repeat),p.match_type].filter(Boolean).map(esc).join(' · ')||'Hand-screened to order.'}</p></div>
      <div><h3>${isLive?'Order':isSample?'Order a sample':'To the trade'}</h3><p>${isLive?'Available now at Designer Wallcoverings — order a sample before specifying.':isSample?('Order a '+esc(BRAND)+' sample for $'+samplePrice+' to see the colorway and hand in person. Hand-screened to order; full pricing is provided to the trade.'):'Hand-screened to order. Request a memo sample and trade quote — pricing is provided to the trade, not shown publicly.'}</p></div>
    </div>
    <section class="pairs" id="pairs" hidden>
      <h3>Pairs well with</h3>
      <div class="pair-grid" id="pairGrid"></div>
    </section>`;

  // JSON-LD Product (no price; held items carry no offer URL)
  const ld={"@context":"https://schema.org","@type":"Product",name:`${p.display_name||p.title} by ${BRAND}`,sku:p.sku,
    brand:{"@type":"Brand",name:BRAND},category:p.product_type||'Wallcoverings',image:imgs,
    description:(p.body_html||p.title||'').replace(/<[^>]+>/g,'').slice(0,300)};
  if(isLive) ld.offers={"@type":"Offer",availability:"https://schema.org/InStock",url:p.store_url};
  else if(isSample) ld.offers={"@type":"Offer",availability:"https://schema.org/InStock",price:samplePrice,priceCurrency:"USD",url:location.href,itemCondition:"https://schema.org/NewCondition",name:"Sample"};
  const s=document.createElement('script');s.type='application/ld+json';s.textContent=JSON.stringify(ld);document.head.appendChild(s);

  // canonical → live PDP for China Seas (complements, never competes); held items self-canonical
  const c=document.createElement('link');c.rel='canonical';c.href=isLive?p.store_url:location.href;document.head.appendChild(c);

  // og
  [['og:title',document.title],['og:description',md.content],['og:image',main],['og:type','product']].forEach(([k,v])=>{
    const m=document.createElement('meta');m.setAttribute('property',k);m.content=v;document.head.appendChild(m);});

  // pairs
  try{
    const pr=await (await fetch(location.origin+'/api/pairs/'+handle)).json();
    if(pr.pairs&&pr.pairs.length){
      document.getElementById('pairGrid').innerHTML=pr.pairs.map(q=>`
        <a class="pcard" href="/product/${q.handle}">
          <img loading="lazy" decoding="async" alt="${esc(q.title)} — ${esc(q.brand)}" src="${q.swatch||q.room||''}">
          <div class="pc-b"><span class="pc-s">${esc(q.brand)}</span><span class="pc-n">${esc(q.display_name||q.color||q.title)}</span></div>
        </a>`).join('');
      document.getElementById('pairs').hidden=false;
    }
  }catch(e){}

  // gallery swap
  document.querySelectorAll('.thumbs .t').forEach(t=>t.onclick=()=>{
    document.getElementById('main').style.backgroundImage=`url('${t.dataset.s}')`;
    document.querySelectorAll('.thumbs .t').forEach(x=>x.classList.remove('on'));t.classList.add('on');
  });

  // quote modal wiring (held brands)
  if(!isLive){
    const reqBtn=document.getElementById('reqBtn');
    if(reqBtn) reqBtn.onclick=openQuote;
  }
  renderShare();
})();

function openQuote(){
  const m=document.getElementById('qmodal');
  document.getElementById('qsub').textContent=`${PROD.brand} · ${PROD.series||''} ${PROD.color?'— '+PROD.color:''} · ${PROD.sku}`;
  m.hidden=false; document.body.style.overflow='hidden';
}
function closeQuote(){document.getElementById('qmodal').hidden=true;document.body.style.overflow='';}
document.getElementById('qx').onclick=closeQuote;
document.getElementById('qmodal').addEventListener('click',e=>{if(e.target.id==='qmodal')closeQuote();});
document.getElementById('qform').addEventListener('submit',async e=>{
  e.preventDefault();
  const f=e.target, fd=Object.fromEntries(new FormData(f).entries());
  const body={sku:PROD.sku,brand:PROD.brand,pattern:PROD.series,color:PROD.color,...fd};
  try{
    const r=await (await fetch(location.origin+'/api/quote',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
    f.hidden=true;
    const d=document.getElementById('qdone'); d.hidden=false;
    d.innerHTML=`<p>${esc(r.message||'Request received.')}</p><button class="cta ghost" type="button" onclick="closeQuote()">Close</button>`;
  }catch(err){ alert('Could not send — please email info@designerwallcoverings.com'); }
});

function renderShare(){
  const el=document.getElementById('share'); if(!el)return;
  const url=el.dataset.url, title=el.dataset.title||'', img=el.dataset.img||'';
  const U=encodeURIComponent(url), T=encodeURIComponent(title), I=encodeURIComponent(img);
  const ic={
    pin:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 2C6.5 2 4 5.8 4 8.9c0 1.9.7 3.6 2.3 4.2.3.1.5 0 .6-.3l.2-.9c.1-.3 0-.4-.2-.6-.4-.5-.7-1.2-.7-2.1 0-2.7 2-5.1 5.3-5.1 2.9 0 4.5 1.8 4.5 4.1 0 3.1-1.4 5.7-3.4 5.7-1.1 0-1.9-.9-1.7-2 .3-1.3.9-2.7.9-3.6 0-.8-.4-1.5-1.4-1.5-1.1 0-2 1.1-2 2.7 0 1 .3 1.6.3 1.6l-1.4 5.7c-.4 1.6 0 3.7 0 3.9 0 .1.2.2.3.1.1-.1 1.6-2 2.1-3.8l.8-3c.4.8 1.5 1.4 2.7 1.4 3.6 0 6-3.3 6-7.6C20.4 5 17 2 12 2z"/></svg>',
    fb:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M22 12a10 10 0 10-11.6 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.2c-1.2 0-1.6.8-1.6 1.6V12h2.7l-.4 2.9h-2.3v7A10 10 0 0022 12z"/></svg>',
    x:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M18.9 2H22l-7.3 8.3L23.3 22h-6.8l-5.3-7-6.1 7H2l7.8-8.9L1.2 2H8l4.8 6.3L18.9 2zm-1.2 18h1.9L7.1 4H5.1l12.6 16z"/></svg>',
    mail:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm8 7L4 6.5V18h16V6.5L12 11z"/></svg>',
    link:'<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor"><path d="M10.6 13.4a1 1 0 001.4 0l4-4a3 3 0 00-4.2-4.2l-1.3 1.3a1 1 0 101.4 1.4l1.3-1.3a1 1 0 011.4 1.4l-4 4a1 1 0 000 1.4zm2.8-2.8a1 1 0 00-1.4 0l-4 4a3 3 0 004.2 4.2l1.3-1.3a1 1 0 10-1.4-1.4l-1.3 1.3a1 1 0 01-1.4-1.4l4-4a1 1 0 000-1.4z"/></svg>',
  };
  el.innerHTML=`<span class="share-lbl">Share</span>
    <a class="sbtn pin" title="Pin on Pinterest" target="_blank" rel="noopener" href="https://pinterest.com/pin/create/button/?url=${U}&media=${I}&description=${T}">${ic.pin}</a>
    <a class="sbtn fb" title="Share on Facebook" target="_blank" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u=${U}">${ic.fb}</a>
    <a class="sbtn x" title="Share on X" target="_blank" rel="noopener" href="https://twitter.com/intent/tweet?url=${U}&text=${T}">${ic.x}</a>
    <a class="sbtn mail" title="Email" href="mailto:?subject=${T}&body=${T}%20${U}">${ic.mail}</a>
    <button class="sbtn copy" title="Copy link" type="button">${ic.link}</button>`;
  el.querySelector('.copy').addEventListener('click',async()=>{
    const b=el.querySelector('.copy');
    try{ if(navigator.share){await navigator.share({title,url});return;}
      await navigator.clipboard.writeText(url); b.classList.add('done'); b.title='Copied!';
      setTimeout(()=>{b.classList.remove('done');b.title='Copy link';},1600);
    }catch(e){}
  });
}
</script>
</body>
</html>