← back to Muralsource Internal

public/product.html

239 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>
<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 Collection</a>
  <a href="https://www.designerwallcoverings.com/collections/all" target="_blank" rel="noopener noreferrer">All Wallcoverings</a>
  <span class="internal-pill" title="Login-gated internal page — not customer-facing">INTERNAL</span>
</nav>

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

<a class="shopbar" href="/">← Back to the Mural Source collection at <b>Designer Wallcoverings</b></a>

<!-- INTERNAL REQUEST MODAL — memo / stock / price -->
<div class="modal-back" id="inqBack">
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="inqTitle">
    <button class="mclose" id="inqClose" aria-label="Close">×</button>
    <div class="eyebrow">Internal — Designer Wallcoverings</div>
    <h3 id="inqTitle">Request a Memo Sample</h3>
    <div class="msub" id="inqSub">Logged for purchasing follow-up.</div>
    <input type="hidden" id="inqType" value="memo">
    <div id="inqQtyWrap" hidden>
      <label for="inqQty">Quantity (optional)</label>
      <input id="inqQty" placeholder="e.g. 30 yards">
    </div>
    <label for="inqNote">Note (optional) — <b>the vendor sees this: never include a client name</b></label>
    <textarea id="inqNote" placeholder="Sidemark by account #, timeline…"></textarea>
    <div class="preview" id="inqPrev" hidden></div>
    <div class="msg" id="inqMsg"></div>
    <div class="mrow"><button class="cta" id="inqSend" style="margin-top:0">Request Memo</button></div>
  </div>
</div>

