[object Object]

← back to Stars of Design

Hydrate saved sort BEFORE first grid render

18acbe8cf71cfdfd634509973ead48ef1cd84671 · 2026-05-19 15:17:57 -0700 · Steve Abrams

Per the standing rule: when a user has picked a sort in /designers or on
the home grid, that preference must apply on the very first render of a
fresh visit, not only after they touch the dropdown again. Server-side
sorting is driven by ?sort= in the URL, so this commit makes
public/js/grid-controls.js do a one-time replace() redirect to the saved
sort value when the URL has no sort param yet.

Guards against:
- cross-page contamination — only redirects if the saved value is one of
  the options actually present in this page's <select> (so a "default"
  saved from /clients won't crash /designers).
- redirect loops — gated by a sessionStorage flag.
- form-driven pages — /clients and /firms wrap their <select> in a <form
  onchange="this.form.submit()">; the JS now defers to the form submit
  instead of double-navigating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 18acbe8cf71cfdfd634509973ead48ef1cd84671
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 15:17:57 2026 -0700

    Hydrate saved sort BEFORE first grid render
    
    Per the standing rule: when a user has picked a sort in /designers or on
    the home grid, that preference must apply on the very first render of a
    fresh visit, not only after they touch the dropdown again. Server-side
    sorting is driven by ?sort= in the URL, so this commit makes
    public/js/grid-controls.js do a one-time replace() redirect to the saved
    sort value when the URL has no sort param yet.
    
    Guards against:
    - cross-page contamination — only redirects if the saved value is one of
      the options actually present in this page's <select> (so a "default"
      saved from /clients won't crash /designers).
    - redirect loops — gated by a sessionStorage flag.
    - form-driven pages — /clients and /firms wrap their <select> in a <form
      onchange="this.form.submit()">; the JS now defers to the form submit
      instead of double-navigating.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/js/grid-controls.js | 54 ++++++++++++++++++++++++++++++++++++----------
 1 file changed, 43 insertions(+), 11 deletions(-)

diff --git a/public/js/grid-controls.js b/public/js/grid-controls.js
index fc8de1a..fc0546e 100644
--- a/public/js/grid-controls.js
+++ b/public/js/grid-controls.js
@@ -1,5 +1,11 @@
 // Density slider + sort select. Sort hits the server via querystring (matches lib/sort.js).
 // Density adjusts CSS var --cols-min on the grid; both values persist in localStorage.
+//
+// Hydration order matters — per the standing rule, the saved sort MUST apply
+// BEFORE the first grid render. Server-side sort happens via the `?sort=` URL
+// param, so on page load we check: if URL has no sort param but a saved value
+// exists in localStorage, redirect once with the saved value as `?sort=`.
+// This makes the saved sort effective on the very first paint of a fresh visit.
 (function () {
   var KEY_SORT = 'sod-sort';
   var KEY_DENS = 'sod-density';
@@ -8,10 +14,45 @@
   var sortEl = document.querySelector('[data-sort]');
   var densEl = document.querySelector('[data-density]');
 
+  // --- Sort: hydrate saved value BEFORE first render via one-time redirect ---
+  if (sortEl) {
+    try {
+      var savedSort = localStorage.getItem(KEY_SORT);
+      var url = new URL(window.location.href);
+      var urlSort = url.searchParams.get('sort');
+      // Only redirect if (a) we have a saved sort, (b) URL has no sort yet,
+      // (c) saved sort is actually one of the options on this page (don't
+      // cross-contaminate between /designers, /clients, /firms which each
+      // have different sort vocabularies). Also gate against loops with a
+      // sessionStorage flag.
+      if (savedSort && !urlSort && sortEl.querySelector('option[value="' + savedSort.replace(/"/g, '') + '"]')) {
+        var hydratedFlag = 'sod-sort-hydrated';
+        if (!sessionStorage.getItem(hydratedFlag)) {
+          sessionStorage.setItem(hydratedFlag, '1');
+          url.searchParams.set('sort', savedSort);
+          window.location.replace(url.toString());
+          return; // stop further setup; page will reload with the saved sort
+        }
+      }
+    } catch (e) {}
+
+    sortEl.addEventListener('change', function () {
+      try { localStorage.setItem(KEY_SORT, sortEl.value); } catch (e) {}
+      // sort selects inside a <form> already submit themselves (see clients.ejs
+      // / firms.ejs onchange="this.form.submit()"); for the standalone partial
+      // (home, /designers) we drive navigation here.
+      if (sortEl.form) return; // form submit handles it
+      var u = new URL(window.location.href);
+      u.searchParams.set('sort', sortEl.value);
+      window.location.href = u.toString();
+    });
+  }
+
+  // --- Density: pure client-side, persist + apply CSS var ---
   if (densEl && grid) {
     try {
-      var saved = localStorage.getItem(KEY_DENS);
-      if (saved) { densEl.value = saved; }
+      var savedDens = localStorage.getItem(KEY_DENS);
+      if (savedDens) { densEl.value = savedDens; }
     } catch (e) {}
     grid.style.setProperty('--cols-min', densEl.value + 'px');
     densEl.addEventListener('input', function () {
@@ -19,13 +60,4 @@
       try { localStorage.setItem(KEY_DENS, densEl.value); } catch (e) {}
     });
   }
-
-  if (sortEl) {
-    sortEl.addEventListener('change', function () {
-      try { localStorage.setItem(KEY_SORT, sortEl.value); } catch (e) {}
-      var url = new URL(window.location.href);
-      url.searchParams.set('sort', sortEl.value);
-      window.location.href = url.toString();
-    });
-  }
 })();

← e85f238 Makefile: audit target  ·  back to Stars of Design  ·  Add noreferrer to all rel=nofollow noopener external links a7ac2a9 →