|
|
<!DOCTYPE html> |
|
|
<html lang="vi" class="scroll-smooth"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
|
|
<title>Crypto Intel Fortress — Offline-First & Bulletproof</title> |
|
|
<meta name="description" content="Real-time Crypto Security & On-chain Monitor."> |
|
|
|
|
|
<meta name="referrer" content="no-referrer-when-downgrade"> |
|
|
|
|
|
|
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛡️</text></svg>"> |
|
|
|
|
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script> |
|
|
tailwind.config = { |
|
|
darkMode: 'class', |
|
|
theme: { |
|
|
extend: { |
|
|
fontFamily: { |
|
|
sans: ['Inter', 'system-ui', 'sans-serif'], |
|
|
mono: ['JetBrains Mono', 'monospace'], |
|
|
}, |
|
|
animation: { |
|
|
'fade-in': 'fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1)', |
|
|
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite', |
|
|
}, |
|
|
keyframes: { |
|
|
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } } |
|
|
}, |
|
|
colors: { |
|
|
'fortress-bg': '#020617', |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
</script> |
|
|
|
|
|
|
|
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> |
|
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> |
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
|
|
|
|
|
<style> |
|
|
body { background-color: #000000; color: #e2e8f0; } |
|
|
::-webkit-scrollbar { width: 6px; height: 6px; } |
|
|
::-webkit-scrollbar-track { background: #020617; } |
|
|
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; } |
|
|
::-webkit-scrollbar-thumb:hover { background: #475569; } |
|
|
.glass-panel { |
|
|
background: rgba(15, 23, 42, 0.6); |
|
|
backdrop-filter: blur(16px); |
|
|
-webkit-backdrop-filter: blur(16px); |
|
|
border: 1px solid rgba(255, 255, 255, 0.05); |
|
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
.no-scrollbar::-webkit-scrollbar { display: none; } |
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } |
|
|
|
|
|
|
|
|
.twitter-container iframe { |
|
|
width: 100% !important; |
|
|
height: 100% !important; |
|
|
border-radius: 8px; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="min-h-screen bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-slate-900 via-black to-black selection:bg-cyan-500/30 selection:text-cyan-200"> |
|
|
<div id="root"></div> |
|
|
|
|
|
<script type="text/babel"> |
|
|
const { useState, useEffect, useRef, useMemo } = React; |
|
|
|
|
|
|
|
|
const Icon = ({ children, size = 20, className = "" }) => ( |
|
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
|
|
{children} |
|
|
</svg> |
|
|
); |
|
|
|
|
|
const icons = { |
|
|
shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/>, |
|
|
search: <><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></>, |
|
|
trash: <><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></>, |
|
|
wifi: <path d="M5 12.55a11 11 0 0 1 14.08 0M1.42 9a16 16 0 0 1 21.16 0M8.53 16.11a6 6 0 0 1 6.95 0M12 20h.01"/>, |
|
|
wifiOff: <><line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a11 11 0 0 1 7.67-9.77"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><path d="M12 20h.01"/></>, |
|
|
external: <><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></>, |
|
|
plus: <><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>, |
|
|
alert: <><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></> |
|
|
}; |
|
|
|
|
|
|
|
|
const DEFAULT_ACCOUNTS = [ |
|
|
{ handle: "PeckShieldAlert", name: "PeckShield", cat: "Security" }, |
|
|
{ handle: "realScamSniffer", name: "Scam Sniffer", cat: "Security" }, |
|
|
{ handle: "ZachXBT", name: "ZachXBT", cat: "Security" }, |
|
|
{ handle: "CertiKAlert", name: "CertiK", cat: "Security" }, |
|
|
{ handle: "lookonchain", name: "Lookonchain", cat: "On-chain" }, |
|
|
{ handle: "whale_alert", name: "Whale Alert", cat: "On-chain" }, |
|
|
{ handle: "ArkhamIntel", name: "Arkham", cat: "On-chain" }, |
|
|
{ handle: "WuBlockchain", name: "Wu Blockchain", cat: "News" }, |
|
|
{ handle: "Tier10k", name: "DB (Tier10k)", cat: "News" }, |
|
|
{ handle: "DeFiLlama", name: "DeFiLlama", cat: "Data" } |
|
|
]; |
|
|
|
|
|
const CATEGORIES = ["All", "Security", "On-chain", "News", "Data", "Custom"]; |
|
|
const STORAGE_KEY = 'CRYPTO_FORTRESS_V70'; |
|
|
|
|
|
|
|
|
|
|
|
const Skeleton = () => ( |
|
|
<div className="w-full h-full p-4 flex flex-col gap-4 animate-pulse-slow"> |
|
|
<div className="flex gap-3"> |
|
|
<div className="w-10 h-10 rounded-full bg-slate-800/50"></div> |
|
|
<div className="flex-1 space-y-2"> |
|
|
<div className="h-3 bg-slate-800/50 rounded w-1/3"></div> |
|
|
<div className="h-2 bg-slate-800/30 rounded w-1/4"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div className="flex-1 bg-slate-800/20 rounded-lg border border-slate-800/30"></div> |
|
|
</div> |
|
|
); |
|
|
|
|
|
|
|
|
const TwitterCard = React.memo(({ account, onRemove, isOnline }) => { |
|
|
const containerRef = useRef(null); |
|
|
const [status, setStatus] = useState('loading'); |
|
|
|
|
|
useEffect(() => { |
|
|
if (!isOnline) { |
|
|
setStatus('offline'); |
|
|
return; |
|
|
} |
|
|
|
|
|
setStatus('loading'); |
|
|
|
|
|
let isMounted = true; |
|
|
const container = containerRef.current; |
|
|
|
|
|
const initWidget = () => { |
|
|
if (!window.twttr || !window.twttr.widgets) { |
|
|
setTimeout(initWidget, 500); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!container) return; |
|
|
container.innerHTML = ''; |
|
|
|
|
|
|
|
|
window.twttr.widgets.createTimeline( |
|
|
{ |
|
|
sourceType: 'profile', |
|
|
screenName: account.handle |
|
|
}, |
|
|
container, |
|
|
{ |
|
|
theme: 'dark', |
|
|
height: 400, |
|
|
chrome: 'noheader,nofooter,noborders,transparent,noscrollbar', |
|
|
dnt: true |
|
|
} |
|
|
).then((el) => { |
|
|
if (isMounted) { |
|
|
if (el) { |
|
|
setStatus('loaded'); |
|
|
} else { |
|
|
|
|
|
setStatus('error'); |
|
|
} |
|
|
} |
|
|
}).catch(() => { |
|
|
if (isMounted) setStatus('error'); |
|
|
}); |
|
|
}; |
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver((entries) => { |
|
|
if (entries[0].isIntersecting) { |
|
|
initWidget(); |
|
|
observer.disconnect(); |
|
|
} |
|
|
}, { rootMargin: '100px' }); |
|
|
|
|
|
if (container) observer.observe(container); |
|
|
|
|
|
|
|
|
const timer = setTimeout(() => { |
|
|
if (isMounted && status === 'loading') { |
|
|
setStatus('error'); |
|
|
} |
|
|
}, 4000); |
|
|
|
|
|
return () => { |
|
|
isMounted = false; |
|
|
observer.disconnect(); |
|
|
clearTimeout(timer); |
|
|
}; |
|
|
}, [account.handle, isOnline]); |
|
|
|
|
|
const colorMap = { |
|
|
Security: "text-red-400 border-red-500/30 bg-red-500/10", |
|
|
"On-chain": "text-cyan-400 border-cyan-500/30 bg-cyan-500/10", |
|
|
News: "text-emerald-400 border-emerald-500/30 bg-emerald-500/10", |
|
|
Data: "text-purple-400 border-purple-500/30 bg-purple-500/10", |
|
|
Custom: "text-amber-400 border-amber-500/30 bg-amber-500/10" |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="glass-panel rounded-2xl overflow-hidden flex flex-col h-[450px] group transition-all duration-300 hover:border-slate-600 hover:shadow-2xl hover:shadow-cyan-900/10 animate-fade-in relative"> |
|
|
|
|
|
{/* Card Header */} |
|
|
<div className="p-4 border-b border-white/5 bg-slate-900/50 flex justify-between items-center z-10 shrink-0"> |
|
|
<div className="flex items-center gap-3 overflow-hidden"> |
|
|
<div className={`text-[10px] font-mono font-bold px-2 py-0.5 rounded border uppercase tracking-wider ${colorMap[account.cat] || colorMap.Custom}`}> |
|
|
{account.cat.substring(0, 3)} |
|
|
</div> |
|
|
<div className="flex flex-col min-w-0"> |
|
|
<span className="font-bold text-slate-100 truncate text-sm leading-tight">{account.name}</span> |
|
|
<a href={`https://x.com/${account.handle}`} target="_blank" rel="noopener noreferrer" className="text-[11px] text-slate-500 hover:text-cyan-400 truncate flex items-center gap-1 transition-colors font-mono"> |
|
|
@{account.handle} <Icon size={10}>{icons.external}</Icon> |
|
|
</a> |
|
|
</div> |
|
|
</div> |
|
|
<button onClick={() => onRemove(account.handle)} className="text-slate-600 hover:text-red-400 p-2 rounded-lg hover:bg-white/5 transition-all opacity-0 group-hover:opacity-100 focus:opacity-100" title="Remove Feed"> |
|
|
<Icon size={16}>{icons.trash}</Icon> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
{/* Card Body */} |
|
|
<div className="flex-1 relative bg-black/20 overflow-hidden"> |
|
|
{/* Twitter Container */} |
|
|
<div ref={containerRef} className={`w-full h-full overflow-y-auto twitter-container scrollbar-thin ${status === 'loaded' ? 'opacity-100' : 'opacity-0'} transition-opacity duration-500`}></div> |
|
|
|
|
|
{status === 'loading' && ( |
|
|
<div className="absolute inset-0 pointer-events-none"> |
|
|
<Skeleton /> |
|
|
</div> |
|
|
)} |
|
|
|
|
|
{(status === 'error' || status === 'offline') && ( |
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center bg-slate-900/90 backdrop-blur-sm z-20"> |
|
|
<div className="p-3 bg-slate-800 rounded-full mb-3 text-slate-400"> |
|
|
<Icon size={24}>{status === 'offline' ? icons.wifiOff : icons.alert}</Icon> |
|
|
</div> |
|
|
<span className="text-slate-300 text-sm font-bold mb-1"> |
|
|
{status === 'offline' ? 'Offline' : 'Feed Unavailable'} |
|
|
</span> |
|
|
<p className="text-xs text-slate-500 mb-4 max-w-[200px]"> |
|
|
{status === 'offline' |
|
|
? 'Reconnect to internet.' |
|
|
: 'Twitter restricts embedding in this environment (HF/AdBlock).'} |
|
|
</p> |
|
|
<a href={`https://x.com/${account.handle}`} target="_blank" rel="noopener noreferrer" className="px-5 py-2 bg-slate-800 hover:bg-cyan-600 hover:text-black text-cyan-400 text-xs font-bold font-mono rounded-lg transition-all border border-cyan-500/30 flex items-center gap-2"> |
|
|
OPEN ON X.COM <Icon size={12}>{icons.external}</Icon> |
|
|
</a> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}); |
|
|
|
|
|
|
|
|
const App = () => { |
|
|
const [accounts, setAccounts] = useState(() => { |
|
|
try { |
|
|
const saved = localStorage.getItem(STORAGE_KEY); |
|
|
return saved ? JSON.parse(saved) : DEFAULT_ACCOUNTS; |
|
|
} catch { return DEFAULT_ACCOUNTS; } |
|
|
}); |
|
|
const [activeCategory, setActiveCategory] = useState("All"); |
|
|
const [input, setInput] = useState(""); |
|
|
const [online, setOnline] = useState(navigator.onLine); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
window.twttr = (function(d, s, id) { |
|
|
var js, fjs = d.getElementsByTagName(s)[0], |
|
|
t = window.twttr || {}; |
|
|
if (d.getElementById(id)) return t; |
|
|
js = d.createElement(s); |
|
|
js.id = id; |
|
|
js.src = "https://platform.twitter.com/widgets.js"; |
|
|
fjs.parentNode.insertBefore(js, fjs); |
|
|
t._e = []; |
|
|
t.ready = function(f) { |
|
|
t._e.push(f); |
|
|
}; |
|
|
return t; |
|
|
}(document, "script", "twitter-wjs")); |
|
|
|
|
|
const updateOnline = () => setOnline(navigator.onLine); |
|
|
window.addEventListener('online', updateOnline); |
|
|
window.addEventListener('offline', updateOnline); |
|
|
return () => { |
|
|
window.removeEventListener('online', updateOnline); |
|
|
window.removeEventListener('offline', updateOnline); |
|
|
}; |
|
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(accounts)); |
|
|
}, [accounts]); |
|
|
|
|
|
const addAccount = (e) => { |
|
|
e.preventDefault(); |
|
|
if (!input.trim()) return; |
|
|
let handle = input.trim(); |
|
|
|
|
|
try { |
|
|
const url = new URL(handle); |
|
|
handle = url.pathname.split('/').filter(Boolean).pop(); |
|
|
} catch (e) {} |
|
|
|
|
|
handle = handle.replace('@', '').replace('?', '').split('/')[0]; |
|
|
|
|
|
if (accounts.some(a => a.handle.toLowerCase() === handle.toLowerCase())) { |
|
|
alert("Target already monitored."); |
|
|
return; |
|
|
} |
|
|
|
|
|
setAccounts([{ handle, name: handle, cat: "Custom" }, ...accounts]); |
|
|
setInput(""); |
|
|
}; |
|
|
|
|
|
const filteredAccounts = useMemo(() => { |
|
|
if (activeCategory === "All") return accounts; |
|
|
return accounts.filter(acc => acc.cat === activeCategory); |
|
|
}, [accounts, activeCategory]); |
|
|
|
|
|
return ( |
|
|
<div className="min-h-screen flex flex-col font-sans pb-10"> |
|
|
{/* HEADER */} |
|
|
<header className="sticky top-0 z-50 glass-panel border-t-0 border-x-0"> |
|
|
<div className="max-w-[1920px] mx-auto"> |
|
|
<div className="px-4 py-4 flex flex-col md:flex-row md:items-center justify-between gap-4"> |
|
|
<div className="flex items-center gap-4"> |
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-600 to-blue-700 flex items-center justify-center shadow-lg shadow-cyan-900/20"> |
|
|
<Icon size={24} className="text-white">{icons.shield}</Icon> |
|
|
</div> |
|
|
<div> |
|
|
<h1 className="text-xl font-black tracking-tight text-white uppercase flex items-center gap-2"> |
|
|
Crypto Fortress <span className="text-[10px] bg-slate-800 text-slate-400 px-1.5 py-0.5 rounded border border-slate-700">HF-Fixed</span> |
|
|
</h1> |
|
|
<p className="text-xs text-slate-500 font-mono">Real-time Intel Command Center</p> |
|
|
</div> |
|
|
</div> |
|
|
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-bold font-mono transition-colors ${online ? 'bg-emerald-950/30 border-emerald-500/30 text-emerald-400' : 'bg-red-950/30 border-red-500/30 text-red-400'}`}> |
|
|
<Icon size={14}>{online ? icons.wifi : icons.wifiOff}</Icon> |
|
|
<span>{online ? 'ONLINE' : 'OFFLINE'}</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className="border-t border-white/5 bg-black/20 px-4 py-2 flex flex-col md:flex-row gap-3 justify-between items-center"> |
|
|
<div className="flex gap-2 overflow-x-auto no-scrollbar w-full md:w-auto pb-1 md:pb-0"> |
|
|
{CATEGORIES.map(cat => ( |
|
|
<button |
|
|
key={cat} |
|
|
onClick={() => setActiveCategory(cat)} |
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold font-mono transition-all whitespace-nowrap border ${ |
|
|
activeCategory === cat |
|
|
? 'bg-slate-800 text-cyan-400 border-cyan-500/50' |
|
|
: 'border-transparent text-slate-500 hover:text-slate-300 hover:bg-white/5' |
|
|
}`} |
|
|
> |
|
|
{cat.toUpperCase()} |
|
|
</button> |
|
|
))} |
|
|
</div> |
|
|
<form onSubmit={addAccount} className="flex w-full md:w-auto relative group"> |
|
|
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-slate-600"> |
|
|
<Icon size={14}>{icons.plus}</Icon> |
|
|
</div> |
|
|
<input |
|
|
type="text" |
|
|
value={input} |
|
|
onChange={(e) => setInput(e.target.value)} |
|
|
placeholder="Add target handle..." |
|
|
className="w-full md:w-64 bg-slate-900/50 border border-slate-700 text-slate-200 text-xs rounded-l-lg pl-9 pr-3 py-2 focus:outline-none focus:border-cyan-500 font-mono transition-all" |
|
|
/> |
|
|
<button className="bg-slate-800 hover:bg-cyan-700 text-white px-4 py-2 rounded-r-lg text-xs font-bold border border-l-0 border-slate-700"> |
|
|
ADD |
|
|
</button> |
|
|
</form> |
|
|
</div> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
{/* MAIN GRID */} |
|
|
<main className="flex-1 p-4 md:p-6 w-full max-w-[1920px] mx-auto"> |
|
|
{filteredAccounts.length > 0 ? ( |
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6"> |
|
|
{filteredAccounts.map(acc => ( |
|
|
<TwitterCard |
|
|
key={acc.handle} |
|
|
account={acc} |
|
|
onRemove={(h) => setAccounts(accounts.filter(a => a.handle !== h))} |
|
|
isOnline={online} |
|
|
/> |
|
|
))} |
|
|
</div> |
|
|
) : ( |
|
|
<div className="flex flex-col items-center justify-center h-[50vh] text-slate-600"> |
|
|
<div className="p-4 bg-slate-900 rounded-full mb-4"> |
|
|
<Icon size={48} className="opacity-50">{icons.search}</Icon> |
|
|
</div> |
|
|
<h2 className="text-xl font-bold text-slate-500">No Intelligence Found</h2> |
|
|
</div> |
|
|
)} |
|
|
</main> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root')); |
|
|
root.render(<App />); |
|
|
</script> |
|
|
</body> |
|
|
</html> |