<script>
const handle=decodeURIComponent(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 VM={}; // vendorMeta from /api/config — phone, account #, discount, pricing model
const money=n=>n==null?null:'$'+Number(n).toFixed(2);

// Internal pricing — retail / designer net / our cost, per the line's pricing model.
// 'map' lines (Kravet family): retail = MAP = whls cost × 1.5; our cost = whls.
// Everything else: retail = cost/0.65/0.85, net = cost/0.65, our cost = cost minus
// the line's vendor_discount_pct when one is on file. Explicit list price wins for retail.
function priceTiers(p){
  const cost=p.cost!=null?Number(p.cost):null, list=p.price!=null?Number(p.price):null;
  const disc=VM.discount_pct!=null?Number(VM.discount_pct):null;
  let retail=null, net=null, ourCost=null;
  if(VM.pricing_model==='map' && cost!=null){ retail=cost*1.5; net=cost*1.5; ourCost=cost; }
  else{
    ourCost = cost!=null ? (disc!=null? cost*(1-disc/100) : cost) : null;
    net     = cost!=null ? cost/0.65 : (list!=null? list*0.85 : null);
    retail  = list!=null ? list : (cost!=null? cost/0.65/0.85 : null);
  }
  return {retail, net, ourCost};
}
function priceBox(p){
  const t=priceTiers(p);
  const unit=VM.pricing_unit?('/'+String(VM.pricing_unit).replace(/^per\s*/i,'').replace('yard','yd').replace('roll','roll')):'';
  const row=(k,v)=>`<div class="row"><span class="k">${k}</span><span class="v">${v!=null?esc(money(v)+unit):'—'}</span></div>`;
  const empty=t.retail==null&&t.net==null&&t.ourCost==null;
  return `<div class="pricebox">
    ${row('Retail',t.retail)}
    ${row('Designer Net',t.net)}
    ${row('Our Cost'+(VM.discount_pct!=null?' (after '+VM.discount_pct+'% discount)':''),t.ourCost)}
    ${p.price_code?`<div class="row"><span class="k">Vendor Price Code</span><span class="v">${esc(p.price_code)}</span></div>`:''}
    ${empty?'<div class="pnote">No pricing on file for this line — use <b>Get Price</b> below.</div>':''}
  </div>`;
}

(async()=>{
  const [cfg,r]=await Promise.all([fetch(location.origin+'/api/config').then(x=>x.json()).catch(()=>({})), fetch(location.origin+'/api/product/'+encodeURIComponent(handle))]);
  const LINE = cfg.line || cfg.vendor || 'Mural Source';
  VM = cfg.vendorMeta || {};
  if(cfg.slug){ document.body.dataset.line = cfg.slug; }
  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();
  document.title=`${p.display_name||p.color||p.title} · ${LINE} | Designer Wallcoverings`;
  const imgs=(p.images&&p.images.length)?p.images:[p.swatch].filter(Boolean);
  const main=imgs[0]||'';
  const label=`${p.display_eyebrow||''} ${p.display_name||p.color||''}`.trim();

  document.getElementById('pdp').innerHTML=`
    <div class="crumbs"><a href="https://www.designerwallcoverings.com">Designer Wallcoverings</a> &nbsp;/&nbsp; <a href="/">${esc(LINE)}</a> &nbsp;/&nbsp; <a href="/#catalog">${esc(p.display_eyebrow||p.series||'')}</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(p.display_eyebrow||p.book||LINE)}</div>
        <h1>${esc(p.display_name||p.color||p.title)}</h1>
        <div class="colr">${esc(VM.name||LINE)} · ${esc(p.sku||'')}${p.material?' · '+esc(p.material):''}</div>
        ${priceBox(p)}
        <div class="act3">
          <button class="cta" data-act="memo">Request Memo</button>
          <button class="cta" data-act="stock">Check Stock</button>
          <button class="cta" data-act="price">Get Price</button>
        </div>
        ${!VM.email?`<div class="noemail">✉️ No vendor email on file — stock &amp; price checks are logged internally; confirm by phone${VM.phone?': '+esc(VM.phone):''}.</div>`:''}
        ${VM.phone?`<a class="cta ghost call" href="tel:+1${(VM.phone||'').replace(/[^0-9]/g,'')}">📞 Call ${esc(VM.buy_from||VM.name||LINE)} — ${esc(VM.phone)}${VM.account_number?' · Acct # '+esc(VM.account_number):''}</a>`:''}
        <div class="spec">
          ${specRow('Vendor',VM.name||LINE)}
          ${specRow('Buy From / Distributor',VM.buy_from)}
          ${specRow('Vendor SKU / Model #',p.sku)}
          ${specRow('DW SKU',p.dw_sku)}
          ${specRow('Our Account #',VM.account_number)}
          ${specRow('Vendor Phone',VM.phone)}
          ${specRow('Pattern',p.series)}
          ${specRow('Colorway',p.color)}
          ${specRow('Width',p.width)}
          ${specRow('Length',p.length)}
          ${specRow('Repeat',p.repeat)}
          ${specRow('Material',p.material)}
          ${specRow('Match',p.match)}
          ${specRow('Format',p.book)}
        </div>
      </div>
    </div>
    <div class="story">
      <div><h3>The story</h3><div>${p.body_html?('<p>'+esc(p.body_html.slice(0,700))+(p.body_html.length>700?'…':'')+'</p>'):('<p>'+esc(p.title)+' from the '+esc(LINE)+' collection.</p>')}</div></div>
      <div><h3>Specifications</h3><p>${esc([p.width,p.repeat&&('repeat '+p.repeat),p.material].filter(Boolean).join(' · ')||'Digitally printed, made to order.')}</p></div>
      <div><h3>Ordering (internal)</h3><p>Order direct from ${esc(VM.name||LINE)}${VM.phone?' at '+esc(VM.phone):''}${VM.account_number?', our account # '+esc(VM.account_number):''}. Use the buttons above to log a memo, stock, or price request for purchasing.</p></div>
    </div>
    <section class="pairs" id="pairs" hidden>
      <h3>Pairs well with</h3>
      <div class="pair-grid" id="pairGrid"></div>
    </section>`;

  // 3 internal actions → shared modal (internal; no checkout, no external store)
  ITEM={dw_sku:p.dw_sku, mfr_sku:p.sku, title:(label||p.title||'').trim()};
  document.querySelectorAll('.act3 .cta').forEach(b=>{
    b.onclick=()=>openInquiry(b.dataset.act);
  });

  // JSON-LD Product schema (no price — trade line; canonical → this page)
  const ld={ "@context":"https://schema.org","@type":"Product",name:p.title,sku:p.sku,
    brand:{"@type":"Brand",name:LINE},category:"Wallcoverings",image:imgs,
    description:(p.body_html||'').replace(/<[^>]+>/g,'').slice(0,300) };
  const s=document.createElement('script');s.type='application/ld+json';s.textContent=JSON.stringify(ld);document.head.appendChild(s);
  // canonical → this internal page (Mural Source is not on Shopify, so no external canonical)
  const c=document.createElement('link');c.rel='canonical';c.href=location.origin+location.pathname;document.head.appendChild(c);

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

  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');
  });
})();

