← 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
M config.jsonM public/app.jsM public/index.htmlA public/photos/README.txtM public/styles.cssM server.js
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 →