[object Object]

← back to AsSeenInMovies

asseeninmovies: /search HTML page — combined movies+people grid, replaces nav link to JSON endpoint

562f3b63f96d354d3ab3a07089539ac164ebd763 · 2026-05-12 15:44:53 -0700 · SteveStudio2

Files touched

Diff

commit 562f3b63f96d354d3ab3a07089539ac164ebd763
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 15:44:53 2026 -0700

    asseeninmovies: /search HTML page — combined movies+people grid, replaces nav link to JSON endpoint
---
 server.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 62 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index ec8461e..3742c10 100644
--- a/server.js
+++ b/server.js
@@ -62,6 +62,67 @@ app.get('/api/health', async (_req, res) => {
 });
 
 // Search across movies + people (basic ILIKE for tick 0 — Postgres FTS in a later tick)
+// /search — HTML search page; renders combined movies + people grid from
+// /api/search. Replaces the old nav link that pointed at the JSON endpoint.
+app.get('/search', async (req, res) => {
+  const q = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q.trim() : '';
+  let movies = [], people = [];
+  if (q.length >= 2) {
+    const like = '%' + q + '%';
+    try {
+      const [m, p] = await Promise.all([
+        pool.query(`SELECT id, title, release_year, poster_url FROM asim_movies WHERE title ILIKE $1 ORDER BY (poster_url IS NOT NULL) DESC, release_year DESC NULLS LAST LIMIT 24`, [like]),
+        pool.query(`SELECT id, full_name, headshot_url, primary_profession FROM asim_people WHERE full_name ILIKE $1 ORDER BY (headshot_url IS NOT NULL) DESC, full_name LIMIT 24`, [like]),
+      ]);
+      movies = m.rows; people = p.rows;
+    } catch (e) { /* fall through to empty */ }
+  }
+  const movieCards = movies.map(m => `
+    <a class="grid-card" href="/movie/${m.id}">
+      <div class="grid-poster" style="background-image:url('${esc(m.poster_url || '/img/made-with-ai.svg')}')"></div>
+      <div class="grid-body">
+        <div class="grid-title">${esc(m.title)}</div>
+        <div class="grid-meta">${m.release_year || '—'}</div>
+      </div>
+    </a>`).join('');
+  const peopleCards = people.map(p => `
+    <a class="grid-card" href="/person/${p.id}">
+      <div class="grid-headshot" style="background-image:url('${esc(p.headshot_url || '/img/made-with-ai.svg')}')"></div>
+      <div class="grid-body">
+        <div class="grid-title">${esc(p.full_name)}</div>
+        <div class="grid-meta">${(p.primary_profession || []).slice(0,2).join(' · ')}</div>
+      </div>
+    </a>`).join('');
+  const body = `
+    <style>
+      .search-bar{display:flex;gap:10px;align-items:center;margin-bottom:32px}
+      .search-bar input{flex:1;background:transparent;border:1px solid var(--line);color:inherit;padding:10px 14px;border-radius:6px;font:inherit;font-size:16px}
+      .search-bar button{padding:10px 18px;background:var(--gold);color:#1a1410;border:0;border-radius:6px;font:inherit;letter-spacing:0.06em;cursor:pointer}
+      .results-head{font-size:13px;letter-spacing:0.16em;text-transform:uppercase;color:var(--ink-faint);margin:32px 0 14px;padding-bottom:6px;border-bottom:1px solid var(--line)}
+      .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:18px}
+      .grid-card{display:block;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card);transition:transform .15s;text-decoration:none;color:inherit}
+      .grid-card:hover{transform:translateY(-2px);border-color:var(--gold)}
+      .grid-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover}
+      .grid-headshot{aspect-ratio:3/4;background:#15130f no-repeat center/cover}
+      .grid-body{padding:10px 12px}
+      .grid-title{font-size:13px;line-height:1.3;margin-bottom:4px;color:var(--ink);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
+      .grid-meta{color:var(--ink-faint);font-size:11px;letter-spacing:0.04em}
+    </style>
+    <h1 class="title">Search</h1>
+    <form class="search-bar" method="get" action="/search">
+      <input name="q" type="search" placeholder="Title, actor, director…" value="${esc(q)}" autofocus>
+      <button type="submit">Search</button>
+    </form>
+    ${q.length >= 2 ? `
+      <div class="results-head">Movies (${movies.length})</div>
+      <div class="grid">${movieCards || '<p class="empty">No movies match.</p>'}</div>
+      <div class="results-head">People (${people.length})</div>
+      <div class="grid">${peopleCards || '<p class="empty">No people match.</p>'}</div>
+    ` : '<p class="empty">Type 2 or more characters to search.</p>'}
+  `;
+  res.type('html').send(htmlShell(q ? `Search: ${q}` : 'Search', body));
+});
+
 app.get('/api/search', async (req, res) => {
   const q = String(req.query.q || '').trim();
   if (!q || q.length < 2) return res.json({ movies: [], people: [] });
@@ -260,7 +321,7 @@ function htmlShell(title, body) {
     <a href="/movies">Movies</a>
     <a href="/people">People</a>
     <a href="/api/spotted">Spotted</a>
-    <a href="/api/search?q=">Search</a>
+    <a href="/search">Search</a>
   </nav>
 </header>
 <main>${body}</main>

← 57e1b20 asseeninmovies: live home page — stats + 2 random-sampled ra  ·  back to AsSeenInMovies  ·  asseeninmovies: SPOTTED submission form — public /spotted/su 1cdcaf5 →