// ---- internal request modal (memo / stock / price) ----
// stock/price = ACTIVE requests: previews the exact vendor email, then sends via
// purchasing (george) and returns an internal REQ number. memo = internal log.
let ITEM={};
const ACT={
  memo:{title:'Request a Memo Sample',btn:'Log Memo Request',sub:s=>`Log a memo request for ${s} — purchasing orders the sample.`},
  stock:{title:'Check Stock',btn:'Send Stock Check',sub:s=>`Emails the vendor a stock check for ${s} (our account # only — no client info). You get a REQ number.`},
  price:{title:'Get Price',btn:'Send Price Request',sub:s=>`Emails the vendor for our current cost on ${s} (our account # only — no client info). You get a REQ number.`},
};
const $=id=>document.getElementById(id);
function reqPayload(){
  const type=$('inqType').value;
  return {type, dw_sku:ITEM.dw_sku, mfr_sku:ITEM.mfr_sku, title:ITEM.title,
          qty:$('inqQty').value.trim()||null, note:$('inqNote').value.trim()||null};
}
let prevTimer=null;
async function refreshPreview(){
  const type=$('inqType').value;
  if(type==='memo'){$('inqPrev').hidden=true;return;}
  try{
    const r=await fetch(location.origin+'/api/request/preview',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(reqPayload())});
    const j=await r.json();
    if(!j.ok)return;
    $('inqPrev').hidden=false;
    $('inqPrev').innerHTML = j.no_email
      ? `<div class="pv-warn">✉️ No vendor email on file — this logs an internal REQ; confirm by phone${j.phone?': '+esc(j.phone):''}.</div>`
      : `<div class="pv-h">Email that will be sent</div>
         <div class="pv-meta"><b>To:</b> ${esc(j.to)} &nbsp; <b>From:</b> ${esc(j.from)}</div>
         <div class="pv-meta"><b>Subject:</b> ${esc(j.subject)}</div>
         <div class="pv-body">${j.body}</div>`;
  }catch(e){}
}
function openInquiry(type){
  const a=ACT[type]||ACT.memo;
  $('inqType').value=ACT[type]?type:'memo';
  $('inqTitle').textContent=a.title;
  $('inqSend').textContent=a.btn;
  $('inqSub').textContent=a.sub(ITEM.title||ITEM.mfr_sku||'this item');
  $('inqQtyWrap').hidden = type==='memo';
  $('inqQty').value=''; $('inqNote').value='';
  $('inqPrev').hidden=true;
  const m=$('inqMsg'); m.textContent=''; m.className='msg';
  $('inqBack').classList.add('on');
  refreshPreview();
}
['inqQty','inqNote'].forEach(id=>$(id).addEventListener('input',()=>{clearTimeout(prevTimer);prevTimer=setTimeout(refreshPreview,500);}));
$('inqClose').onclick=()=>$('inqBack').classList.remove('on');
$('inqBack').addEventListener('click',e=>{if(e.target.id==='inqBack')$('inqBack').classList.remove('on');});
$('inqSend').onclick=async()=>{
  const m=$('inqMsg');
  $('inqSend').disabled=true;
  try{
    const r=await fetch(location.origin+'/api/request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(reqPayload())});
    const j=await r.json();
    if(j.ok){m.textContent=j.message;m.className='msg ok';setTimeout(()=>$('inqBack').classList.remove('on'),3200);}
    else{m.textContent=j.error||'Something went wrong.';m.className='msg err';}
  }catch(e){m.textContent='Network error — request not sent.';m.className='msg err';}
  $('inqSend').disabled=false;
};

</script>
</body>
</html>