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}}
{{end}}
{{if .Rank}}#{{.Rank}}{{end}}
-
{{.Title}}
+ {{.Title}}
{{if .Author}}
{{.Author}}
{{end}}
@@ -731,21 +732,14 @@ const rankingTmpl = `
{{range .Genres}}{{.}}{{end}}
{{if .SourceURL}}
-
@@ -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();
+ });
+
}());
`