← back to Whatsmystyle
yolo tick 32: copy-URL button on /share/recs/:id page (clipboard with input-select fallback) + admin-only inline 'X embedded of Y catalog · pct%' status pill above recs grid (mini progress bar, polls /api/admin/catalog-status, hides for non-admin or view_as) + brand-deep-link screen routing (boot reads ?brand=, hoists __brandDeepLink flag, falls through to route('recs') instead of route('home') so filter pill is immediately visible)
24e6d95996ae1ec6016cf207709997fa27967ee4 · 2026-05-12 17:10:39 -0700 · SteveStudio2
Files touched
M data/waitlist.csvM data/whatsmystyle.db-shmM data/whatsmystyle.db-walM public/css/app.cssM public/index.htmlM public/js/app.jsM public/share-recs.html
Diff
commit 24e6d95996ae1ec6016cf207709997fa27967ee4
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 17:10:39 2026 -0700
yolo tick 32: copy-URL button on /share/recs/:id page (clipboard with input-select fallback) + admin-only inline 'X embedded of Y catalog · pct%' status pill above recs grid (mini progress bar, polls /api/admin/catalog-status, hides for non-admin or view_as) + brand-deep-link screen routing (boot reads ?brand=, hoists __brandDeepLink flag, falls through to route('recs') instead of route('home') so filter pill is immediately visible)
---
data/waitlist.csv | 2 ++
data/whatsmystyle.db-shm | Bin 32768 -> 32768 bytes
data/whatsmystyle.db-wal | Bin 4610312 -> 4610312 bytes
public/css/app.css | 12 ++++++++++++
public/index.html | 5 +++++
public/js/app.js | 27 ++++++++++++++++++++++++++-
public/share-recs.html | 20 ++++++++++++++++++++
7 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/data/waitlist.csv b/data/waitlist.csv
index fe5d598..2331d33 100644
--- a/data/waitlist.csv
+++ b/data/waitlist.csv
@@ -69,3 +69,5 @@ e2e-1778628530526@example.com,e2e,2026-05-12T23:28:50.526Z
e2e-1778628742953@example.com,e2e,2026-05-12T23:32:22.953Z
e2e-1778629716355@example.com,e2e,2026-05-12T23:48:36.377Z
e2e-1778630034615@example.com,e2e,2026-05-12T23:53:54.615Z
+e2e-1778630719170@example.com,e2e,2026-05-13T00:05:19.172Z
+e2e-1778630998656@example.com,e2e,2026-05-13T00:09:58.661Z
diff --git a/data/whatsmystyle.db-shm b/data/whatsmystyle.db-shm
index dfb6320..6ef09d7 100644
Binary files a/data/whatsmystyle.db-shm and b/data/whatsmystyle.db-shm differ
diff --git a/data/whatsmystyle.db-wal b/data/whatsmystyle.db-wal
index 771dad8..f821608 100644
Binary files a/data/whatsmystyle.db-wal and b/data/whatsmystyle.db-wal differ
diff --git a/public/css/app.css b/public/css/app.css
index 6bc96d2..9c959b4 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -167,6 +167,18 @@ h2 { font-family: var(--font-display); font-weight: 600; font-size: 36px; margin
background: var(--paper-2); border-radius: 16px; border: 1px dashed var(--line);
}
.recs-empty p { margin: 0 0 16px; }
+.embed-status-pill {
+ display: inline-flex; align-items: center; gap: 10px;
+ font-size: 12px; color: #707070; padding: 6px 12px;
+ background: var(--paper-2); border-radius: 999px;
+ margin: 6px 0 14px; font-variant-numeric: tabular-nums;
+}
+.embed-status-pill .embed-status-bar {
+ width: 80px; height: 4px; background: rgba(0,0,0,.08); border-radius: 999px; overflow: hidden;
+}
+.embed-status-pill .embed-status-fill {
+ height: 100%; background: var(--ok, #2ea44f); width: 0%; transition: width .4s ease;
+}
@media (hover: none) and (pointer: coarse) {
.kb-hint { display: none !important; }
}
diff --git a/public/index.html b/public/index.html
index 21a43e4..08daa6b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -156,6 +156,11 @@
<p class="muted" id="recs-empty-msg">No matches with your current filters.</p>
<button type="button" class="ghost" id="recs-empty-clear">Clear filters</button>
</div>
+ <!-- Tick 32: inline embedded/catalog status pill — admin only -->
+ <div id="recs-embed-status" class="embed-status-pill" data-role-only="admin" hidden aria-live="polite">
+ <span id="recs-embed-text">…</span>
+ <div class="embed-status-bar"><div class="embed-status-fill" id="recs-embed-fill"></div></div>
+ </div>
<!-- Tailor the view — set-decorator only (tick 16). Sliders re-rank
the grid to match the show's visual language. Persisted to localStorage. -->
diff --git a/public/js/app.js b/public/js/app.js
index 12686a8..1a3e733 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -1143,6 +1143,8 @@ async function loadRecs() {
} else if (empty) {
empty.hidden = true;
}
+ // Tick 32: inline embed-status pill — admin only, surfaces drainer backlog.
+ refreshRecsEmbedStatus();
items.forEach(it => {
const a = document.createElement('a');
a.className = 'card';
@@ -1163,13 +1165,16 @@ $('#sort').addEventListener('change', e => { localStorage.setItem('wms.sort', e.
$('#density').addEventListener('input', e => { localStorage.setItem('wms.density', e.target.value); loadRecs(); });
// Tick 31: brand filter on boot — read ?brand= and stash; clear-brand-filter pill resets.
+// Tick 32: when ?brand= is set on boot, also force route to /recs so the
+// filter is immediately visible (deep-link from /brands → main app).
+let __brandDeepLink = false;
(function hydrateBrandFilterFromURL() {
try {
const params = new URLSearchParams(location.search);
const b = (params.get('brand') || '').trim();
if (b) {
localStorage.setItem('wms.brand_filter', b);
- // Strip the param so reloads don't keep re-applying it after manual clear.
+ __brandDeepLink = true;
params.delete('brand');
const qs = params.toString();
history.replaceState(null, '', location.pathname + (qs ? `?${qs}` : '') + location.hash);
@@ -1177,6 +1182,23 @@ $('#density').addEventListener('input', e => { localStorage.setItem('wms.density
} catch {}
})();
+// Tick 32: admin-only inline embed-status pill above the recs grid.
+async function refreshRecsEmbedStatus() {
+ const el = $('#recs-embed-status');
+ if (!el) return;
+ if (!Role.is_admin || Role.view_as) { el.hidden = true; return; }
+ try {
+ const r = await fetch('/api/admin/catalog-status');
+ if (!r.ok) { el.hidden = true; return; }
+ const j = await r.json();
+ if (!j.total) { el.hidden = true; return; }
+ const pct = Math.round((j.embedded / j.total) * 100);
+ $('#recs-embed-text').textContent = `${j.embedded.toLocaleString()} embedded of ${j.total.toLocaleString()} catalog · ${pct}%`;
+ $('#recs-embed-fill').style.width = pct + '%';
+ el.hidden = false;
+ } catch { el.hidden = true; }
+}
+
document.addEventListener('click', e => {
if (e.target.closest('#clear-brand-filter')) {
localStorage.removeItem('wms.brand_filter');
@@ -2176,5 +2198,8 @@ document.addEventListener('click', e => {
await refreshFavorites(); // tick 24 — populate star state before card renders
const me = await fetch('/api/me').then(r => r.json());
if (!me.onboarded) { show('onboard'); onbRender(); }
+ // Tick 32: if user came in via /brands → ?brand= deep link, jump straight
+ // to the recs grid (which renders the filter pill) instead of duel.
+ else if (__brandDeepLink) route('recs');
else route('home');
})();
diff --git a/public/share-recs.html b/public/share-recs.html
index 93f49e3..1c68c73 100644
--- a/public/share-recs.html
+++ b/public/share-recs.html
@@ -23,6 +23,13 @@
<body>
<h1 id="title">Shared list</h1>
<p class="sub" id="sub">Loading…</p>
+ <div style="margin: -6px 0 16px;">
+ <button type="button" id="copy-url-btn"
+ style="font-family: inherit; font-size: 13px; padding: 7px 14px; border: 1px solid #c8c1b3; border-radius: 999px; background: #fff; color: #1d1d1f; cursor: pointer;">
+ Copy share URL
+ </button>
+ <span id="copy-url-out" style="margin-left: 10px; color: #707070; font-size: 13px;"></span>
+ </div>
<div id="state-pills" class="state-pills" hidden></div>
<div id="grid" class="grid"></div>
<p class="cta">Want a list like this? <a href="/">Build yours on WhatsMyStyle →</a></p>
@@ -30,6 +37,19 @@
<script>
const $ = s => document.querySelector(s);
function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+ // Copy URL button — try clipboard, fall back to selecting the visible URL.
+ document.getElementById('copy-url-btn').addEventListener('click', async () => {
+ const url = location.href;
+ const out = document.getElementById('copy-url-out');
+ try { await navigator.clipboard.writeText(url); out.textContent = 'Copied'; setTimeout(() => out.textContent = '', 2400); }
+ catch {
+ // Fallback: write the URL into a span and select it
+ out.innerHTML = `<input type="text" readonly value="${url.replace(/"/g, '"')}" style="width: 320px; font-family: ui-monospace, Menlo, monospace; font-size: 12px; padding: 4px 6px;">`;
+ const inp = out.querySelector('input');
+ inp.focus(); inp.select();
+ }
+ });
+
(async function () {
const shareId = location.pathname.split('/').pop();
const r = await fetch(`/api/share/recs/${encodeURIComponent(shareId)}`);
← 91dc0d4 yolo tick 31: /recs empty-state panel (clear-filters button
·
back to Whatsmystyle
·
scripts/e2e-random-photos.js — real-photo E2E harness 8af7af1 →