← back to Wallco Ai
/me/saved: surface 'Saved Xh ago' badge per card (SELECT already pulled saved_at, render dropped it)
80b15e737254540066aa33a94c50c12d98610c0b · 2026-05-12 20:28:42 -0700 · SteveStudio2
Files touched
Diff
commit 80b15e737254540066aa33a94c50c12d98610c0b
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 12 20:28:42 2026 -0700
/me/saved: surface 'Saved Xh ago' badge per card (SELECT already pulled saved_at, render dropped it)
---
server.js | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/server.js b/server.js
index 4978991..5981985 100644
--- a/server.js
+++ b/server.js
@@ -1129,17 +1129,38 @@ app.get('/me/saved', (req, res) => {
// snapshot we already render everywhere). Designs that no longer exist in
// the catalog are silently filtered out.
let savedIds = [];
+ // Carry saved_at + note through to the card render so users can see
+ // "Saved 2h ago" without opening DevTools — the rail audit gap that
+ // previously dropped this metadata immediately after the SELECT.
+ let savedMetaById = new Map();
try {
const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.saved_at DESC), '[]'::json) FROM (
SELECT design_id, note, saved_at FROM wallco_saved_designs
WHERE user_id=${parseInt(u.id, 10)}) t;`);
const rows = JSON.parse(raw || '[]');
savedIds = rows.map(r => r.design_id);
+ savedMetaById = new Map(rows.map(r => [r.design_id, r]));
} catch (e) {
console.error('[me/saved] query failed:', e.message);
}
const byId = new Map(DESIGNS.map(d => [d.id, d]));
const designs = savedIds.map(id => byId.get(id)).filter(Boolean);
+ // Relative-time formatter (server-rendered so the badge ships in HTML — no JS
+ // flash). Uses 'just now' under 60s, '12m', '4h', '3d', else date.
+ const _meSavedRelTime = (iso) => {
+ if (!iso) return '';
+ const t = Date.parse(iso);
+ if (!t) return '';
+ const dMs = Date.now() - t;
+ if (dMs < 60_000) return 'just now';
+ const m = Math.floor(dMs / 60_000);
+ if (m < 60) return m + 'm ago';
+ const h = Math.floor(m / 60);
+ if (h < 24) return h + 'h ago';
+ const days = Math.floor(h / 24);
+ if (days < 30) return days + 'd ago';
+ return new Date(t).toISOString().slice(0, 10);
+ };
const cards = designs.map(d => {
const roomKey = (d.room_mockups || []).includes('living_room') ? 'living_room'
@@ -1149,6 +1170,12 @@ app.get('/me/saved', (req, res) => {
const roomOverlay = roomKey
? `<img class="card-room" src="/designs/room/design_${d.id}_${roomKey}.png" loading="lazy" alt="" aria-hidden="true">`
: '';
+ const meta = savedMetaById.get(d.id) || {};
+ const rel = _meSavedRelTime(meta.saved_at);
+ const absISO = meta.saved_at ? new Date(meta.saved_at).toISOString() : '';
+ const savedBadge = rel
+ ? `<span class="saved-when" title="Saved ${_escTrade(absISO)}" style="position:absolute;top:8px;left:8px;background:rgba(15,14,12,.78);color:#e8e2d6;border-radius:4px;padding:3px 7px;font:11px var(--sans,system-ui);letter-spacing:.02em;pointer-events:none">Saved ${_escTrade(rel)}</span>`
+ : '';
return `
<div class="saved-card-wrap" data-design-id="${d.id}" style="position:relative">
<a href="/design/${d.id}" class="design-card${roomKey ? ' has-room' : ''}" aria-label="${_escTrade(d.title || ('Design #' + d.id))}">
@@ -1159,6 +1186,7 @@ app.get('/me/saved', (req, res) => {
<span class="color-dot" style="background:${_escTrade(d.dominant_hex || '#ccc')}" aria-hidden="true"></span>
</div>
</a>
+ ${savedBadge}
<button type="button" class="btn-remove-saved" data-id="${d.id}" title="Remove from saved"
style="position:absolute;top:8px;right:8px;background:rgba(15,14,12,.85);color:#e8e2d6;border:0;border-radius:4px;padding:5px 9px;font:11px var(--sans,system-ui);cursor:pointer;letter-spacing:.04em;text-transform:uppercase">
Remove
← 69c2529 drunk-animals: hero block on /designs?cat=drunk-animals + gh
·
back to Wallco Ai
·
/compare: ETag + 304 keyed on (catalog lastmod, view, ordere 73f6770 →