[object Object]

← back to Robet Site

robert barber v2: gallery (drop photos in public/photos, /api/photos + placeholders), link-first booking (reliable), AgentAbrams corner credit, migrate email info@DW → robert@agentabrams.com (config + JSON-LD)

a673808417bd2cd5101487142709bc40a69d0eff · 2026-06-30 05:16:22 -0700 · Steve

Files touched

Diff

commit a673808417bd2cd5101487142709bc40a69d0eff
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 30 05:16:22 2026 -0700

    robert barber v2: gallery (drop photos in public/photos, /api/photos + placeholders), link-first booking (reliable), AgentAbrams corner credit, migrate email info@DW → robert@agentabrams.com (config + JSON-LD)
---
 config.json              |  6 +++---
 public/app.js            | 50 ++++++++++++++++++------------------------------
 public/index.html        | 25 +++++++++++++++++-------
 public/photos/README.txt |  3 +++
 public/styles.css        | 23 ++++++++++++++++++++++
 server.js                | 14 ++++++++++++++
 6 files changed, 80 insertions(+), 41 deletions(-)

diff --git a/config.json b/config.json
index ac5bb30..4c97d1d 100644
--- a/config.json
+++ b/config.json
@@ -5,15 +5,15 @@
   "heroSub": "Walk in sharp. Pick a time, pick your cut — in and out, no waiting around.",
   "heroImage": "/hero.svg",
   "contact": {
-    "email": "info@designerwallcoverings.com",
+    "email": "robert@agentabrams.com",
     "phone": "",
     "address": "",
     "hours": "By appointment — pick a chair below"
   },
   "scheduling": {
-    "owner": "info@designerwallcoverings.com",
+    "owner": "robert@agentabrams.com",
     "embedLoader": "https://venturacorridor.com/embed/appointments.js",
-    "bookUrl": "https://venturacorridor.com/book?owner=info@designerwallcoverings.com"
+    "bookUrl": "https://venturacorridor.com/book?owner=robert@agentabrams.com"
   },
   "social": []
 }
diff --git a/public/app.js b/public/app.js
index 60a840e..7f591b3 100644
--- a/public/app.js
+++ b/public/app.js
@@ -51,40 +51,28 @@
     }
   });
 
-  // ── Smart Scheduling embed: iframe the chromeless booking page directly ──
-  //   The /embed/appointments.js loader returns text/html (not JS), so injecting it as a
-  //   <script> throws an uncaught SyntaxError (caught cross-browser, loudest in WebKit) and never
-  //   renders. /book?embed=1 returns the chromeless widget with no X-Frame-Options, so it frames
-  //   cleanly; CSP already allows frame-src venturacorridor.com. Skeleton → iframe OR fallback link.
+  // ── Smart Scheduling: link-first (reliable) — the live /book page does the real booking ──
   const sched = cfg.scheduling || {};
