[object Object]

← back to AsSeenInMovies

asseeninmovies: /person/:id — surface 'SPOTTED in their films' section (person ⇄ movie ⇄ spotted graph; renders only when at least one verified placement matches person's filmography)

4f8629cd18a26e9f350af1a5f01fc281d960e251 · 2026-05-12 16:25:29 -0700 · SteveStudio2

Files touched

Diff

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

    asseeninmovies: /person/:id — surface 'SPOTTED in their films' section (person ⇄ movie ⇄ spotted graph; renders only when at least one verified placement matches person's filmography)
---
 server.js | 30 +++++++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 09e0fa2..9ea2f47 100644
--- a/server.js
+++ b/server.js
@@ -775,6 +775,20 @@ app.get('/person/:id', async (req, res) => {
              m.id AS movie_id, m.title, m.release_year, m.poster_url, m.poster_source
         FROM asim_credits c JOIN asim_movies m ON m.id = c.movie_id
        WHERE c.person_id=$1 ORDER BY m.release_year DESC NULLS LAST`, [id]);
+    // Spotted placements from films this person worked on — surfaces the
+    // person ⇄ movie ⇄ spotted graph. Today this is mostly empty (only 14
+    // verified spotted rows), but the join is structurally correct so it
+    // lights up as submissions accumulate.
+    const personSpotted = await pool.query(`
+      SELECT DISTINCT s.id AS spot_id, s.product_name, s.product_category, s.brand,
+                      s.scene_timecode, s.shop_url,
+                      m.id AS movie_id, m.title, m.release_year
+        FROM asim_spotted s
+        JOIN asim_movies m ON m.id = s.movie_id
+        JOIN asim_credits c ON c.movie_id = m.id
+       WHERE c.person_id = $1 AND s.verified_at IS NOT NULL
+       ORDER BY m.release_year DESC NULLS LAST, s.product_name
+       LIMIT 24`, [id]);
 
     const isPremium = person.claimed_at && (!person.premium_expires_at || new Date(person.premium_expires_at) > new Date());
     const heroHtml = `
@@ -806,7 +820,21 @@ app.get('/person/:id', async (req, res) => {
           </div>`).join('')}
       </section>` : '<div class="empty">No credits loaded yet.</div>';
 
-    res.type('html').send(htmlShell(person.full_name, heroHtml + filmHtml));
+    const personSpottedHtml = personSpotted.rows.length ? `
+      <section>
+        <h2>SPOTTED in their films &middot; ${personSpotted.rows.length}</h2>
+        ${personSpotted.rows.map(s => `
+          <div class="spotted-row">
+            <div class="badge-col">${esc(s.product_category || 'dressing')}</div>
+            <div>
+              <div style="font-weight:500">${esc(s.product_name)}${s.brand ? ' <span style="color:var(--ink-faint);font-weight:400">— '+esc(s.brand)+'</span>' : ''}</div>
+              <div class="desc"><a href="/movie/${esc(s.movie_id)}#spotted">${esc(s.title)}${s.release_year ? ' ('+esc(s.release_year)+')' : ''}</a>${s.scene_timecode ? ' · ' + esc(s.scene_timecode) : ''}</div>
+              ${s.shop_url ? `<a class="shop" href="${esc(s.shop_url)}" target="_blank" rel="nofollow noopener">Where to find it ↗</a>` : ''}
+            </div>
+          </div>`).join('')}
+      </section>` : '';
+
+    res.type('html').send(htmlShell(person.full_name, heroHtml + filmHtml + personSpottedHtml));
   } catch (e) {
     res.status(500).type('html').send(htmlShell('Error', `<div class="empty">${esc(e.message)}</div>`));
   }

← 3a3c2c7 asseeninmovies: home page — third rail 'Recent SPOTTED' (6 v  ·  back to AsSeenInMovies  ·  asseeninmovies: /spotted/queue admin moderation — token-gate 0206a11 →