← back to Prestige Car Wash

public/contact.html

122 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Book / Contact — Prestige Car Wash</title>
<meta name="description" content="Book a wash or detail with Prestige Car Wash in the San Fernando Valley. No-pressure flat pricing.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/pcw.css">
<style>
  form{max-width:560px}
  .fld{margin-bottom:14px}
  .fld label{display:block;font-size:13px;color:var(--mut);font-weight:600;margin-bottom:6px}
  .fld input,.fld select,.fld textarea{width:100%;background:var(--panel);border:1px solid var(--line);color:var(--ink);border-radius:10px;padding:11px 12px;font-size:15px;font-family:inherit}
  .fld textarea{min-height:90px;resize:vertical}
  .two{display:grid;grid-template-columns:1fr 1fr;gap:12px}
  @media(max-width:520px){.two{grid-template-columns:1fr}}
  .ok{background:rgba(58,210,159,.12);border:1px solid var(--good);color:var(--good);padding:12px;border-radius:10px;margin-bottom:14px;display:none}
  /* Honeypot: off-screen, non-tabbable, hidden from AT — only bots fill it. */
  .hp{position:absolute!important;left:-9999px!important;top:auto;width:1px;height:1px;opacity:0;overflow:hidden}
</style>
</head>
<body>
<nav class="nav"><div class="wrap row">
  <div class="brand"><span class="mk">🚗</span><div>Prestige<br><small>Car Wash · SFV</small></div></div>
  <button class="hamb" aria-label="Menu" aria-expanded="false" onclick="var o=document.getElementById('lnk').classList.toggle('open');this.setAttribute('aria-expanded',o)">☰</button>
  <div class="links" id="lnk">
    <a href="/">Home</a><a href="/services">Services</a><a href="/contact" class="on">Book / Contact</a>
  </div>
</div></nav>

<section class="section"><div class="wrap">
  <h2>Book your wash</h2>
  <p class="sub">Tell us what you need — we'll follow up to confirm a time, usually within a few hours. No upsell pressure, ever.</p>
  <div id="waitchip" style="display:none;margin:0 0 18px;padding:8px 15px;border:1px solid var(--line);border-radius:999px;font-size:14px;width:fit-content" title=""></div>
  <div class="ok" id="ok"></div>
  <form id="f">
    <div class="hp" aria-hidden="true"><label>Leave this field empty<input type="text" name="b_confirm" tabindex="-1" autocomplete="off"></label></div>
    <div class="two">
      <div class="fld"><label>Name *</label><input name="name" required></div>
      <div class="fld"><label>Phone *</label><input name="phone" type="tel"></div>
    </div>
    <div class="two">
      <div class="fld"><label>Email</label><input name="email" type="email"></div>
      <div class="fld"><label>Vehicle (year / make / model)</label><input name="vehicle" placeholder="2021 Tesla Model 3"></div>
    </div>
    <div class="two">
      <div class="fld"><label>Service</label><select name="service" id="svcSel"><option value="">— choose —</option></select></div>
      <div class="fld"><label>Preferred date / time</label><input name="preferred" placeholder="Sat morning"></div>
    </div>
    <div class="fld"><label>Anything else?</label><textarea name="message" placeholder="Pet hair in back seat, fragrance-free please…"></textarea></div>
    <button class="btn" type="submit">Request booking</button>
  </form>
</div></section>

<footer class="foot"><div class="wrap">© 2026 Prestige Car Wash · San Fernando Valley, CA · Instagram @prestigecwash</div></footer>

