Snapshot: full project state
This commit is contained in:
289
static/index.html
Normal file
289
static/index.html
Normal file
@@ -0,0 +1,289 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment Recon — Search Engine</title>
|
||||
<style>
|
||||
:root{--bg:#06060e;--card:#0e0e1a;--accent:#f7931a;--accent2:#00d4aa;--accent3:#6c63ff;--text:#e0e0e0;--muted:#777;--danger:#ff4757;--success:#2ed573;--warning:#ffa502;--radius:12px}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
|
||||
body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(circle at 30% 20%,rgba(108,99,255,0.03) 0%,transparent 50%),radial-gradient(circle at 70% 80%,rgba(247,147,26,0.03) 0%,transparent 50%);pointer-events:none;z-index:0}
|
||||
|
||||
.app{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:1rem}
|
||||
|
||||
/* Header */
|
||||
header{text-align:center;padding:2rem 1rem 1.5rem}
|
||||
header h1{font-size:2rem;font-weight:800;background:linear-gradient(135deg,var(--accent),var(--accent2),var(--accent3));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-0.5px}
|
||||
header p{color:var(--muted);font-size:0.85rem;margin-top:0.3rem}
|
||||
|
||||
/* Search Bar */
|
||||
.search-bar{background:var(--card);border-radius:var(--radius);padding:0.7rem;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;border:1px solid rgba(255,255,255,0.06);margin-bottom:1.5rem;position:sticky;top:1rem;z-index:10;backdrop-filter:blur(10px)}
|
||||
.search-bar select,.search-bar button{background:rgba(0,0,0,0.4);color:var(--text);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:0.5rem 0.8rem;font-size:0.78rem;cursor:pointer}
|
||||
.search-bar select{min-width:150px}
|
||||
.search-bar select:hover,.search-bar button:hover{border-color:var(--accent)}
|
||||
.search-bar .btn-search{background:linear-gradient(135deg,var(--accent),#e6730d);color:#000;border:none;font-weight:700;padding:0.5rem 1.5rem;font-size:0.82rem}
|
||||
.search-bar .btn-search:hover{filter:brightness(1.15)}
|
||||
.search-bar .btn-search:disabled{opacity:0.5;cursor:not-allowed}
|
||||
.search-bar .btn-scan{background:linear-gradient(135deg,var(--accent3),#8b5cf6);color:#fff;border:none;font-weight:700;padding:0.5rem 1.2rem;font-size:0.82rem}
|
||||
|
||||
/* Stats Bar */
|
||||
.stats-bar{display:flex;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;font-size:0.75rem;color:var(--muted)}
|
||||
.stats-bar strong{color:var(--accent2)}
|
||||
.stat-pill{background:var(--card);padding:0.3rem 0.7rem;border-radius:20px;border:1px solid rgba(255,255,255,0.05);font-size:0.68rem}
|
||||
|
||||
/* Results */
|
||||
.results-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:0.8rem}
|
||||
.site-card{background:var(--card);border-radius:var(--radius);overflow:hidden;border:1px solid rgba(255,255,255,0.05);transition:all 0.25s;display:flex;flex-direction:column}
|
||||
.site-card:hover{transform:translateY(-2px);border-color:rgba(255,255,255,0.12);box-shadow:0 8px 30px rgba(0,0,0,0.3)}
|
||||
.site-card .screenshot{height:180px;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
|
||||
.site-card .screenshot img{width:100%;height:100%;object-fit:cover}
|
||||
.site-card .screenshot .placeholder{font-size:2.5rem;opacity:0.3}
|
||||
.site-card .body{padding:0.9rem;flex:1;display:flex;flex-direction:column}
|
||||
.site-card .domain{font-weight:700;font-size:0.9rem;color:var(--accent2);margin-bottom:0.15rem}
|
||||
.site-card .domain a{color:inherit;text-decoration:none}
|
||||
.site-card .domain a:hover{text-decoration:underline}
|
||||
.site-card .desc{font-size:0.72rem;color:var(--muted);line-height:1.4;margin:0.4rem 0;flex:1}
|
||||
.site-card .tags{margin-top:0.4rem;display:flex;gap:0.3rem;flex-wrap:wrap}
|
||||
.tag{display:inline-block;padding:0.15rem 0.45rem;border-radius:3px;font-size:0.6rem;font-weight:600}
|
||||
.t-type{background:rgba(108,99,255,0.2);color:#a78bfa}
|
||||
.t-industry{background:rgba(247,147,26,0.2);color:var(--accent)}
|
||||
.t-country{background:rgba(0,212,170,0.15);color:var(--accent2);font-size:0.55rem}
|
||||
.t-US{background:rgba(59,130,246,0.2);color:#60a5fa}
|
||||
.t-EU{background:rgba(168,85,247,0.2);color:#a78bfa}
|
||||
.t-INTL{background:rgba(251,191,36,0.2);color:#fbbf24}
|
||||
.t-gambling{background:rgba(255,71,87,0.2);color:var(--danger)}
|
||||
.t-ecommerce{background:rgba(0,212,170,0.2);color:var(--accent2)}
|
||||
.t-adult{background:rgba(247,147,26,0.2);color:var(--accent)}
|
||||
.t-electronics{background:rgba(59,130,246,0.2);color:#60a5fa}
|
||||
.site-card .actions{margin-top:0.5rem;display:flex;gap:0.3rem}
|
||||
.btn-sm{padding:0.25rem 0.6rem;border-radius:4px;border:none;cursor:pointer;font-size:0.65rem;font-weight:600}
|
||||
.btn-visit{background:var(--accent2);color:#000}
|
||||
.btn-visit:hover{filter:brightness(1.2)}
|
||||
.btn-screenshot{background:rgba(108,99,255,0.2);color:#a78bfa;border:1px solid rgba(108,99,255,0.3)}
|
||||
.btn-screenshot:hover{background:rgba(108,99,255,0.3)}
|
||||
|
||||
/* Loader */
|
||||
.loader{text-align:center;padding:2rem}
|
||||
.spinner{display:inline-block;width:30px;height:30px;border:3px solid rgba(255,255,255,0.1);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* Empty */
|
||||
.empty{text-align:center;padding:3rem;color:var(--muted)}
|
||||
.empty .icon{font-size:4rem;margin-bottom:1rem;opacity:0.3}
|
||||
|
||||
/* Responsive */
|
||||
@media(max-width:768px){.results-grid{grid-template-columns:1fr}.search-bar{flex-direction:column;align-items:stretch}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header>
|
||||
<h1>🔍 Payment Recon</h1>
|
||||
<p>Search the web for payment processors. Screenshots. AI descriptions. Organized by industry.</p>
|
||||
</header>
|
||||
|
||||
<div class="search-bar">
|
||||
<select id="processor-select">
|
||||
<option value="">🎯 Select Processor</option>
|
||||
</select>
|
||||
<select id="industry-select">
|
||||
<option value="">🏷️ All Industries</option>
|
||||
</select>
|
||||
<select id="country-select">
|
||||
<option value="US" selected>🇺🇸 US Only</option>
|
||||
<option value="">🌍 All Countries</option>
|
||||
<option value="EU">🇪🇺 Europe</option>
|
||||
<option value="INTL">🌍 International</option>
|
||||
</select>
|
||||
<button class="btn-search" onclick="doSearch()" id="btn-search">🔍 Search</button>
|
||||
<button class="btn-scan" onclick="scanAll()" id="btn-scan">⚡ Scan All Processors</button>
|
||||
<span style="font-size:0.68rem;color:var(--muted);margin-left:auto;" id="status-text">Ready</span>
|
||||
</div>
|
||||
|
||||
<div class="stats-bar" id="stats-bar"></div>
|
||||
<div class="results-grid" id="results"></div>
|
||||
<div class="loader" id="loader" style="display:none"><div class="spinner"></div><p style="margin-top:0.5rem;font-size:0.75rem;color:var(--muted);">Searching...</p></div>
|
||||
|
||||
<div id="screenshot-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:100;align-items:center;justify-content:center;" onclick="this.style.display='none'">
|
||||
<img id="screenshot-img" style="max-width:90vw;max-height:90vh;border-radius:8px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allProcessors = {};
|
||||
let allIndustries = [];
|
||||
let currentResults = [];
|
||||
|
||||
function safeId(str){return 'id-'+btoa(unescape(encodeURIComponent(str))).replace(/[+/=]/g,'')}
|
||||
|
||||
async function init(){
|
||||
const r = await fetch('/api/processors');
|
||||
const data = await r.json();
|
||||
allProcessors = data.processors;
|
||||
|
||||
// Populate processor dropdown
|
||||
const ps = document.getElementById('processor-select');
|
||||
for(const [name, info] of Object.entries(allProcessors)){
|
||||
ps.innerHTML += `<option value="${name}">${name} (${info.type})</option>`;
|
||||
}
|
||||
|
||||
// Populate industry dropdown
|
||||
allIndustries = data.industries;
|
||||
const is = document.getElementById('industry-select');
|
||||
for(const ind of allIndustries){
|
||||
is.innerHTML += `<option value="${ind}">${ind}</option>`;
|
||||
}
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = `
|
||||
<span class="stat-pill"><strong>${Object.keys(allProcessors).length}</strong> processors</span>
|
||||
<span class="stat-pill"><strong>${allIndustries.length}</strong> industries</span>
|
||||
`;
|
||||
}
|
||||
|
||||
async function doSearch(){
|
||||
const proc = document.getElementById('processor-select').value;
|
||||
if(!proc) return alert('Select a processor first');
|
||||
|
||||
const industry = document.getElementById('industry-select').value;
|
||||
const country = document.getElementById('country-select').value;
|
||||
showLoader(true);
|
||||
document.getElementById('status-text').textContent = `Searching for ${proc}...`;
|
||||
|
||||
const url = `/api/search/${encodeURIComponent(proc)}?industry=${encodeURIComponent(industry)}&country=${encodeURIComponent(country)}&max=12`;
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
|
||||
currentResults = data.results || [];
|
||||
renderResults(currentResults, proc);
|
||||
showLoader(false);
|
||||
document.getElementById('status-text').textContent = `${data.result_count} results for ${proc}`;
|
||||
}
|
||||
|
||||
async function scanAll(){
|
||||
if(!confirm('Scan all processors? This may take a minute.')) return;
|
||||
showLoader(true);
|
||||
|
||||
const industry = document.getElementById('industry-select').value;
|
||||
document.getElementById('status-text').textContent = 'Scanning all processors...';
|
||||
|
||||
const url = `/api/search-all?industry=${encodeURIComponent(industry)}&country=${encodeURIComponent(country)}`;
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
|
||||
let all = [];
|
||||
for(const [name, result] of Object.entries(data)){
|
||||
if(result.results){
|
||||
all = all.concat(result.results);
|
||||
}
|
||||
}
|
||||
|
||||
currentResults = all;
|
||||
renderResults(currentResults, 'All Processors');
|
||||
showLoader(false);
|
||||
document.getElementById('status-text').textContent = `${all.length} results across all processors`;
|
||||
}
|
||||
|
||||
function renderResults(results, procName){
|
||||
const grid = document.getElementById('results');
|
||||
if(!results.length){
|
||||
grid.innerHTML = `<div class="empty"><div class="icon">🔍</div><p>No results found for ${procName}.</p><p style="font-size:0.7rem;margin-top:0.2rem;">Try a different processor or industry.</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = results.map(s => {
|
||||
const domain = s.domain || (()=>{try{return new URL(s.url||'').hostname.replace('www.','')}catch{return s.domain||'unknown'}})();
|
||||
const proc = s.processor || procName;
|
||||
const tags = [
|
||||
`<span class="tag t-type">${s.processor_type||''}</span>`,
|
||||
`<span class="tag t-industry t-${s.industry||'ecommerce'}">${s.industry||'ecommerce'}</span>`,
|
||||
`<span class="tag t-country t-${s.country||'US'}">${s.country||'US'}</span>`
|
||||
].join('');
|
||||
|
||||
return `
|
||||
<div class="site-card">
|
||||
<div class="screenshot" id="ss-${safeId(domain)}">
|
||||
<span class="placeholder">🌐</span>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="domain"><a href="${s.url||'https://'+domain}" target="_blank">${domain}</a></div>
|
||||
<div class="desc">${s.snippet||s.description||'Visit site for details.'}</div>
|
||||
<div class="tags">${tags}</div>
|
||||
<div class="actions">
|
||||
<button class="btn-sm btn-visit" onclick="window.open('${s.url||'https://'+domain}','_blank')">Visit →</button>
|
||||
<button class="btn-sm btn-screenshot" onclick="takeScreenshot('${domain}','${s.url||'https://'+domain}','${safeId(domain)}')">📸 Screenshot</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('stats-bar').innerHTML = `
|
||||
<span class="stat-pill"><strong>${results.length}</strong> results</span>
|
||||
<span class="stat-pill">Processor: <strong>${procName}</strong></span>
|
||||
${[...new Set(results.map(r=>r.industry))].map(i=>`<span class="stat-pill">${i}</span>`).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
async function takeScreenshot(domain, url, id){
|
||||
const el = document.getElementById('ss-'+id);
|
||||
el.innerHTML = '<div class="spinner" style="width:20px;height:20px;"></div>';
|
||||
|
||||
try{
|
||||
// Queue analysis job
|
||||
const r = await fetch('/api/analyze', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({sites:[{domain,url}],processor:currentResults[0]?.processor||'Unknown'})
|
||||
});
|
||||
const {job_id} = await r.json();
|
||||
|
||||
// Poll for result
|
||||
let attempts=0;
|
||||
while(attempts<30){
|
||||
await new Promise(r=>setTimeout(r,1000));
|
||||
const jr = await fetch(`/api/job/${job_id}`);
|
||||
const job = await jr.json();
|
||||
if(job.status==='complete'){
|
||||
const result = job.results[0];
|
||||
if(result?.description){
|
||||
// Update description
|
||||
for(const card of document.querySelectorAll('.site-card')){
|
||||
if(card.querySelector('.domain a')?.textContent === domain){
|
||||
card.querySelector('.desc').textContent = result.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(result?.screenshot_base64){
|
||||
el.innerHTML = `<img src="data:image/png;base64,${result.screenshot_base64}" style="width:100%;height:100%;object-fit:cover" onclick="event.stopPropagation();showFullScreenshot('${result.screenshot_base64}')">`;
|
||||
} else {
|
||||
el.innerHTML = '<span class="placeholder">📸</span>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
if(attempts>=30) el.innerHTML = '<span class="placeholder">⏰</span>';
|
||||
}catch(e){
|
||||
el.innerHTML = '<span class="placeholder">❌</span>';
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoader(show){document.getElementById('loader').style.display=show?'block':'none'}
|
||||
|
||||
function showFullScreenshot(b64){
|
||||
const modal = document.getElementById('screenshot-modal');
|
||||
const img = document.getElementById('screenshot-img');
|
||||
img.src = `data:image/png;base64,${b64}`;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
<div style="text-align:center;padding:1rem;margin-top:1.5rem;border-top:1px solid rgba(255,255,255,0.05)">
|
||||
<a href="https://buymeacoffee.com/r26xrthzttg" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;background:linear-gradient(135deg,#FF813F,#FF5E0E);color:#fff;padding:0.45rem 1.1rem;border-radius:25px;text-decoration:none;font-weight:600;font-size:0.75rem;transition:all 0.2s;box-shadow:0 3px 12px rgba(255,94,14,0.25)" onmouseover="this.style.transform='scale(1.04)';this.style.boxShadow='0 5px 18px rgba(255,94,14,0.4)'" onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 3px 12px rgba(255,94,14,0.25)'">
|
||||
<span style="font-size:1rem">☕</span> Support This Project — Buy Me a Coffee
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user