[object Object]

← back to AsSeenInMovies

asseeninmovies: /movies + /people sort dropdowns + density slider (per standing grid rule); localStorage-persisted density

67784fe1d15fdcbcc663946988e433b33ed0eb2b · 2026-05-12 15:36:59 -0700 · SteveStudio2

Files touched

Diff

commit 67784fe1d15fdcbcc663946988e433b33ed0eb2b
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 15:36:59 2026 -0700

    asseeninmovies: /movies + /people sort dropdowns + density slider (per standing grid rule); localStorage-persisted density
---
 server.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 68 insertions(+), 10 deletions(-)

diff --git a/server.js b/server.js
index 0f9ca7e..58fa8dc 100644
--- a/server.js
+++ b/server.js
@@ -411,12 +411,21 @@ app.get('/person/:id', async (req, res) => {
 // release year and genre. Query: ?page= ?year= ?genre= ?q=
 app.get('/movies', async (req, res) => {
   try {
+    const MOVIE_SORTS = {
+      popular:  `(popularity IS NOT NULL) DESC, popularity DESC NULLS LAST, release_year DESC NULLS LAST, id DESC`,
+      newest:   `release_year DESC NULLS LAST, id DESC`,
+      oldest:   `release_year ASC NULLS LAST, id`,
+      title_az: `title ASC, id`,
+      title_za: `title DESC, id`,
+      rating:   `vote_average DESC NULLS LAST, id`,
+    };
     const page  = Math.max(1, parseInt(req.query.page  || '1', 10));
     const limit = 48;
     const offset = (page - 1) * limit;
     const year  = /^\d{4}$/.test(req.query.year || '') ? parseInt(req.query.year, 10) : null;
     const genre = typeof req.query.genre === 'string' && req.query.genre.length < 32 ? req.query.genre : null;
     const q     = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q.trim() : null;
+    const sort  = MOVIE_SORTS[req.query.sort] ? req.query.sort : 'popular';
     const params = [];
     let where = `poster_url IS NOT NULL`;
     if (year)  { params.push(year);  where += ` AND release_year = $${params.length}`; }
@@ -427,7 +436,7 @@ app.get('/movies', async (req, res) => {
       SELECT id, title, release_year, genres, poster_url, poster_source, vote_average
         FROM asim_movies
        WHERE ${where}
-       ORDER BY (popularity IS NOT NULL) DESC, popularity DESC NULLS LAST, release_year DESC NULLS LAST, id DESC
+       ORDER BY ${MOVIE_SORTS[sort]}
        LIMIT $${params.length-1} OFFSET $${params.length}`, params);
     const totalR = await pool.query(`SELECT count(*) AS n FROM asim_movies WHERE ${where}`, params.slice(0, params.length - 2));
     const total = parseInt(totalR.rows[0].n, 10);
@@ -445,6 +454,7 @@ app.get('/movies', async (req, res) => {
       if (year)  u.set('year', year);
       if (genre) u.set('genre', genre);
       if (q)     u.set('q', q);
+      if (sort && sort !== 'popular') u.set('sort', sort);
       Object.entries(overrides).forEach(([k,v]) => v == null ? u.delete(k) : u.set(k, v));
       return u.toString();
     };
@@ -454,18 +464,36 @@ app.get('/movies', async (req, res) => {
         <span class="chip" style="background:var(--gold);color:#1a1410">Page ${page} of ${totalPages.toLocaleString()}</span>
         ${page < totalPages ? `<a class="chip" href="/movies?${qs({page: page+1})}">Next →</a>` : ''}
       </nav>`;
+    const sortOpt = (val, label) => `<option value="${val}"${sort===val?' selected':''}>${label}</option>`;
     const filterBar = `
       <form class="filter-bar" method="get" action="/movies">
         <input name="q"     type="search" placeholder="Search title" value="${esc(q || '')}">
         <input name="year"  type="text"   placeholder="Year (e.g. 1994)" value="${esc(year || '')}" maxlength="4" size="6">
         <input name="genre" type="text"   placeholder="Genre (e.g. Drama)" value="${esc(genre || '')}" size="14">
+        <label class="control"><span style="font-size:11px;letter-spacing:0.08em;color:var(--ink-faint);margin-right:4px">Sort</span>
+          <select name="sort" onchange="this.form.submit()">
+            ${sortOpt('popular',  'Most popular')}
+            ${sortOpt('newest',   'Newest')}
+            ${sortOpt('oldest',   'Oldest')}
+            ${sortOpt('title_az', 'Title A→Z')}
+            ${sortOpt('title_za', 'Title Z→A')}
+            ${sortOpt('rating',   'Highest rated')}
+          </select>
+        </label>
+        <label class="control"><span style="font-size:11px;letter-spacing:0.08em;color:var(--ink-faint);margin-right:4px">Density</span>
+          <input type="range" min="140" max="280" step="20" value="180" data-density="movies">
+        </label>
         <button type="submit" class="chip" style="background:transparent">Filter</button>
       </form>`;
     const body = `
       <style>
-        .filter-bar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px}
-        .filter-bar input{background:transparent;border:1px solid var(--line);color:inherit;padding:7px 12px;border-radius:6px;font:inherit;min-width:140px}
-        .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:20px}
+        :root{--movies-card:180px}
+        .filter-bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:24px}
+        .filter-bar input[type=search],.filter-bar input[type=text]{background:transparent;border:1px solid var(--line);color:inherit;padding:7px 12px;border-radius:6px;font:inherit;min-width:140px}
+        .filter-bar .control{display:inline-flex;align-items:center;gap:6px}
+        .filter-bar select{background:transparent;border:1px solid var(--line);color:inherit;padding:7px 12px;border-radius:6px;font:inherit}
+        .filter-bar input[type=range]{accent-color:var(--gold)}
+        .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--movies-card),1fr));gap:20px}
         .grid-card{display:block;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card);transition:transform .15s}
         .grid-card:hover{transform:translateY(-2px)}
         .grid-poster{aspect-ratio:2/3;background:#15130f no-repeat center/cover}
@@ -478,7 +506,8 @@ app.get('/movies', async (req, res) => {
       <p class="meta-line">${total.toLocaleString()} movies with rights-cleared posters</p>
       ${filterBar}
       <div class="grid">${cards || '<div class="empty">No movies match those filters yet.</div>'}</div>
-      ${pager}`;
+      ${pager}
+      <script>(function(){var KEY='asim_movies_density';var saved=parseInt(localStorage.getItem(KEY)||'180',10);var r=document.querySelector('[data-density="movies"]');if(!r)return;if(saved&&saved>=140&&saved<=280){r.value=saved;document.documentElement.style.setProperty('--movies-card',saved+'px')}r.addEventListener('input',function(){document.documentElement.style.setProperty('--movies-card',r.value+'px');localStorage.setItem(KEY,r.value)})})();</script>`;
     res.type('html').send(htmlShell('Movies', body));
   } catch (e) {
     res.status(500).type('html').send(htmlShell('Error', `<div class="empty">${esc(e.message)}</div>`));
@@ -488,10 +517,19 @@ app.get('/movies', async (req, res) => {
 // /people — paginated grid of people with headshots, popularity-sorted.
 app.get('/people', async (req, res) => {
   try {
+    const PEOPLE_SORTS = {
+      default:    `(claimed_at IS NOT NULL) DESC, full_name`,
+      name_az:    `full_name ASC`,
+      name_za:    `full_name DESC`,
+      youngest:   `birth_year DESC NULLS LAST, full_name`,
+      oldest:     `birth_year ASC NULLS LAST, full_name`,
+      living:     `(death_year IS NULL) DESC, full_name`,
+    };
     const page   = Math.max(1, parseInt(req.query.page || '1', 10));
     const limit  = 60;
     const offset = (page - 1) * limit;
     const q      = typeof req.query.q === 'string' && req.query.q.length < 80 ? req.query.q.trim() : null;
+    const sort   = PEOPLE_SORTS[req.query.sort] ? req.query.sort : 'default';
     const params = [];
     let where = `headshot_url IS NOT NULL`;
     if (q) { params.push(`%${q.toLowerCase()}%`); where += ` AND LOWER(full_name) LIKE $${params.length}`; }
@@ -500,7 +538,7 @@ app.get('/people', async (req, res) => {
       SELECT id, full_name, headshot_url, headshot_source, primary_profession, birth_year, death_year
         FROM asim_people
        WHERE ${where}
-       ORDER BY (claimed_at IS NOT NULL) DESC, full_name
+       ORDER BY ${PEOPLE_SORTS[sort]}
        LIMIT $${params.length-1} OFFSET $${params.length}`, params);
     const totalR = await pool.query(`SELECT count(*) AS n FROM asim_people WHERE ${where}`, params.slice(0, params.length - 2));
     const total = parseInt(totalR.rows[0].n, 10);
@@ -516,6 +554,7 @@ app.get('/people', async (req, res) => {
     const qs = (overrides = {}) => {
       const u = new URLSearchParams();
       if (q) u.set('q', q);
+      if (sort && sort !== 'default') u.set('sort', sort);
       Object.entries(overrides).forEach(([k,v]) => v == null ? u.delete(k) : u.set(k, v));
       return u.toString();
     };
@@ -525,11 +564,16 @@ app.get('/people', async (req, res) => {
         <span class="chip" style="background:var(--gold);color:#1a1410">Page ${page} of ${totalPages.toLocaleString()}</span>
         ${page < totalPages ? `<a class="chip" href="/people?${qs({page: page+1})}">Next →</a>` : ''}
       </nav>`;
+    const sortOpt = (val, label) => `<option value="${val}"${sort===val?' selected':''}>${label}</option>`;
     const body = `
       <style>
-        .filter-bar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px}
-        .filter-bar input{background:transparent;border:1px solid var(--line);color:inherit;padding:7px 12px;border-radius:6px;font:inherit;min-width:200px}
-        .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:18px}
+        :root{--people-card:160px}
+        .filter-bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:24px}
+        .filter-bar input[type=search]{background:transparent;border:1px solid var(--line);color:inherit;padding:7px 12px;border-radius:6px;font:inherit;min-width:200px}
+        .filter-bar .control{display:inline-flex;align-items:center;gap:6px}
+        .filter-bar select{background:transparent;border:1px solid var(--line);color:inherit;padding:7px 12px;border-radius:6px;font:inherit}
+        .filter-bar input[type=range]{accent-color:var(--gold)}
+        .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--people-card),1fr));gap:18px}
         .grid-card{display:block;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--card);transition:transform .15s}
         .grid-card:hover{transform:translateY(-2px)}
         .grid-headshot{aspect-ratio:3/4;background:#15130f no-repeat center/cover}
@@ -542,10 +586,24 @@ app.get('/people', async (req, res) => {
       <p class="meta-line">${total.toLocaleString()} people with public headshots</p>
       <form class="filter-bar" method="get" action="/people">
         <input name="q" type="search" placeholder="Search name" value="${esc(q || '')}">
+        <label class="control"><span style="font-size:11px;letter-spacing:0.08em;color:var(--ink-faint);margin-right:4px">Sort</span>
+          <select name="sort" onchange="this.form.submit()">
+            ${sortOpt('default',  'Claimed first')}
+            ${sortOpt('name_az',  'Name A→Z')}
+            ${sortOpt('name_za',  'Name Z→A')}
+            ${sortOpt('youngest', 'Youngest')}
+            ${sortOpt('oldest',   'Oldest')}
+            ${sortOpt('living',   'Living first')}
+          </select>
+        </label>
+        <label class="control"><span style="font-size:11px;letter-spacing:0.08em;color:var(--ink-faint);margin-right:4px">Density</span>
+          <input type="range" min="120" max="280" step="20" value="160" data-density="people">
+        </label>
         <button type="submit" class="chip" style="background:transparent">Filter</button>
       </form>
       <div class="grid">${cards || '<div class="empty">No people match that filter yet.</div>'}</div>
-      ${pager}`;
+      ${pager}
+      <script>(function(){var KEY='asim_people_density';var saved=parseInt(localStorage.getItem(KEY)||'160',10);var r=document.querySelector('[data-density="people"]');if(!r)return;if(saved&&saved>=120&&saved<=280){r.value=saved;document.documentElement.style.setProperty('--people-card',saved+'px')}r.addEventListener('input',function(){document.documentElement.style.setProperty('--people-card',r.value+'px');localStorage.setItem(KEY,r.value)})})();</script>`;
     res.type('html').send(htmlShell('People', body));
   } catch (e) {
     res.status(500).type('html').send(htmlShell('Error', `<div class="empty">${esc(e.message)}</div>`));

← 96cf4db scaffold wikidata-commons-fallback.js — search Commons for h  ·  back to AsSeenInMovies  ·  wikidata-enrich: add --byCast (filter to role=actor) to targ 8205ad7 →