[object Object]

← back to AsSeenInMovies

asseeninmovies: /spotted HTML gallery — verified placements paginated, category chip-bar filter, shop-button CTA per row, submit banner up top

6ed5341142b58dcc2d140c69acbaa19dda1545f8 · 2026-05-12 16:12:25 -0700 · SteveStudio2

Files touched

Diff

commit 6ed5341142b58dcc2d140c69acbaa19dda1545f8
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 16:12:25 2026 -0700

    asseeninmovies: /spotted HTML gallery — verified placements paginated, category chip-bar filter, shop-button CTA per row, submit banner up top
---
 server.js | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 100 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 2227cb8..735f3ef 100644
--- a/server.js
+++ b/server.js
@@ -229,6 +229,105 @@ function spottedRateLimit(ip) {
   return true;
 }
 
+// /spotted — public HTML gallery of verified placements (companion to the
+// /spotted/submit form and /api/spotted JSON feed). Filterable by category +
+// pageable.
+app.get('/spotted', async (req, res) => {
+  try {
+    const page  = Math.max(1, parseInt(req.query.page || '1', 10));
+    const limit = 30;
+    const offset = (page - 1) * limit;
+    const cat = typeof req.query.category === 'string' && req.query.category.length < 60 ? req.query.category : null;
+    const params = [];
+    let where = `s.verified_at IS NOT NULL`;
+    if (cat) { params.push(cat); where += ` AND s.product_category = $${params.length}`; }
+    params.push(limit, offset);
+    const r = await pool.query(`
+      SELECT s.id, s.product_name, s.product_category, s.brand, s.scene_description,
+             s.scene_timecode, s.shop_url, s.image_url, s.image_source,
+             m.id AS movie_id, m.title AS movie_title, m.release_year, m.poster_url, m.poster_source
+        FROM asim_spotted s JOIN asim_movies m ON m.id=s.movie_id
+       WHERE ${where}
+       ORDER BY s.verified_at DESC, s.id DESC
+       LIMIT $${params.length-1} OFFSET $${params.length}`, params);
+    const totalR = await pool.query(`SELECT count(*) AS n FROM asim_spotted s WHERE ${where}`, params.slice(0, params.length - 2));
+    const total = parseInt(totalR.rows[0].n, 10);
+    const totalPages = Math.max(1, Math.ceil(total / limit));
+    const catsR = await pool.query(`SELECT product_category, count(*) AS n FROM asim_spotted WHERE verified_at IS NOT NULL AND product_category IS NOT NULL GROUP BY 1 ORDER BY 2 DESC`);
+    const qs = (overrides = {}) => {
+      const u = new URLSearchParams();
+      if (cat) u.set('category', cat);
+      Object.entries(overrides).forEach(([k,v]) => v == null ? u.delete(k) : u.set(k, v));
+      return u.toString();
+    };
+    const filterBar = `
+      <nav class="cat-bar">
+        <a href="/spotted" class="${!cat ? 'active' : ''}">All <span class="muted">${total}</span></a>
+        ${catsR.rows.map(c => `<a href="/spotted?${qs({category: c.product_category, page: null})}" class="${cat===c.product_category?'active':''}">${esc(c.product_category)} <span class="muted">${c.n}</span></a>`).join('')}
+      </nav>`;
+    const rows = r.rows.map(s => `
+      <article class="spot-row">
+        <a class="spot-poster" href="/movie/${s.movie_id}" aria-label="${esc(s.movie_title)}">
+          <img src="${esc(s.poster_url && (s.poster_source||'').toLowerCase()!=='made-with-ai' ? s.poster_url : '/img/made-with-ai.svg')}" alt="" loading="lazy">
+        </a>
+        <div class="spot-body">
+          <div class="spot-meta">
+            <a href="/movie/${s.movie_id}">${esc(s.movie_title)}${s.release_year ? ' <span class="muted">('+s.release_year+')</span>' : ''}</a>
+            ${s.scene_timecode ? '<span class="muted"> · '+esc(s.scene_timecode)+'</span>' : ''}
+          </div>
+          <h3 class="spot-title">${esc(s.product_name)}${s.brand ? ' <span class="muted">— '+esc(s.brand)+'</span>' : ''}</h3>
+          ${s.product_category ? '<span class="chip">'+esc(s.product_category)+'</span>' : ''}
+          ${s.scene_description ? '<p class="spot-desc">'+esc(s.scene_description)+'</p>' : ''}
+          ${s.shop_url ? '<a class="shop-btn" href="'+esc(s.shop_url)+'" target="_blank" rel="nofollow noopener">Where to find it ↗</a>' : ''}
+        </div>
+      </article>`).join('');
+    const pager = `
+      <nav class="pager" aria-label="Pagination">
+        ${page > 1 ? `<a class="chip" href="/spotted?${qs({page: page-1})}">← Prev</a>` : ''}
+        <span class="chip" style="background:var(--gold);color:#1a1410">Page ${page} of ${totalPages}</span>
+        ${page < totalPages ? `<a class="chip" href="/spotted?${qs({page: page+1})}">Next →</a>` : ''}
+      </nav>`;
+    const body = `
+      <style>
+        .cat-bar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid var(--line)}
+        .cat-bar a{padding:6px 14px;border:1px solid var(--line);border-radius:999px;font-size:11.5px;letter-spacing:0.08em;text-transform:uppercase;color:var(--ink-soft);text-decoration:none}
+        .cat-bar a.active{background:var(--gold);color:#1a1410;border-color:var(--gold);font-weight:600}
+        .cat-bar a:hover{border-color:var(--gold);color:var(--gold)}
+        .cat-bar a.active:hover{color:#1a1410}
+        .cat-bar .muted{color:inherit;opacity:0.55;margin-left:4px}
+        .spot-row{display:grid;grid-template-columns:120px 1fr;gap:18px;padding:18px 0;border-bottom:1px dotted var(--line)}
+        @media (max-width:560px){.spot-row{grid-template-columns:90px 1fr;gap:14px}}
+        .spot-poster{display:block;aspect-ratio:2/3;background:#15130f;border-radius:6px;overflow:hidden;border:1px solid var(--line)}
+        .spot-poster img{width:100%;height:100%;object-fit:cover;display:block}
+        .spot-meta{font-size:13px;color:var(--ink-faint);margin-bottom:4px}
+        .spot-meta a{color:var(--ink);text-decoration:none}
+        .spot-meta a:hover{color:var(--gold)}
+        .spot-title{margin:2px 0 8px;font-family:var(--serif);font-weight:400;font-size:20px}
+        .spot-desc{margin:8px 0 10px;color:var(--ink-soft);font-size:13.5px;line-height:1.55}
+        .shop-btn{display:inline-block;padding:5px 12px;border:1px solid var(--gold);color:var(--gold);font-size:11.5px;letter-spacing:0.1em;text-transform:uppercase;border-radius:4px;text-decoration:none}
+        .shop-btn:hover{background:var(--gold);color:#1a1410}
+        .submit-banner{margin:24px 0;padding:14px 18px;border:1px solid var(--gold);border-radius:8px;background:rgba(201,161,75,0.05);display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap}
+        .submit-banner a{padding:7px 18px;background:var(--gold);color:#1a1410;border-radius:5px;font-size:11.5px;letter-spacing:0.12em;text-transform:uppercase;text-decoration:none;font-weight:600}
+        .submit-banner a:hover{filter:brightness(1.1)}
+        .pager{display:flex;gap:10px;align-items:center;justify-content:center;margin:32px 0}
+      </style>
+      <h1 class="title">SPOTTED</h1>
+      <p class="meta-line">${total.toLocaleString()} verified product placements — set decor, wallcoverings, lighting, props.</p>
+
+      <div class="submit-banner">
+        <span>Saw something on screen that we don't have yet?</span>
+        <a href="/spotted/submit">Submit a placement →</a>
+      </div>
+
+      ${filterBar}
+      ${rows || '<p class="empty">No verified placements in this category yet.</p>'}
+      ${pager}`;
+    res.type('html').send(htmlShell(cat ? 'Spotted · ' + cat : 'Spotted', body));
+  } catch (e) {
+    res.status(500).type('html').send(htmlShell('Error', `<div class="empty">${esc(e.message)}</div>`));
+  }
+});
+
 app.get('/spotted/submit', async (req, res) => {
   const movieId = parseInt(req.query.movie_id || '0', 10);
   let movieTitle = '', movieYear = '';
@@ -556,7 +655,7 @@ function htmlShell(title, body) {
   <nav>
     <a href="/movies">Movies</a>
     <a href="/people">People</a>
-    <a href="/api/spotted">Spotted</a>
+    <a href="/spotted">Spotted</a>
     <a href="/spotted/submit">Submit</a>
     <a href="/search">Search</a>
   </nav>

← 1cdcaf5 asseeninmovies: SPOTTED submission form — public /spotted/su  ·  back to AsSeenInMovies  ·  asseeninmovies: cross-link /movie/:id ↔ /spotted/submit?movi c22b6be →