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