<script>
// Membership interest: if arriving from the "Join the membership list" CTA, prefill
// the form so the lead is tagged. No billing here — live membership billing awaits Stripe.
if(new URLSearchParams(location.search).get('intent')==='membership'){
  const m=document.querySelector('[name=message]'); if(m) m.value="I'm interested in the unlimited monthly membership — please add me to the list.";
  const h=document.querySelector('h2'); if(h) h.textContent='Join the membership list';
}
// Honest "typical wait" chip — estimate from the demand model, never a live queue count.
fetch('/api/wait').then(r=>r.json()).then(w=>{
  const el=document.getElementById('waitchip');
  el.innerHTML=`⏱️ ${w.day} is typically <b>${w.level}</b> — ${w.phrase} <span style="opacity:.65">· book ahead to skip it</span>`;
  el.title=w.basis||'';el.style.display='';
}).catch(()=>{});
// Deep-link pre-select: /contact?service=<name> from a service card lands here with the
// service already chosen (one fewer step). We only select if it matches a REAL service
// name from the list — the param is never written into the DOM, so no injection risk.
const _p=new URLSearchParams(location.search);
// The membership flow owns the whole form (its own h2 + message), so don't ALSO cross-wire
// a service pre-select into it — that would leave a contradictory "Join membership" heading
// with a service chosen. Service deep-link only applies outside the membership flow.
const qsService=_p.get('intent')==='membership'?null:_p.get('service');
fetch('/api/services?sort=featured').then(r=>r.json()).then(list=>{
  const sel=document.getElementById('svcSel');
  // Escape option name/price so a service named with a quote/angle-bracket can't corrupt the
  // option markup (which would silently break value-matching and POST the wrong service).
  const e=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  sel.innerHTML += list.map(s=>`<option value="${e(s.name)}">${e(s.name)} — ${e(s.price_display||('$'+s.price))}</option>`).join('');
  if(qsService){
    const match=list.find(s=>s.name===qsService);   // exact match against the real list
    if(match){
      sel.value=match.name;   // browser decodes the escaped attr, so this still matches
      const h=document.querySelector('h2'); if(h) h.textContent=`Book: ${match.name}`;
    }
  }
});
// analytics-lite beacon (aggregate only, no PII) — booking-funnel start/submit events.
function track(ev){try{var b=JSON.stringify({event:ev});if(navigator.sendBeacon){navigator.sendBeacon('/api/track',new Blob([b],{type:'application/json'}));}else{fetch('/api/track',{method:'POST',headers:{'Content-Type':'application/json'},body:b,keepalive:true});}}catch(e){}}
(function(){var f=document.getElementById('f'),fired=false;if(f)f.addEventListener('input',function(){if(!fired){fired=true;track('form_start');}});})();
let _submitting=false;
document.getElementById('f').addEventListener('submit',async e=>{
  e.preventDefault();
  if(_submitting) return;                       // duplicate-submit guard (double-click / Enter-mash)
  const form=e.target, ok=document.getElementById('ok');
  const data=Object.fromEntries(new FormData(form).entries());
  const showErr=m=>{ok.textContent=m;ok.style.borderColor='var(--bad)';ok.style.color='var(--bad)';ok.style.background='rgba(255,107,107,.10)';ok.style.display='block';};
  // Client-side validation (the server re-validates — this is just a fast, friendly first pass).
  if(!data.name||data.name.trim().length<2){showErr('Please enter your name.');return;}
  const phoneOk=data.phone&&data.phone.replace(/\D/g,'').length>=7;
  const emailOk=data.email&&/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email);
  if(!phoneOk&&!emailOk){showErr('Please leave a phone number or email so we can reach you.');return;}
  const btn=form.querySelector('button[type=submit]');
  _submitting=true; if(btn){btn.disabled=true;btn.dataset.t=btn.textContent;btn.textContent='Sending…';}
  try{
    const r=await fetch('/api/contact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
    const j=await r.json();
    if(j.ok){track('form_submit');ok.textContent=j.message;ok.style.borderColor='var(--good)';ok.style.color='var(--good)';ok.style.background='rgba(58,210,159,.12)';ok.style.display='block';form.reset();window.scrollTo({top:0,behavior:window.matchMedia&&window.matchMedia('(prefers-reduced-motion:reduce)').matches?'auto':'smooth'});}
    else{showErr(j.error||'Something went wrong — please try again.');}
  }catch{ showErr('Could not reach the server — please check your connection and try again.'); }
  finally{ _submitting=false; if(btn){btn.disabled=false;btn.textContent=btn.dataset.t||'Request booking';} }
});
</script>
</body>
</html>