← 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 →