scraper/views/scraper.ejs
2026-04-21 19:21:04 +07:00

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 &nbsp;·&nbsp; 12 INDONESIAN SOURCES &nbsp;·&nbsp; 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;">&gt;_</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>