From f03ff393972660fad006c4f7d1f5059e319e995d Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 1 Mar 2026 22:28:47 +0500 Subject: [PATCH] Replace ranking Scrape button with two-click card interaction - Remove Scrape form/button and Source text link from non-local ranking cards - First click on a card highlights it amber and shows 'Click again to scrape' hint - Second click POSTs to /ui/scrape/book and replaces card with polling status badge - Pending state auto-cancels after 3 s or when clicking outside - Source icon (external link SVG) retained but dimmed; stops click propagation --- scraper/internal/server/ui.go | 90 +++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/scraper/internal/server/ui.go b/scraper/internal/server/ui.go index 61b5b94..d6bd2af 100644 --- a/scraper/internal/server/ui.go +++ b/scraper/internal/server/ui.go @@ -716,14 +716,15 @@ const rankingTmpl = ` data-status="{{.Status}}" data-genres="{{range .Genres}}{{.}}|{{end}}" data-local="0" - class="group flex gap-3 rounded-xl border border-zinc-800 bg-zinc-900 p-3 hover:border-amber-500 transition-colors min-w-0"> + {{if .SourceURL}}data-scrape-url="{{.SourceURL}}"{{end}} + class="ranking-card group flex gap-3 rounded-xl border border-zinc-800 bg-zinc-900 p-3 transition-colors min-w-0{{if .SourceURL}} cursor-pointer hover:border-zinc-600{{end}}"> {{if .Cover}} cover {{end}}
{{if .Rank}}#{{.Rank}}{{end}} -

{{.Title}}

+

{{.Title}}

{{if .Author}}

{{.Author}}

{{end}}
@@ -731,21 +732,14 @@ const rankingTmpl = ` {{range .Genres}}{{.}}{{end}}
{{if .SourceURL}} -
-
- - -
- + {{end}}
@@ -865,6 +859,74 @@ const rankingTmpl = ` applyFilters(); }); } + + /* ── two-click scrape on ranking cards ─────────────────────────────────── */ + var pendingCard = null; + var pendingTimer = null; + + function cancelPending() { + if (!pendingCard) return; + pendingCard.style.borderColor = ''; + pendingCard.style.background = ''; + var hint = pendingCard.querySelector('.scrape-hint'); + if (hint) hint.classList.add('hidden'); + pendingCard = null; + if (pendingTimer) { clearTimeout(pendingTimer); pendingTimer = null; } + } + + function submitScrape(card) { + var url = card.dataset.scrapeUrl; + if (!url) return; + // Replace inner content with a running badge immediately. + var badge = document.createElement('div'); + badge.className = 'flex items-center text-sm px-3 py-2 rounded-lg border text-amber-300 bg-amber-950 border-amber-800'; + badge.innerHTML = 'Scraping…'; + badge.setAttribute('hx-get', '/ui/scrape/status'); + badge.setAttribute('hx-trigger', 'every 3s'); + badge.setAttribute('hx-target', 'this'); + badge.setAttribute('hx-swap', 'outerHTML'); + card.innerHTML = ''; + card.appendChild(badge); + card.style.borderColor = ''; + card.style.background = ''; + card.style.cursor = 'default'; + card.classList.remove('cursor-pointer'); + htmx.process(badge); + + var body = new URLSearchParams(); + body.set('url', url); + fetch('/ui/scrape/book', { method: 'POST', body: body }); + } + + grid.addEventListener('click', function (e) { + var card = e.target.closest('.ranking-card[data-scrape-url]'); + if (!card) return; + // Ignore clicks on the source button. + if (e.target.closest('button')) return; + + if (pendingCard && pendingCard !== card) cancelPending(); + + if (card === pendingCard) { + // Second click — go. + cancelPending(); + submitScrape(card); + } else { + // First click — enter pending state. + pendingCard = card; + card.style.borderColor = '#f59e0b'; + card.style.background = 'rgba(245,158,11,0.04)'; + var hint = card.querySelector('.scrape-hint'); + if (hint) hint.classList.remove('hidden'); + // Auto-cancel after 3 s if no second click. + pendingTimer = setTimeout(cancelPending, 3000); + } + }); + + // Clicking anywhere outside a card cancels pending state. + document.addEventListener('click', function (e) { + if (pendingCard && !e.target.closest('.ranking-card')) cancelPending(); + }); + }()); `