[object Object]

← back to Wallco Ai

wallco.ai · /api/me/saved bulk endpoint + ♥ saved badges in /designs autocomplete · GET /api/me/saved returns {authenticated, count, ids[]} for the trade-user (anon → {false, []}); Cache-Control:no-store (mid-session changes). Dropdown lazily fetches saved-ids once on first input event, caches in window._savedIds Set, and decorates result rows with a gold ♥ when r.id ∈ savedIds. Saves the N round-trips that /api/saved/state would require for 24 suggestions. Verified: anon {false,[]}, smoke@example.com {true,1,[72]}, /designs has 7 wiring refs (ensureSavedIds, _savedIds, /api/me/saved)

0fccfb6e654947cbd030f7396605ced23937adaf · 2026-05-12 09:59:19 -0700 · SteveStudio2

Files touched

Diff

commit 0fccfb6e654947cbd030f7396605ced23937adaf
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Tue May 12 09:59:19 2026 -0700

    wallco.ai · /api/me/saved bulk endpoint + ♥ saved badges in /designs autocomplete · GET /api/me/saved returns {authenticated, count, ids[]} for the trade-user (anon → {false, []}); Cache-Control:no-store (mid-session changes). Dropdown lazily fetches saved-ids once on first input event, caches in window._savedIds Set, and decorates result rows with a gold ♥ when r.id ∈ savedIds. Saves the N round-trips that /api/saved/state would require for 24 suggestions. Verified: anon {false,[]}, smoke@example.com {true,1,[72]}, /designs has 7 wiring refs (ensureSavedIds, _savedIds, /api/me/saved)
---
 server.js | 35 ++++++++++++++++++++++++++++++++++-
 1 file changed, 34 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 2aabe2a..c734903 100644
--- a/server.js
+++ b/server.js
@@ -915,6 +915,25 @@ app.get('/api/trade/me', (req, res) => {
   });
 });
 
+// GET /api/me/saved — bulk-friendly: returns all saved design IDs for the current user.
+// Used by autocomplete + grid renderers to mark ♥ on already-saved items without N round-trips.
+// Anonymous users get { authenticated:false, ids:[] }.
+app.get('/api/me/saved', (req, res) => {
+  const u = getTradeUser(req);
+  // No-store — saved state changes mid-session; cache poisoning here is a real risk.
+  res.setHeader('Cache-Control', 'no-store');
+  if (!u) return res.json({ authenticated: false, ids: [] });
+  try {
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(design_id ORDER BY saved_at DESC), '[]'::json)
+                             FROM wallco_saved_designs WHERE user_id=${parseInt(u.id, 10)};`);
+    const ids = JSON.parse(raw || '[]');
+    res.json({ authenticated: true, count: ids.length, ids });
+  } catch (e) {
+    console.error('[api/me/saved] failed:', e.message);
+    res.status(500).json({ ok: false, error: 'Lookup failed.' });
+  }
+});
+
 // GET /api/saved/state?design_id=... — idempotent probe for the PDP heart icon.
 // Returns { authenticated, saved }. Anonymous users get { authenticated:false, saved:false }.
 app.get('/api/saved/state', (req, res) => {
@@ -2182,13 +2201,15 @@ ${FOOTER}
         sItems = []; sIdx = -1;
         return;
       }
+      var savedSet = window._savedIds || null;
       var rows = results.slice(0, 8).map(function(r, i){
         var img = r.image_url ? '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8 url('+JSON.stringify(r.image_url).slice(1,-1)+') center/cover;border-radius:4px;border:1px solid var(--line,#eee)"></div>' : '<div style="width:44px;height:44px;flex-shrink:0;background:#f2efe8;border-radius:4px"></div>';
         var dot = r.dominant_hex ? '<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:'+esc(r.dominant_hex)+';border:1px solid rgba(0,0,0,.12);vertical-align:middle;margin-right:4px"></span>' : '';
         var roomBadge = (r.room_mockups && r.room_mockups.length) ? '<span style="font:10px var(--sans,system-ui);color:var(--gold,#c9a14b);margin-left:6px">·room</span>' : '';
+        var heart = (savedSet && savedSet.has(r.id)) ? '<span title="Saved" style="font:11px var(--sans,system-ui);color:var(--gold,#c9a14b);margin-left:6px" aria-label="saved">♥</span>' : '';
         return '<a href="'+esc(r.url)+'" role="option" data-idx="'+i+'" style="display:flex;align-items:center;gap:10px;padding:8px 12px;text-decoration:none;color:var(--ink,#111);border-bottom:1px solid var(--line,#f0eee9);font:13px var(--sans,system-ui)">'
           + img
-          + '<div style="min-width:0;flex:1"><div style="font-weight:500;color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title||('Design #'+r.id))+'</div>'
+          + '<div style="min-width:0;flex:1"><div style="font-weight:500;color:var(--ink,#111);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+esc(r.title||('Design #'+r.id))+heart+'</div>'
           + '<div style="font:11px var(--sans,system-ui);color:var(--ink-faint,#888);margin-top:2px">'+dot+esc(r.category||'')+roomBadge+'</div></div>'
         + '</a>';
       }).join('');
@@ -2216,10 +2237,22 @@ ${FOOTER}
         .then(function(j){ window._facets = j.facets || null; window._didYouMean = j.did_you_mean || []; render(j.results||[], qStr); })
         .catch(function(){ /* aborted or network — ignore */ });
     };
+    // Lazy-fetch saved-ids once on first input so the dropdown can render ♥ on already-saved designs.
+    // Anonymous users get authenticated:false + empty ids; we cache the result either way.
+    var savedFetched = false;
+    function ensureSavedIds(){
+      if (savedFetched || window._savedIds) return;
+      savedFetched = true;
+      fetch('/api/me/saved', { credentials: 'same-origin' })
+        .then(function(r){ return r.json(); })
+        .then(function(j){ window._savedIds = new Set(j.ids || []); })
+        .catch(function(){ window._savedIds = new Set(); });
+    }
     sIn.addEventListener('input', function(){
       var v = this.value.trim();
       if (sTimer) clearTimeout(sTimer);
       if (v.length < 2) { hide(); return; }
+      ensureSavedIds();
       sTimer = setTimeout(function(){ fetchSuggest(v); }, 180);
     });
     sIn.addEventListener('keydown', function(e){

← c82055d security parity with starsofdesign: app.disable(x-powered-by  ·  back to Wallco Ai  ·  wallco.ai · home 'Stumble through the catalog' rail gets a c 3ae8ad8 →