-  const embedBox = $('#embedBox');
-  const bookAlt = $('#bookAlt');
-  if (bookAlt && sched.bookUrl) bookAlt.href = sched.bookUrl;   // permanent escape hatch
-  const fallbackHTML = () =>
-    `<div class="fallback mono">Booking opens in a new tab — <a class="cta" href="${sched.bookUrl || '#'}" target="_blank" rel="noopener noreferrer">Open booking page →</a></div>`;
-  if (embedBox) {
-    if (!sched.bookUrl) {
-      embedBox.innerHTML = fallbackHTML();
+  const bookPrimary = $('#bookPrimary');
+  if (bookPrimary && sched.bookUrl) bookPrimary.href = sched.bookUrl;
+
+  // ── gallery: render real photos from /api/photos, else tasteful placeholders ──
+  (async () => {
+    const grid = $('#galleryGrid'); if (!grid) return;
+    let photos = [];
+    try { photos = await (await fetch('/api/photos')).json(); } catch (_) {}
+    if (photos.length) {
+      grid.innerHTML = photos.map((src) =>
+        `<a class="shot" href="${src}" target="_blank" rel="noopener noreferrer">
+           <img src="${src}" alt="A cut by Robert" loading="lazy" width="600" height="600" />
+         </a>`).join('');
     } else {
-      embedBox.innerHTML = '<div class="embed-skeleton" aria-hidden="true"><span></span><span></span><span></span><span></span></div>';
-      let settled = false;
-      const settle = (mode) => {
-        if (settled) return; settled = true; clearTimeout(timer);
-        const sk = embedBox.querySelector('.embed-skeleton'); if (sk) sk.remove();
-        if (mode === 'fallback') embedBox.innerHTML = fallbackHTML();
-      };
-      const sep = sched.bookUrl.includes('?') ? '&' : '?';
-      const frame = document.createElement('iframe');
-      frame.src = sched.bookUrl + sep + 'embed=1';
-      frame.title = 'Book a meeting — Smart Scheduling';
-      frame.loading = 'lazy';
-      frame.style.cssText = 'width:100%;min-height:560px;border:0;display:block;background:#fff';
-      frame.addEventListener('load', () => settle('embed'));      // booking page rendered
-      frame.addEventListener('error', () => settle('fallback'));
-      const timer = setTimeout(() => settle('fallback'), 8000);    // never loaded → fallback link
-      embedBox.appendChild(frame);
+      // pending state — looks intentional, not broken, until real photos are dropped in public/photos
+      grid.innerHTML = Array.from({ length: 6 }, (_, i) =>
+        `<div class="shot shot--pending"><span class="mono">PHOTO ${i + 1}</span></div>`).join('') +
+        `<p class="mono gallery-note">Robert's cuts go here — drop photos in <code>public/photos/</code>.</p>`;
     }
-  }
+  })();
 
   // ── contact form ──
   $('#contactForm').addEventListener('submit', async (e) => {
diff --git a/public/index.html b/public/index.html
index 5b1eece..38647f9 100644
--- a/public/index.html
+++ b/public/index.html
@@ -36,7 +36,7 @@
     "name": "Robert",
     "url": "https://robert.agentabrams.com/",
     "image": "https://robert.agentabrams.com/og.png",
-    "email": "info@designerwallcoverings.com",
+    "email": "robert@agentabrams.com",
     "description": "Robert — barber. Cuts, fades, beard line-ups and hot-towel shaves. Book a chair online.",
     "priceRange": "$$",
     "potentialAction": {
@@ -44,7 +44,7 @@
       "name": "Book a cut",
       "target": {
         "@type": "EntryPoint",
-        "urlTemplate": "https://venturacorridor.com/book?owner=info@designerwallcoverings.com",
+        "urlTemplate": "https://venturacorridor.com/book?owner=robert@agentabrams.com",
         "actionPlatform": [
           "https://schema.org/DesktopWebPlatform",
           "https://schema.org/MobileWebPlatform"
@@ -71,8 +71,9 @@
     <button class="close" id="navclose" aria-label="Close menu">×</button>
     <nav>
       <a href="#top">Home</a>
-      <a href="#work">Work</a>
+      <a href="#work">Services</a>
       <a href="#booking">Book</a>
+      <a href="#gallery">Gallery</a>
       <a href="#contact">Contact</a>
       <a href="/admin">Admin</a>
     </nav>
@@ -102,14 +103,22 @@ CUT.</h1>
       </div>
     </section>
 
-    <!-- booking: Smart Scheduling -->
+    <!-- booking: Smart Scheduling (link-first → real live availability) -->
     <section id="booking">
       <span class="kicker">/ book a chair</span>
       <h2 class="sec-h">Pick a time.</h2>
-      <div class="embed-box" id="embedBox"></div>
-      <p class="mono" style="margin-top:14px">
-        Trouble with the scheduler? <a id="bookAlt" href="#" target="_blank" rel="noopener noreferrer">Open booking in a new tab →</a>
+      <p class="mono" style="font-size:18px; max-width:560px; margin-bottom:24px; font-weight:600;">
+        Live availability — pick a slot and it lands straight in the chair. Opens the booking page in a new tab.
       </p>
+      <a class="cta" id="bookPrimary" href="#" target="_blank" rel="noopener noreferrer"
+         style="font-size:28px; padding:20px 40px;">Book your cut →</a>
+    </section>
+
+    <!-- gallery: real photos drop into public/photos -->
+    <section id="gallery">
+      <span class="kicker">/ the cuts</span>
+      <h2 class="sec-h">The work.</h2>
+      <div class="gallery-grid" id="galleryGrid"></div>
     </section>
 
     <!-- contact -->
@@ -139,6 +148,8 @@ CUT.</h1>
     <span class="mono">Booking by Smart Scheduling · <a href="/admin">Admin</a></span>
   </footer>
 
+  <a class="credit" href="https://agentabrams.com" target="_blank" rel="noopener noreferrer">Designed by <b>AgentAbrams</b></a>
+
   <script src="/app.js"></script>
 </body>
 </html>
diff --git a/public/photos/README.txt b/public/photos/README.txt
new file mode 100644
index 0000000..607623f
--- /dev/null
+++ b/public/photos/README.txt
@@ -0,0 +1,3 @@
+Drop Robert's real barber photos here (.jpg/.jpeg/.png/.webp).
+They auto-appear in the site's Gallery section (newest first), lazy-loaded.
+Recommended: landscape or square, ≥1200px wide. No upscaling — use real-res originals.
diff --git a/public/styles.css b/public/styles.css
index 5950eb0..f5064df 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -161,6 +161,29 @@ section { padding: 70px 24px; border-bottom: var(--bord); }
 .formmsg { margin-top: 12px; font-family: "SF Mono", monospace; }
 .hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
 
+/* gallery */
+.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0; }
+.shot { display: block; border: var(--bord); margin: -2.5px 0 0 -2.5px; aspect-ratio: 1 / 1; overflow: hidden; background: var(--ink); position: relative; }
+.shot img { width: 100%; height: 100%; object-fit: cover; display: block; filter: grayscale(.15) contrast(1.05); transition: transform .2s, filter .2s; }
+.shot:hover img { transform: scale(1.04); filter: grayscale(0) contrast(1.1); }
+.shot--pending { display: grid; place-content: center; background:
+  repeating-linear-gradient(135deg, #1c1b16 0 18px, #232219 18px 36px); color: var(--accent); }
+.shot--pending span { font-size: 13px; letter-spacing: 2px; opacity: .55; text-transform: uppercase; }
+.gallery-note { grid-column: 1 / -1; margin-top: 18px; font-weight: 600; }
+.gallery-note code { background: var(--ink); color: var(--accent); padding: 2px 8px; }
+
+/* "Designed by AgentAbrams" corner credit */
+.credit {
+  position: fixed; right: 12px; bottom: 12px; z-index: 40;
+  background: var(--ink); color: var(--paper); border: 3px solid var(--accent);
+  font-family: "SF Mono", monospace; font-size: 11px; text-transform: uppercase; letter-spacing: 1px;
+  padding: 7px 11px; text-decoration: none;
+}
+.credit b { color: var(--accent); }
+.credit:hover { background: var(--accent); color: var(--ink); }
+.credit:hover b { color: var(--ink); }
+@media print { .credit { display: none !important; } }
+
 /* footer */
 footer { padding: 40px 24px; background: var(--ink); color: var(--paper); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
 footer a { color: var(--accent); }
diff --git a/server.js b/server.js
index 0b859b7..12ec189 100644
--- a/server.js
+++ b/server.js
@@ -79,6 +79,20 @@ function adminAuth(req, res, next) {
 // ── public config (secrets stripped) ──
 app.get('/api/config', (_req, res) => res.json(readJson(CONFIG_FILE, {})));
 
+// ── gallery: list real photos dropped into public/photos (newest first) ──
+app.get('/api/photos', (_req, res) => {
+  const dir = path.join(__dirname, 'public', 'photos');
+  let files = [];
+  try {
+    files = fs.readdirSync(dir)
+      .filter((f) => /\.(jpe?g|png|webp|gif)$/i.test(f))
+      .map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs }))
+      .sort((a, b) => b.t - a.t)
+      .map((x) => '/photos/' + x.f);
+  } catch (_) {}
+  res.json(files);
+});
+
 // ── contact form (hardened: honeypot + validation + per-IP rate limit) ──
 const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 const rateHits = new Map(); // ip -> [timestamps]

← 9b96ab4 ledger: barber re-theme noted  ·  back to Robet Site  ·  auto-save: 2026-06-30T05:28:32 (5 files) — config.json publi 0d21f3b →