829 lines
44 KiB
Plaintext
829 lines
44 KiB
Plaintext
<!DOCTYPE html>
|
|
<html data-theme="night" lang="id">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>SCRPR // News Scraper API</title>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.10.1/dist/full.min.css" rel="stylesheet" />
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
|
|
<style>
|
|
:root {
|
|
--cyan: #00f5ff;
|
|
--pink: #ff2d78;
|
|
--yellow: #f5e642;
|
|
--green: #00ff88;
|
|
--purple: #b44fff;
|
|
--dark: #03060e;
|
|
--dark2: #070b17;
|
|
--card: #0a0e1a;
|
|
--card2: #0d1220;
|
|
--border: #151d30;
|
|
--bord2: #1e2840;
|
|
}
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; }
|
|
html { scroll-behavior: smooth; }
|
|
body {
|
|
background: var(--dark);
|
|
font-family: 'Rajdhani', sans-serif;
|
|
color: #b0bcd4;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* scanlines */
|
|
.scanlines {
|
|
position: fixed; inset: 0;
|
|
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.055) 2px, rgba(0,0,0,0.055) 4px);
|
|
pointer-events: none; z-index: 9999;
|
|
}
|
|
/* grid */
|
|
.grid-bg {
|
|
position: fixed; inset: 0;
|
|
background-image:
|
|
linear-gradient(rgba(0,245,255,0.022) 1px, transparent 1px),
|
|
linear-gradient(90deg, rgba(0,245,255,0.022) 1px, transparent 1px);
|
|
background-size: 48px 48px;
|
|
pointer-events: none; z-index: 0;
|
|
}
|
|
/* blobs */
|
|
.blob { position: fixed; border-radius: 50%; filter: blur(130px); pointer-events: none; z-index: 0; opacity: 0.065; }
|
|
.blob-1 { width:600px;height:600px;background:var(--cyan);top:-220px;left:-200px; }
|
|
.blob-2 { width:500px;height:500px;background:var(--pink);bottom:-160px;right:-180px; }
|
|
.blob-3 { width:350px;height:350px;background:var(--purple);top:45%;left:42%;opacity:0.04; }
|
|
|
|
.mono { font-family: 'Share Tech Mono', monospace; }
|
|
.orb { font-family: 'Orbitron', sans-serif; }
|
|
|
|
/* glitch */
|
|
.glitch {
|
|
position: relative;
|
|
color: var(--cyan);
|
|
text-shadow: 0 0 25px rgba(0,245,255,0.45), 0 0 70px rgba(0,245,255,0.12);
|
|
}
|
|
.glitch::before,.glitch::after {
|
|
content: attr(data-text);
|
|
position: absolute; top:0; left:0; width:100%;
|
|
}
|
|
.glitch::before { color:var(--pink); animation:g1 4s infinite; clip-path:polygon(0 0,100% 0,100% 38%,0 38%); }
|
|
.glitch::after { color:var(--yellow);animation:g2 4s infinite; clip-path:polygon(0 62%,100% 62%,100% 100%,0 100%); }
|
|
@keyframes g1 {
|
|
0%,88%,100%{transform:translate(0);opacity:0}
|
|
90%{transform:translate(-4px,1px);opacity:.85}
|
|
93%{transform:translate(4px,-2px);opacity:.85}
|
|
95%{transform:translate(0);opacity:0}
|
|
}
|
|
@keyframes g2 {
|
|
0%,88%,100%{transform:translate(0);opacity:0}
|
|
91%{transform:translate(4px,2px);opacity:.75}
|
|
94%{transform:translate(-3px,-1px);opacity:.75}
|
|
96%{transform:translate(0);opacity:0}
|
|
}
|
|
|
|
/* cards */
|
|
.neon-card { background:var(--card); border:1px solid var(--bord2); position:relative; overflow:hidden; }
|
|
.neon-card::after {
|
|
content:''; position:absolute; top:0;left:0;right:0;height:1px;
|
|
background:linear-gradient(90deg,transparent,var(--cyan),transparent); opacity:.55;
|
|
}
|
|
.neon-card-pink::after { background:linear-gradient(90deg,transparent,var(--pink),transparent); }
|
|
.neon-card-purple::after { background:linear-gradient(90deg,transparent,var(--purple),transparent); }
|
|
.neon-card-green::after { background:linear-gradient(90deg,transparent,var(--green),transparent); }
|
|
|
|
/* corners */
|
|
.corner::before,.corner::after {
|
|
content:''; position:absolute; width:14px; height:14px;
|
|
}
|
|
.corner::before { top:-1px;left:-1px; border-top:2px solid var(--cyan); border-left:2px solid var(--cyan); }
|
|
.corner::after { bottom:-1px;right:-1px; border-bottom:2px solid var(--cyan); border-right:2px solid var(--cyan); }
|
|
|
|
/* nav */
|
|
nav {
|
|
position:sticky; top:0; z-index:100;
|
|
background:rgba(3,6,14,0.9);
|
|
backdrop-filter:blur(16px);
|
|
border-bottom:1px solid var(--bord2);
|
|
}
|
|
.nav-link {
|
|
font-family:'Share Tech Mono',monospace; font-size:0.68rem;
|
|
color:#2d3d55; text-transform:uppercase; letter-spacing:.1em;
|
|
padding:6px 12px; border:1px solid transparent; transition:all .2s; cursor:pointer;
|
|
}
|
|
.nav-link:hover,.nav-link.active {
|
|
color:var(--cyan); border-color:rgba(0,245,255,.2);
|
|
text-shadow:0 0 8px rgba(0,245,255,.5);
|
|
}
|
|
|
|
/* tabs */
|
|
/* ── tabs scroll container ── */
|
|
.tabs-scroll {
|
|
display: flex;
|
|
gap: 6px;
|
|
overflow-x: auto;
|
|
overflow-y: visible;
|
|
padding-bottom: 6px;
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
position: relative;
|
|
}
|
|
.tabs-scroll::-webkit-scrollbar { display: none; }
|
|
|
|
/* fade edges to hint scroll */
|
|
.tabs-wrap {
|
|
position: relative;
|
|
}
|
|
.tabs-wrap::before, .tabs-wrap::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; bottom: 6px;
|
|
width: 28px;
|
|
pointer-events: none;
|
|
z-index: 2;
|
|
}
|
|
.tabs-wrap::before { left: 0; background: linear-gradient(90deg, var(--card), transparent); }
|
|
.tabs-wrap::after { right: 0; background: linear-gradient(-90deg, var(--card), transparent); }
|
|
|
|
.scraper-tab {
|
|
background: transparent;
|
|
border: 1px solid var(--border);
|
|
color: #2d3d55;
|
|
font-family: 'Share Tech Mono', monospace;
|
|
font-size: 0.67rem;
|
|
padding: 6px 13px;
|
|
cursor: pointer;
|
|
transition: all .2s;
|
|
text-transform: uppercase;
|
|
letter-spacing: .06em;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
.scraper-tab:hover {
|
|
border-color: rgba(0,245,255,.4);
|
|
color: var(--cyan);
|
|
text-shadow: 0 0 8px rgba(0,245,255,.4);
|
|
}
|
|
.scraper-tab.active {
|
|
border-color: var(--cyan);
|
|
background: rgba(0,245,255,.07);
|
|
color: var(--cyan);
|
|
text-shadow: 0 0 8px rgba(0,245,255,.5);
|
|
box-shadow: 0 0 16px rgba(0,245,255,.08), inset 0 0 16px rgba(0,245,255,.04);
|
|
}
|
|
/* active bottom line accent */
|
|
.scraper-tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px; left: 10%; right: 10%;
|
|
height: 2px;
|
|
background: var(--cyan);
|
|
box-shadow: 0 0 6px var(--cyan);
|
|
}
|
|
.tab-all { border-color: rgba(180,79,255,.2); color: #3a2a55; }
|
|
.tab-all:hover { border-color: rgba(180,79,255,.5) !important; color: var(--purple) !important; text-shadow: 0 0 8px rgba(180,79,255,.4) !important; }
|
|
.tab-all.active { border-color: var(--purple) !important; color: var(--purple) !important; background: rgba(180,79,255,.07) !important; box-shadow: 0 0 16px rgba(180,79,255,.08) !important; }
|
|
.tab-all.active::after { background: var(--purple) !important; box-shadow: 0 0 6px var(--purple) !important; }
|
|
|
|
/* input */
|
|
.cyber-input {
|
|
background:rgba(0,0,0,.45); border:1px solid var(--bord2); color:#e2e8f0;
|
|
font-family:'Share Tech Mono',monospace; font-size:.84rem;
|
|
padding:11px 16px; width:100%; outline:none; transition:all .2s;
|
|
}
|
|
.cyber-input:focus { border-color:var(--cyan); box-shadow:0 0 0 2px rgba(0,245,255,.07),inset 0 0 18px rgba(0,245,255,.02); }
|
|
.cyber-input::placeholder { color:#1a2535; }
|
|
|
|
/* buttons */
|
|
.cyber-btn {
|
|
background:transparent; border:1px solid var(--cyan); color:var(--cyan);
|
|
font-family:'Share Tech Mono',monospace; font-size:.78rem;
|
|
padding:11px 26px; cursor:pointer; letter-spacing:.12em;
|
|
text-transform:uppercase; position:relative; overflow:hidden; transition:all .25s; white-space:nowrap;
|
|
}
|
|
.cyber-btn::before {
|
|
content:''; position:absolute; top:0;left:-100%;width:100%;height:100%;
|
|
background:linear-gradient(90deg,transparent,rgba(0,245,255,.12),transparent); transition:left .4s;
|
|
}
|
|
.cyber-btn:hover::before{left:100%}
|
|
.cyber-btn:hover { background:rgba(0,245,255,.07); box-shadow:0 0 24px rgba(0,245,255,.16); text-shadow:0 0 10px rgba(0,245,255,.9); }
|
|
.cyber-btn:disabled { border-color:var(--border); color:#1a2535; cursor:not-allowed; }
|
|
.cyber-btn:disabled::before { display:none; }
|
|
|
|
/* result */
|
|
.result-item {
|
|
background:rgba(0,0,0,.22); border:1px solid var(--border); border-left:2px solid var(--cyan);
|
|
padding:13px 15px; transition:all .2s; animation:slideIn .32s ease forwards; opacity:0;
|
|
}
|
|
.result-item:hover {
|
|
background:rgba(0,245,255,.025); border-color:rgba(0,245,255,.2);
|
|
border-left-color:var(--pink); transform:translateX(2px);
|
|
}
|
|
@keyframes slideIn { from{opacity:0;transform:translateX(-10px)} to{opacity:1;transform:translateX(0)} }
|
|
|
|
/* tags */
|
|
.tag { font-family:'Share Tech Mono',monospace; font-size:.58rem; padding:2px 7px; border:1px solid; text-transform:uppercase; letter-spacing:.07em; display:inline-block; }
|
|
.tag-cyan { border-color:rgba(0,245,255,.35); color:var(--cyan); background:rgba(0,245,255,.05); }
|
|
.tag-pink { border-color:rgba(255,45,120,.35); color:var(--pink); background:rgba(255,45,120,.05); }
|
|
.tag-yellow { border-color:rgba(245,230,66,.35); color:var(--yellow); background:rgba(245,230,66,.05); }
|
|
.tag-green { border-color:rgba(0,255,136,.35); color:var(--green); background:rgba(0,255,136,.05); }
|
|
.tag-purple { border-color:rgba(180,79,255,.35); color:var(--purple); background:rgba(180,79,255,.05); }
|
|
|
|
/* status bar */
|
|
.status-bar {
|
|
font-family:'Share Tech Mono',monospace; font-size:.62rem; color:#1a2535;
|
|
border-top:1px solid var(--border); padding:5px 16px;
|
|
display:flex; justify-content:space-between; align-items:center; gap:6px; flex-wrap:wrap;
|
|
}
|
|
.blink-dot {
|
|
display:inline-block; width:5px; height:5px; border-radius:50%;
|
|
background:var(--green); box-shadow:0 0 5px var(--green);
|
|
animation:blink 1.8s infinite; margin-right:5px;
|
|
}
|
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:.15}}
|
|
|
|
/* spinner */
|
|
.cyber-spinner {
|
|
width:26px; height:26px;
|
|
border:2px solid var(--bord2); border-top-color:var(--cyan); border-right-color:rgba(0,245,255,.25);
|
|
border-radius:50%; animation:spin .65s linear infinite;
|
|
}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
|
|
/* code blocks */
|
|
.code-block {
|
|
background:rgba(0,0,0,.5); border:1px solid var(--bord2); border-left:2px solid var(--purple);
|
|
font-family:'Share Tech Mono',monospace; font-size:.73rem; color:#7a8899;
|
|
padding:13px 15px; overflow-x:auto; line-height:1.75; white-space:pre;
|
|
}
|
|
.kw { color:var(--pink); }
|
|
.str { color:var(--green); }
|
|
.num { color:var(--yellow); }
|
|
.cm { color:#2d3d55; }
|
|
.url { color:var(--cyan); }
|
|
|
|
/* section label */
|
|
.section-label {
|
|
font-family:'Share Tech Mono',monospace; font-size:.68rem; text-transform:uppercase;
|
|
letter-spacing:.15em; display:flex; align-items:center; gap:10px;
|
|
}
|
|
.section-label::after { content:''; flex:1; height:1px; background:linear-gradient(90deg,var(--bord2),transparent); }
|
|
|
|
/* endpoint rows */
|
|
.endpoint-row { padding:14px 0; border-bottom:1px solid var(--border); display:flex; gap:12px; flex-wrap:wrap; }
|
|
.endpoint-row:last-child { border-bottom:none; }
|
|
|
|
/* stat card */
|
|
.stat-card {
|
|
background:var(--card2); border:1px solid var(--bord2); padding:18px 10px;
|
|
text-align:center; position:relative; overflow:hidden;
|
|
}
|
|
.stat-card::before {
|
|
content:''; position:absolute; bottom:0;left:0;right:0;height:1px;
|
|
background:linear-gradient(90deg,transparent,var(--cyan),transparent); opacity:.25;
|
|
}
|
|
|
|
/* toast */
|
|
.toast-msg {
|
|
position:fixed; bottom:22px; right:20px;
|
|
background:var(--card2); border:1px solid var(--cyan); color:var(--cyan);
|
|
font-family:'Share Tech Mono',monospace; font-size:.7rem;
|
|
padding:9px 16px; box-shadow:0 0 28px rgba(0,245,255,.18); z-index:9998;
|
|
animation:toastIn .2s ease;
|
|
}
|
|
@keyframes toastIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
|
|
/* tab content */
|
|
.tab-content { display:none; }
|
|
.tab-content.active { display:block; }
|
|
|
|
/* scrollbar */
|
|
::-webkit-scrollbar{width:3px;height:3px}
|
|
::-webkit-scrollbar-track{background:var(--dark)}
|
|
::-webkit-scrollbar-thumb{background:var(--bord2)}
|
|
::-webkit-scrollbar-thumb:hover{background:var(--cyan)}
|
|
|
|
/* ── RESPONSIVE ── */
|
|
@media(max-width:640px){
|
|
.glitch{font-size:2.2rem!important}
|
|
.stat-grid{grid-template-columns:repeat(2,1fr)!important}
|
|
.input-row{flex-direction:column}
|
|
.input-row .cyber-btn{width:100%;text-align:center}
|
|
.status-bar{font-size:.56rem}
|
|
.hide-sm{display:none}
|
|
}
|
|
@media(max-width:400px){
|
|
.scraper-tab{font-size:.62rem;padding:4px 8px}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="scanlines"></div>
|
|
<div class="grid-bg"></div>
|
|
<div class="blob blob-1"></div>
|
|
<div class="blob blob-2"></div>
|
|
<div class="blob blob-3"></div>
|
|
|
|
<!-- NAV -->
|
|
<nav>
|
|
<div class="max-w-5xl mx-auto px-4 py-2 flex items-center justify-between gap-3">
|
|
<div class="flex items-center gap-2">
|
|
<span class="orb text-base font-black tracking-widest" style="color:var(--cyan);text-shadow:0 0 16px rgba(0,245,255,.4);">SCRPR</span>
|
|
<span class="tag tag-green" style="font-size:.52rem;">LIVE</span>
|
|
</div>
|
|
<div class="flex items-center gap-1 flex-wrap">
|
|
<button class="nav-link active" onclick="switchSection('scraper',this)">SCRAPER</button>
|
|
<button class="nav-link" onclick="switchSection('api',this)">API DOCS</button>
|
|
<button class="nav-link" onclick="switchSection('sources',this)">SOURCES</button>
|
|
</div>
|
|
<div class="mono text-xs hide-sm" style="color:#1a2535;" id="clock">--:--:--</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="relative z-10 max-w-5xl mx-auto px-4 py-6 space-y-5">
|
|
|
|
<!-- HERO -->
|
|
<div class="text-center py-6 sm:py-10 space-y-2">
|
|
<div class="mono text-xs mb-3" style="color:#1a2535;letter-spacing:.3em;">// NEURAL SCRAPING ENGINE v1.0</div>
|
|
<h1 class="glitch orb text-5xl sm:text-7xl font-black tracking-widest" data-text="SCRPR">SCRPR</h1>
|
|
<p class="text-xs sm:text-sm mt-4" style="color:#2d3d55;font-family:'Rajdhani',sans-serif;letter-spacing:.1em;">
|
|
REAL-TIME NEWS INTELLIGENCE · 12 INDONESIAN SOURCES · FREE PUBLIC API
|
|
</p>
|
|
<div class="stat-grid grid grid-cols-4 gap-2 sm:gap-3 mt-8 max-w-sm sm:max-w-md mx-auto">
|
|
<div class="stat-card"><div class="orb text-lg sm:text-xl font-bold" style="color:var(--cyan);">12+</div><div class="mono text-xs mt-1" style="color:#2d3d55;font-size:.55rem;">SOURCES</div></div>
|
|
<div class="stat-card"><div class="orb text-lg sm:text-xl font-bold" style="color:var(--pink);">FREE</div><div class="mono text-xs mt-1" style="color:#2d3d55;font-size:.55rem;">API</div></div>
|
|
<div class="stat-card"><div class="orb text-lg sm:text-xl font-bold" style="color:var(--green);">REST</div><div class="mono text-xs mt-1" style="color:#2d3d55;font-size:.55rem;">PROTOCOL</div></div>
|
|
<div class="stat-card"><div class="orb text-lg sm:text-xl font-bold" style="color:var(--yellow);">JSON</div><div class="mono text-xs mt-1" style="color:#2d3d55;font-size:.55rem;">OUTPUT</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ SCRAPER SECTION ═══════════ -->
|
|
<div id="section-scraper" class="tab-content active space-y-4">
|
|
|
|
<!-- Control panel -->
|
|
<div class="neon-card corner p-4 sm:p-5 space-y-4">
|
|
<div class="section-label" style="color:var(--cyan);">
|
|
<div class="w-1 h-5 flex-shrink-0" style="background:var(--cyan);box-shadow:0 0 8px var(--cyan);"></div>
|
|
SELECT_TARGET
|
|
</div>
|
|
|
|
<!-- Source tabs - horizontal scroll, no wrap -->
|
|
<div class="tabs-wrap">
|
|
<div class="tabs-scroll" id="scraperTabs">
|
|
<button class="scraper-tab active" data-scraper="kompas" onclick="selectScraper(this)">Kompas</button>
|
|
<button class="scraper-tab" data-scraper="detik" onclick="selectScraper(this)">Detik</button>
|
|
<button class="scraper-tab" data-scraper="cnnindonesia" onclick="selectScraper(this)">CNN ID</button>
|
|
<button class="scraper-tab" data-scraper="liputan6" onclick="selectScraper(this)">Liputan6</button>
|
|
<button class="scraper-tab" data-scraper="tribun" onclick="selectScraper(this)">Tribun</button>
|
|
<button class="scraper-tab" data-scraper="tempo" onclick="selectScraper(this)">Tempo</button>
|
|
<button class="scraper-tab" data-scraper="republika" onclick="selectScraper(this)">Republika</button>
|
|
<button class="scraper-tab" data-scraper="antara" onclick="selectScraper(this)">Antara</button>
|
|
<button class="scraper-tab" data-scraper="okezone" onclick="selectScraper(this)">Okezone</button>
|
|
<button class="scraper-tab" data-scraper="sindonews" onclick="selectScraper(this)">Sindonews</button>
|
|
<button class="scraper-tab" data-scraper="merdeka" onclick="selectScraper(this)">Merdeka</button>
|
|
<button class="scraper-tab" data-scraper="kumparan" onclick="selectScraper(this)">Kumparan</button>
|
|
<button class="scraper-tab tab-all" data-scraper="all" onclick="selectScraper(this)">⚡ ALL</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input -->
|
|
<div class="input-row flex gap-2 sm:gap-3">
|
|
<div class="flex-1 relative">
|
|
<span class="mono absolute left-3 top-1/2 -translate-y-1/2 text-xs pointer-events-none" style="color:var(--cyan);opacity:.5;">>_</span>
|
|
<input id="queryInput" class="cyber-input pl-8" type="text"
|
|
placeholder="masukkan keyword berita..."
|
|
onkeydown="if(event.key==='Enter')runScraper()" />
|
|
</div>
|
|
<button class="cyber-btn" id="runBtn" onclick="runScraper()">EXECUTE</button>
|
|
</div>
|
|
|
|
<!-- Options -->
|
|
<div class="flex items-center gap-3 sm:gap-5 flex-wrap pt-1">
|
|
<label class="mono text-xs flex items-center gap-1.5 cursor-pointer" style="color:#2d3d55;">
|
|
<input type="checkbox" id="chkTime" class="checkbox checkbox-xs" checked /> TIMESTAMP
|
|
</label>
|
|
<label class="mono text-xs flex items-center gap-1.5 cursor-pointer" style="color:#2d3d55;">
|
|
<input type="checkbox" id="chkSource" class="checkbox checkbox-xs" checked /> SOURCE
|
|
</label>
|
|
<label class="mono text-xs flex items-center gap-1.5 cursor-pointer" style="color:#2d3d55;">
|
|
<input type="checkbox" id="chkThumb" class="checkbox checkbox-xs" /> THUMB
|
|
</label>
|
|
<div class="ml-auto flex items-center gap-2 mono text-xs" style="color:#2d3d55;">
|
|
LIMIT:<span style="color:var(--yellow);min-width:22px;text-align:center;" id="maxLabel">10</span>
|
|
<input type="range" min="5" max="50" step="5" value="10" class="range range-xs w-20"
|
|
id="limitRange" oninput="document.getElementById('maxLabel').textContent=this.value"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Output panel -->
|
|
<div class="neon-card neon-card-pink corner" style="min-height:300px;">
|
|
<div class="flex items-center justify-between px-4 sm:px-5 py-3 flex-wrap gap-2" style="border-bottom:1px solid var(--border);">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-1 h-5" style="background:var(--pink);box-shadow:0 0 8px var(--pink);flex-shrink:0;"></div>
|
|
<span class="mono text-xs" style="color:var(--pink);">OUTPUT_STREAM</span>
|
|
<span id="resultCount" class="mono text-xs" style="color:#1e2840;">-- results</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button onclick="clearResults()" class="mono text-xs px-3 py-1 transition-all" style="color:#1e2840;border:1px solid var(--border);" onmouseover="this.style.color='var(--pink)';this.style.borderColor='var(--pink)'" onmouseout="this.style.color='#1e2840';this.style.borderColor='var(--border)'">CLR</button>
|
|
<button onclick="copyResults()" class="mono text-xs px-3 py-1 transition-all" style="color:#1e2840;border:1px solid var(--border);" onmouseover="this.style.color='var(--cyan)';this.style.borderColor='var(--cyan)'" onmouseout="this.style.color='#1e2840';this.style.borderColor='var(--border)'">JSON</button>
|
|
<button onclick="exportCSV()" class="mono text-xs px-3 py-1 transition-all" style="color:#1e2840;border:1px solid var(--border);" onmouseover="this.style.color='var(--green)';this.style.borderColor='var(--green)'" onmouseout="this.style.color='#1e2840';this.style.borderColor='var(--border)'">CSV</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="resultsArea" class="p-4 space-y-2 overflow-y-auto" style="max-height:500px;">
|
|
<div id="idleState" class="flex flex-col items-center justify-center py-20 gap-3">
|
|
<div class="mono text-xs" style="color:#131c28;">▓▒░ AWAITING INPUT ░▒▓</div>
|
|
<div class="mono text-xs" style="color:#0e1520;">select source · type keyword · execute</div>
|
|
</div>
|
|
<div id="loadingState" class="hidden flex-col items-center justify-center py-20 gap-4" style="display:none;">
|
|
<div class="cyber-spinner"></div>
|
|
<div class="mono text-xs" style="color:var(--cyan);" id="loadingText">SCRAPING...</div>
|
|
<div class="mono text-xs" style="color:#1a2535;" id="loadingSub">accessing target</div>
|
|
</div>
|
|
<div id="errorState" class="hidden p-4" style="border-left:2px solid var(--pink);">
|
|
<div class="mono text-xs mb-1" style="color:var(--pink);">⚠ SCRAPE_FAILED</div>
|
|
<div class="mono text-xs" style="color:#2d3d55;" id="errorMsg"></div>
|
|
</div>
|
|
<div id="resultsList" class="space-y-2" style="display:none;"></div>
|
|
</div>
|
|
|
|
<div class="status-bar">
|
|
<span><span class="blink-dot"></span>SYS::READY</span>
|
|
<span id="lastTarget" style="color:#1a2535;">NO_TARGET</span>
|
|
<span id="execTime" style="color:#1a2535;">--ms</span>
|
|
<span id="statusDate" style="color:#1a2535;" class="hide-sm"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ API DOCS SECTION ═══════════ -->
|
|
<div id="section-api" class="tab-content space-y-4">
|
|
<div class="neon-card neon-card-purple corner p-4 sm:p-5 space-y-5">
|
|
<div class="section-label" style="color:var(--purple);">
|
|
<div class="w-1 h-5 flex-shrink-0" style="background:var(--purple);box-shadow:0 0 8px var(--purple);"></div>
|
|
PUBLIC API DOCUMENTATION
|
|
</div>
|
|
|
|
<p class="text-sm" style="color:#4a5a70;line-height:1.7;">
|
|
API ini <span style="color:var(--green);">gratis</span> dan terbuka untuk publik. Tidak perlu API key atau registrasi.
|
|
Semua endpoint mengembalikan JSON.
|
|
<br/>Base URL: <span class="mono" style="color:var(--cyan);" id="baseUrlDisplay">detecting...</span>
|
|
</p>
|
|
|
|
<!-- Endpoints -->
|
|
<div>
|
|
<!-- /api/scraper -->
|
|
<div class="endpoint-row">
|
|
<span class="tag tag-green flex-shrink-0" style="margin-top:2px;">GET</span>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="mono text-sm" style="color:#e2e8f0;">/api/scraper</div>
|
|
<div class="text-xs mt-1" style="color:#3a4a60;">Scrape berita dari satu sumber tertentu.</div>
|
|
<div class="flex flex-wrap gap-3 mt-3 mb-2">
|
|
<span class="mono text-xs"><span class="tag tag-yellow">site</span> <span style="color:#3a4a60;">string · wajib</span></span>
|
|
<span class="mono text-xs"><span class="tag tag-yellow">q</span> <span style="color:#3a4a60;">string · wajib</span></span>
|
|
<span class="mono text-xs"><span class="tag tag-yellow">limit</span><span style="color:#3a4a60;">number · default 10</span></span>
|
|
</div>
|
|
<div class="code-block"><span class="cm">// request</span>
|
|
<span class="url">GET [BASE_URL]/api/scraper?site=kompas&q=pemilu&limit=5</span>
|
|
|
|
<span class="cm">// response</span>
|
|
{
|
|
<span class="str">"status"</span>: <span class="kw">true</span>,
|
|
<span class="str">"total"</span>: <span class="num">5</span>,
|
|
<span class="str">"source"</span>: <span class="str">"kompas"</span>,
|
|
<span class="str">"data"</span>: [
|
|
{
|
|
<span class="str">"title"</span>: <span class="str">"Judul berita..."</span>,
|
|
<span class="str">"link"</span>: <span class="str">"https://..."</span>,
|
|
<span class="str">"time"</span>: <span class="str">"2 jam lalu"</span>,
|
|
<span class="str">"source"</span>: <span class="str">"Kompas"</span>,
|
|
<span class="str">"thumb"</span>: <span class="str">"https://..."</span>
|
|
}
|
|
]
|
|
}</div>
|
|
<div class="flex gap-2 mt-2 flex-wrap">
|
|
<button onclick="tryCopy(`fetch('${BASE_URL}/api/scraper?site=kompas&q=pemilu&limit=5')\n .then(r => r.json())\n .then(console.log)`)" class="mono text-xs px-3 py-1 transition-all" style="color:#1e2840;border:1px solid var(--border);" onmouseover="this.style.color='var(--purple)';this.style.borderColor='var(--purple)'" onmouseout="this.style.color='#1e2840';this.style.borderColor='var(--border)'">COPY JS</button>
|
|
<button onclick="tryCopy(`curl \"${BASE_URL}/api/scraper?site=kompas&q=pemilu&limit=5\"`)" class="mono text-xs px-3 py-1 transition-all" style="color:#1e2840;border:1px solid var(--border);" onmouseover="this.style.color='var(--purple)';this.style.borderColor='var(--purple)'" onmouseout="this.style.color='#1e2840';this.style.borderColor='var(--border)'">COPY CURL</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- /api/scraper/all -->
|
|
<div class="endpoint-row">
|
|
<span class="tag tag-green flex-shrink-0" style="margin-top:2px;">GET</span>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="mono text-sm" style="color:#e2e8f0;">/api/scraper/all</div>
|
|
<div class="text-xs mt-1" style="color:#3a4a60;">Scrape dari semua sumber sekaligus secara paralel.</div>
|
|
<div class="flex flex-wrap gap-3 mt-3 mb-2">
|
|
<span class="mono text-xs"><span class="tag tag-yellow">q</span> <span style="color:#3a4a60;">string · wajib</span></span>
|
|
<span class="mono text-xs"><span class="tag tag-yellow">limit</span> <span style="color:#3a4a60;">per sumber · default 5</span></span>
|
|
</div>
|
|
<div class="code-block"><span class="url">GET [BASE_URL]/api/scraper/all?q=banjir&limit=3</span>
|
|
|
|
{
|
|
<span class="str">"status"</span>: <span class="kw">true</span>,
|
|
<span class="str">"total"</span>: <span class="num">28</span>,
|
|
<span class="str">"sources"</span>: [
|
|
{ <span class="str">"source"</span>: <span class="str">"kompas"</span>, <span class="str">"total"</span>: <span class="num">3</span>, <span class="str">"ok"</span>: <span class="kw">true</span> },
|
|
{ <span class="str">"source"</span>: <span class="str">"detik"</span>, <span class="str">"total"</span>: <span class="num">3</span>, <span class="str">"ok"</span>: <span class="kw">true</span> }
|
|
],
|
|
<span class="str">"data"</span>: [ ... ]
|
|
}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- /api/scraper/list -->
|
|
<div class="endpoint-row">
|
|
<span class="tag tag-green flex-shrink-0" style="margin-top:2px;">GET</span>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="mono text-sm" style="color:#e2e8f0;">/api/scraper/list</div>
|
|
<div class="text-xs mt-1 mb-3" style="color:#3a4a60;">Daftar semua scraper yang tersedia.</div>
|
|
<div class="code-block"><span class="url">GET [BASE_URL]/api/scraper/list</span>
|
|
|
|
{ <span class="str">"status"</span>: <span class="kw">true</span>, <span class="str">"scrapers"</span>: [<span class="str">"kompas"</span>, <span class="str">"detik"</span>, ...] }</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Code examples -->
|
|
<div class="space-y-3 pt-2">
|
|
<div class="section-label text-xs" style="color:#2d3d55;">USAGE EXAMPLES</div>
|
|
|
|
<div class="mono text-xs mb-1" style="color:var(--yellow);">// JavaScript</div>
|
|
<div class="code-block"><span class="kw">const</span> res = <span class="kw">await</span> fetch(<span class="str">'[BASE_URL]/api/scraper?site=detik&q=teknologi&limit=10'</span>)
|
|
<span class="kw">const</span> data = <span class="kw">await</span> res.json()
|
|
console.log(data.data) <span class="cm">// array of articles</span></div>
|
|
|
|
<div class="mono text-xs mb-1 mt-3" style="color:var(--yellow);">// Node.js (axios)</div>
|
|
<div class="code-block"><span class="kw">const</span> { data } = <span class="kw">await</span> axios.get(<span class="str">'[BASE_URL]/api/scraper'</span>, {
|
|
params: { site: <span class="str">'tempo'</span>, q: <span class="str">'ekonomi'</span>, limit: <span class="num">10</span> }
|
|
})</div>
|
|
|
|
<div class="mono text-xs mb-1 mt-3" style="color:var(--yellow);">// cURL</div>
|
|
<div class="code-block">curl <span class="str">"[BASE_URL]/api/scraper?site=antara&q=politik"</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ SOURCES SECTION ═══════════ -->
|
|
<div id="section-sources" class="tab-content">
|
|
<div class="neon-card neon-card-green corner p-4 sm:p-5 space-y-4">
|
|
<div class="section-label" style="color:var(--green);">
|
|
<div class="w-1 h-5 flex-shrink-0" style="background:var(--green);box-shadow:0 0 8px var(--green);"></div>
|
|
DATA_SOURCES
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--cyan);box-shadow:0 0 5px var(--cyan);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Kompas</div><div class="mono text-xs" style="color:#2d3d55;">kompas</div></div>
|
|
<span class="tag tag-cyan">Nasional</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--pink);box-shadow:0 0 5px var(--pink);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Detik</div><div class="mono text-xs" style="color:#2d3d55;">detik</div></div>
|
|
<span class="tag tag-pink">Nasional</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--yellow);box-shadow:0 0 5px var(--yellow);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">CNN Indonesia</div><div class="mono text-xs" style="color:#2d3d55;">cnnindonesia</div></div>
|
|
<span class="tag tag-yellow">Nasional</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--green);box-shadow:0 0 5px var(--green);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Liputan6</div><div class="mono text-xs" style="color:#2d3d55;">liputan6</div></div>
|
|
<span class="tag tag-green">Nasional</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--purple);box-shadow:0 0 5px var(--purple);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Tribunnews</div><div class="mono text-xs" style="color:#2d3d55;">tribun</div></div>
|
|
<span class="tag tag-purple">Daerah</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--cyan);box-shadow:0 0 5px var(--cyan);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Tempo</div><div class="mono text-xs" style="color:#2d3d55;">tempo</div></div>
|
|
<span class="tag tag-cyan">Investigasi</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--pink);box-shadow:0 0 5px var(--pink);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Republika</div><div class="mono text-xs" style="color:#2d3d55;">republika</div></div>
|
|
<span class="tag tag-pink">Umum</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--green);box-shadow:0 0 5px var(--green);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Antara</div><div class="mono text-xs" style="color:#2d3d55;">antara</div></div>
|
|
<span class="tag tag-green">Negara</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--yellow);box-shadow:0 0 5px var(--yellow);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Okezone</div><div class="mono text-xs" style="color:#2d3d55;">okezone</div></div>
|
|
<span class="tag tag-yellow">Hiburan</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--purple);box-shadow:0 0 5px var(--purple);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Sindonews</div><div class="mono text-xs" style="color:#2d3d55;">sindonews</div></div>
|
|
<span class="tag tag-purple">Nasional</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--cyan);box-shadow:0 0 5px var(--cyan);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Merdeka</div><div class="mono text-xs" style="color:#2d3d55;">merdeka</div></div>
|
|
<span class="tag tag-cyan">Umum</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 p-3" style="background:rgba(0,0,0,.2);border:1px solid var(--border);">
|
|
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background:var(--pink);box-shadow:0 0 5px var(--pink);"></div>
|
|
<div class="flex-1 min-w-0"><div class="font-semibold text-sm" style="color:#b0bcd4;">Kumparan</div><div class="mono text-xs" style="color:#2d3d55;">kumparan</div></div>
|
|
<span class="tag tag-pink">Digital</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
// ── base url auto-detect ──────────────────────────────────────────────────
|
|
const BASE_URL = window.location.origin
|
|
|
|
// inject base URL into docs display + code snippets
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const el = document.getElementById('baseUrlDisplay')
|
|
if (el) el.textContent = BASE_URL
|
|
|
|
// replace all [BASE_URL] placeholders in code-blocks
|
|
document.querySelectorAll('.code-block').forEach(block => {
|
|
block.innerHTML = block.innerHTML.replace(/\[BASE_URL\]/g, BASE_URL)
|
|
})
|
|
})
|
|
|
|
// clock
|
|
function updateClock() {
|
|
const now = new Date()
|
|
document.getElementById('clock').textContent = now.toLocaleTimeString('id-ID',{hour12:false})
|
|
document.getElementById('statusDate').textContent = now.toLocaleDateString('id-ID',{day:'2-digit',month:'short'})
|
|
}
|
|
setInterval(updateClock, 1000)
|
|
updateClock()
|
|
|
|
// nav
|
|
function switchSection(name, btn) {
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'))
|
|
document.querySelectorAll('.nav-link').forEach(el => el.classList.remove('active'))
|
|
document.getElementById('section-'+name).classList.add('active')
|
|
btn.classList.add('active')
|
|
}
|
|
|
|
// state
|
|
let currentScraper = 'kompas'
|
|
let lastResults = []
|
|
|
|
function selectScraper(btn) {
|
|
document.querySelectorAll('.scraper-tab').forEach(t => t.classList.remove('active'))
|
|
btn.classList.add('active')
|
|
currentScraper = btn.dataset.scraper
|
|
}
|
|
|
|
function showState(state) {
|
|
const ids = ['idleState','loadingState','errorState','resultsList']
|
|
ids.forEach(id => {
|
|
const el = document.getElementById(id)
|
|
if (id === state) {
|
|
el.style.display = id === 'loadingState' ? 'flex' : (id === 'resultsList' ? 'block' : 'flex')
|
|
el.classList.remove('hidden')
|
|
} else {
|
|
el.style.display = 'none'
|
|
el.classList.add('hidden')
|
|
}
|
|
})
|
|
}
|
|
|
|
// run
|
|
async function runScraper() {
|
|
const query = document.getElementById('queryInput').value.trim()
|
|
if (!query) { document.getElementById('queryInput').focus(); return }
|
|
|
|
const runBtn = document.getElementById('runBtn')
|
|
const limit = document.getElementById('limitRange').value
|
|
runBtn.disabled = true
|
|
|
|
const isAll = currentScraper === 'all'
|
|
showState('loadingState')
|
|
document.getElementById('loadingText').textContent = isAll ? 'SCRAPING ALL SOURCES...' : `SCRAPING ${currentScraper.toUpperCase()}...`
|
|
document.getElementById('loadingSub').textContent = isAll ? 'running parallel requests' : `accessing ${currentScraper}.com`
|
|
document.getElementById('resultCount').textContent = '-- results'
|
|
document.getElementById('lastTarget').textContent = currentScraper.toUpperCase()
|
|
document.getElementById('execTime').textContent = '--ms'
|
|
|
|
const t0 = Date.now()
|
|
try {
|
|
const endpoint = isAll
|
|
? `/api/scraper/all?q=${encodeURIComponent(query)}&limit=${limit}`
|
|
: `/api/scraper?site=${encodeURIComponent(currentScraper)}&q=${encodeURIComponent(query)}&limit=${limit}`
|
|
|
|
const res = await fetch(endpoint)
|
|
const data = await res.json()
|
|
const ms = Date.now() - t0
|
|
|
|
document.getElementById('execTime').textContent = `${ms}ms`
|
|
if (!data.status) throw new Error(data.error || 'scrape failed')
|
|
|
|
lastResults = data.data || []
|
|
renderResults(lastResults)
|
|
document.getElementById('resultCount').textContent = `${lastResults.length} results`
|
|
if (isAll && data.sources) {
|
|
const ok = data.sources.filter(s=>s.ok).length
|
|
document.getElementById('lastTarget').textContent = `${ok}/${data.sources.length} OK`
|
|
}
|
|
} catch(err) {
|
|
showState('errorState')
|
|
document.getElementById('errorMsg').textContent = err.message
|
|
document.getElementById('execTime').textContent = `${Date.now()-t0}ms`
|
|
} finally {
|
|
runBtn.disabled = false
|
|
}
|
|
}
|
|
|
|
// render
|
|
function renderResults(results) {
|
|
const list = document.getElementById('resultsList')
|
|
const showTime = document.getElementById('chkTime').checked
|
|
const showSrc = document.getElementById('chkSource').checked
|
|
const showThumb = document.getElementById('chkThumb').checked
|
|
list.innerHTML = ''
|
|
|
|
if (!results.length) {
|
|
list.innerHTML = `<div class="mono text-xs text-center py-12" style="color:#1a2535;">NO RESULTS FOUND</div>`
|
|
showState('resultsList')
|
|
return
|
|
}
|
|
|
|
results.forEach((item,i) => {
|
|
const div = document.createElement('div')
|
|
div.className = 'result-item'
|
|
div.style.animationDelay = `${i*30}ms`
|
|
const thumb = showThumb && item.thumb
|
|
? `<img src="${item.thumb}" alt="" style="width:56px;height:40px;object-fit:cover;border:1px solid var(--border2);flex-shrink:0;" onerror="this.remove()">`
|
|
: ''
|
|
div.innerHTML = `
|
|
<div style="display:flex;align-items:flex-start;gap:10px;">
|
|
${thumb}
|
|
<div style="flex:1;min-width:0;">
|
|
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;flex-wrap:wrap;">
|
|
<span class="mono" style="font-size:.62rem;color:rgba(0,245,255,.35);">[${String(i+1).padStart(2,'0')}]</span>
|
|
${showSrc&&item.source?`<span class="tag tag-pink">${item.source}</span>`:''}
|
|
${showTime&&item.time?`<span class="tag tag-yellow">${item.time}</span>`:''}
|
|
</div>
|
|
<a href="${item.link||'#'}" target="_blank" rel="noopener"
|
|
style="color:#c8d4e8;font-family:'Rajdhani',sans-serif;font-weight:600;font-size:.9rem;display:block;line-height:1.3;text-decoration:none;"
|
|
onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">
|
|
${item.title||'(no title)'}
|
|
</a>
|
|
${item.link?`<div class="mono" style="font-size:.6rem;margin-top:3px;color:#1a2840;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${item.link}</div>`:''}
|
|
</div>
|
|
<button onclick="copyItem(${i})" class="mono" style="font-size:.65rem;padding:4px 8px;color:#1a2535;border:1px solid var(--border);cursor:pointer;background:transparent;flex-shrink:0;transition:all .2s;"
|
|
onmouseover="this.style.color='var(--cyan)';this.style.borderColor='var(--cyan)'"
|
|
onmouseout="this.style.color='#1a2535';this.style.borderColor='var(--border)'">⧉</button>
|
|
</div>`
|
|
list.appendChild(div)
|
|
})
|
|
showState('resultsList')
|
|
}
|
|
|
|
function clearResults() {
|
|
lastResults = []
|
|
showState('idleState')
|
|
document.getElementById('resultCount').textContent = '-- results'
|
|
document.getElementById('lastTarget').textContent = 'NO_TARGET'
|
|
document.getElementById('execTime').textContent = '--ms'
|
|
document.getElementById('queryInput').value = ''
|
|
}
|
|
|
|
function copyResults() {
|
|
if (!lastResults.length) return
|
|
navigator.clipboard.writeText(JSON.stringify(lastResults,null,2)).then(()=>toast('JSON COPIED'))
|
|
}
|
|
function copyItem(i) {
|
|
navigator.clipboard.writeText(JSON.stringify(lastResults[i],null,2)).then(()=>toast('COPIED'))
|
|
}
|
|
function tryCopy(str) {
|
|
navigator.clipboard.writeText(str).then(()=>toast('SNIPPET COPIED'))
|
|
}
|
|
function exportCSV() {
|
|
if (!lastResults.length) return
|
|
const rows = lastResults.map(r=>[r.title,r.link,r.time,r.source].map(v=>`"${(v||'').replace(/"/g,'""')}"`).join(','))
|
|
const csv = ['title,link,time,source',...rows].join('\n')
|
|
const a = Object.assign(document.createElement('a'),{href:URL.createObjectURL(new Blob([csv],{type:'text/csv'})),download:'scrpr.csv'})
|
|
a.click(); toast('CSV EXPORTED')
|
|
}
|
|
function toast(msg) {
|
|
const el = document.createElement('div')
|
|
el.className = 'toast-msg'
|
|
el.textContent = `✓ ${msg}`
|
|
document.body.appendChild(el)
|
|
setTimeout(()=>el.remove(),2000)
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|