Update index.html
Browse files- index.html +118 -103
index.html
CHANGED
|
@@ -4,7 +4,9 @@
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
<title>Crypto Intel Fortress — Offline-First & Bulletproof</title>
|
| 7 |
-
<meta name="description" content="Real-time Crypto Security & On-chain Monitor.
|
|
|
|
|
|
|
| 8 |
|
| 9 |
<!-- Favicon SVG -->
|
| 10 |
<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>">
|
|
@@ -22,16 +24,13 @@
|
|
| 22 |
},
|
| 23 |
animation: {
|
| 24 |
'fade-in': 'fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
| 25 |
-
'slide-up': 'slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
| 26 |
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 27 |
},
|
| 28 |
keyframes: {
|
| 29 |
-
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } }
|
| 30 |
-
slideUp: { '0%': { opacity: '0', transform: 'translateY(20px)' }, '100%': { opacity: '1', transform: 'translateY(0)' } }
|
| 31 |
},
|
| 32 |
colors: {
|
| 33 |
-
'fortress-bg': '#020617',
|
| 34 |
-
'fortress-card': '#0f172a', // Slate 900
|
| 35 |
}
|
| 36 |
}
|
| 37 |
}
|
|
@@ -45,13 +44,10 @@
|
|
| 45 |
|
| 46 |
<style>
|
| 47 |
body { background-color: #000000; color: #e2e8f0; }
|
| 48 |
-
|
| 49 |
-
/* Custom Scrollbar for the Elite */
|
| 50 |
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 51 |
::-webkit-scrollbar-track { background: #020617; }
|
| 52 |
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
| 53 |
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
| 54 |
-
|
| 55 |
.glass-panel {
|
| 56 |
background: rgba(15, 23, 42, 0.6);
|
| 57 |
backdrop-filter: blur(16px);
|
|
@@ -59,18 +55,24 @@
|
|
| 59 |
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 60 |
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
| 61 |
}
|
| 62 |
-
|
| 63 |
.no-scrollbar::-webkit-scrollbar { display: none; }
|
| 64 |
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</style>
|
| 66 |
</head>
|
| 67 |
<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">
|
| 68 |
<div id="root"></div>
|
| 69 |
|
| 70 |
<script type="text/babel">
|
| 71 |
-
const { useState, useEffect, useRef,
|
| 72 |
|
| 73 |
-
// ================== ICONS
|
| 74 |
const Icon = ({ children, size = 20, className = "" }) => (
|
| 75 |
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
| 76 |
{children}
|
|
@@ -79,7 +81,6 @@
|
|
| 79 |
|
| 80 |
const icons = {
|
| 81 |
shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/>,
|
| 82 |
-
zap: <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>,
|
| 83 |
search: <><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></>,
|
| 84 |
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"/></>,
|
| 85 |
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"/>,
|
|
@@ -89,7 +90,7 @@
|
|
| 89 |
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"/></>
|
| 90 |
};
|
| 91 |
|
| 92 |
-
// ================== DATA
|
| 93 |
const DEFAULT_ACCOUNTS = [
|
| 94 |
{ handle: "PeckShieldAlert", name: "PeckShield", cat: "Security" },
|
| 95 |
{ handle: "realScamSniffer", name: "Scam Sniffer", cat: "Security" },
|
|
@@ -108,7 +109,6 @@
|
|
| 108 |
|
| 109 |
// ================== COMPONENTS ==================
|
| 110 |
|
| 111 |
-
// 1. Skeleton Loading (Fortress Style)
|
| 112 |
const Skeleton = () => (
|
| 113 |
<div className="w-full h-full p-4 flex flex-col gap-4 animate-pulse-slow">
|
| 114 |
<div className="flex gap-3">
|
|
@@ -122,7 +122,7 @@
|
|
| 122 |
</div>
|
| 123 |
);
|
| 124 |
|
| 125 |
-
//
|
| 126 |
const TwitterCard = React.memo(({ account, onRemove, isOnline }) => {
|
| 127 |
const containerRef = useRef(null);
|
| 128 |
const [status, setStatus] = useState('loading'); // loading | loaded | error | offline
|
|
@@ -135,47 +135,68 @@
|
|
| 135 |
|
| 136 |
setStatus('loading');
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
const observer = new IntersectionObserver((entries) => {
|
| 140 |
if (entries[0].isIntersecting) {
|
| 141 |
-
|
| 142 |
observer.disconnect();
|
| 143 |
}
|
| 144 |
-
}, { rootMargin: '
|
| 145 |
|
| 146 |
-
if (
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
setTimeout(loadWidget, 1000);
|
| 155 |
-
return;
|
| 156 |
-
}
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
const link = document.createElement('a');
|
| 165 |
-
link.className = 'twitter-timeline';
|
| 166 |
-
link.href = `https://twitter.com/${account.handle}?ref_src=twsrc%5Etfw`;
|
| 167 |
-
link.setAttribute('data-theme', 'dark');
|
| 168 |
-
link.setAttribute('data-chrome', 'noheader,nofooter,noborders,transparent,noscrollbar');
|
| 169 |
-
link.setAttribute('data-height', '400');
|
| 170 |
-
link.textContent = `Tweets by ${account.name}`;
|
| 171 |
-
container.appendChild(link);
|
| 172 |
-
|
| 173 |
-
// Initialize Widget
|
| 174 |
-
window.twttr.widgets.load(container).then((el) => {
|
| 175 |
-
if (el) setStatus('loaded');
|
| 176 |
-
else setStatus('error');
|
| 177 |
-
});
|
| 178 |
-
};
|
| 179 |
|
| 180 |
const colorMap = {
|
| 181 |
Security: "text-red-400 border-red-500/30 bg-red-500/10",
|
|
@@ -189,14 +210,14 @@
|
|
| 189 |
<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">
|
| 190 |
|
| 191 |
{/* Card Header */}
|
| 192 |
-
<div className="p-4 border-b border-white/5 bg-slate-900/50 flex justify-between items-center z-10">
|
| 193 |
<div className="flex items-center gap-3 overflow-hidden">
|
| 194 |
<div className={`text-[10px] font-mono font-bold px-2 py-0.5 rounded border uppercase tracking-wider ${colorMap[account.cat] || colorMap.Custom}`}>
|
| 195 |
{account.cat.substring(0, 3)}
|
| 196 |
</div>
|
| 197 |
<div className="flex flex-col min-w-0">
|
| 198 |
<span className="font-bold text-slate-100 truncate text-sm leading-tight">{account.name}</span>
|
| 199 |
-
<a href={`https://x.com/${account.handle}`} target="_blank" className="text-[11px] text-slate-500 hover:text-cyan-400 truncate flex items-center gap-1 transition-colors font-mono">
|
| 200 |
@{account.handle} <Icon size={10}>{icons.external}</Icon>
|
| 201 |
</a>
|
| 202 |
</div>
|
|
@@ -207,28 +228,31 @@
|
|
| 207 |
</div>
|
| 208 |
|
| 209 |
{/* Card Body */}
|
| 210 |
-
<div className="flex-1 relative bg-black/20">
|
| 211 |
-
|
|
|
|
| 212 |
|
| 213 |
{status === 'loading' && (
|
| 214 |
-
<div className="absolute inset-0">
|
| 215 |
<Skeleton />
|
| 216 |
</div>
|
| 217 |
)}
|
| 218 |
|
| 219 |
{(status === 'error' || status === 'offline') && (
|
| 220 |
-
<div className="absolute inset-0 flex flex-col items-center justify-center p-6 text-center bg-slate-900/
|
| 221 |
<div className="p-3 bg-slate-800 rounded-full mb-3 text-slate-400">
|
| 222 |
<Icon size={24}>{status === 'offline' ? icons.wifiOff : icons.alert}</Icon>
|
| 223 |
</div>
|
| 224 |
<span className="text-slate-300 text-sm font-bold mb-1">
|
| 225 |
-
{status === 'offline' ? 'Offline
|
| 226 |
</span>
|
| 227 |
<p className="text-xs text-slate-500 mb-4 max-w-[200px]">
|
| 228 |
-
{status === 'offline'
|
|
|
|
|
|
|
| 229 |
</p>
|
| 230 |
-
<a href={`https://x.com/${account.handle}`} target="_blank" 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">
|
| 231 |
-
OPEN X.COM
|
| 232 |
</a>
|
| 233 |
</div>
|
| 234 |
)}
|
|
@@ -249,18 +273,23 @@
|
|
| 249 |
const [input, setInput] = useState("");
|
| 250 |
const [online, setOnline] = useState(navigator.onLine);
|
| 251 |
|
| 252 |
-
// Init
|
| 253 |
useEffect(() => {
|
| 254 |
-
// Twitter Script
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
-
// Listeners
|
| 264 |
const updateOnline = () => setOnline(navigator.onLine);
|
| 265 |
window.addEventListener('online', updateOnline);
|
| 266 |
window.addEventListener('offline', updateOnline);
|
|
@@ -270,7 +299,6 @@
|
|
| 270 |
};
|
| 271 |
}, []);
|
| 272 |
|
| 273 |
-
// Persistence
|
| 274 |
useEffect(() => {
|
| 275 |
localStorage.setItem(STORAGE_KEY, JSON.stringify(accounts));
|
| 276 |
}, [accounts]);
|
|
@@ -279,8 +307,13 @@
|
|
| 279 |
e.preventDefault();
|
| 280 |
if (!input.trim()) return;
|
| 281 |
let handle = input.trim();
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
if (accounts.some(a => a.handle.toLowerCase() === handle.toLowerCase())) {
|
| 286 |
alert("Target already monitored.");
|
|
@@ -297,12 +330,10 @@
|
|
| 297 |
}, [accounts, activeCategory]);
|
| 298 |
|
| 299 |
return (
|
| 300 |
-
<div className="min-h-screen flex flex-col font-sans">
|
| 301 |
-
|
| 302 |
-
{/* TOP BAR / HEADER */}
|
| 303 |
<header className="sticky top-0 z-50 glass-panel border-t-0 border-x-0">
|
| 304 |
<div className="max-w-[1920px] mx-auto">
|
| 305 |
-
{/* Logo & Status Area */}
|
| 306 |
<div className="px-4 py-4 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
| 307 |
<div className="flex items-center gap-4">
|
| 308 |
<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">
|
|
@@ -310,31 +341,26 @@
|
|
| 310 |
</div>
|
| 311 |
<div>
|
| 312 |
<h1 className="text-xl font-black tracking-tight text-white uppercase flex items-center gap-2">
|
| 313 |
-
Crypto Fortress <span className="text-[10px] bg-slate-800 text-slate-400 px-1.5 py-0.5 rounded border border-slate-700">
|
| 314 |
</h1>
|
| 315 |
<p className="text-xs text-slate-500 font-mono">Real-time Intel Command Center</p>
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
<
|
| 321 |
-
<Icon size={14}>{online ? icons.wifi : icons.wifiOff}</Icon>
|
| 322 |
-
<span>{online ? 'SYSTEM ONLINE' : 'DISCONNECTED'}</span>
|
| 323 |
-
</div>
|
| 324 |
</div>
|
| 325 |
</div>
|
| 326 |
|
| 327 |
-
{/* Controls Area */}
|
| 328 |
<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">
|
| 329 |
-
|
| 330 |
-
<div className="flex gap-2 overflow-x-auto no-scrollbar w-full md:w-auto mask-linear-fade pb-1 md:pb-0">
|
| 331 |
{CATEGORIES.map(cat => (
|
| 332 |
<button
|
| 333 |
key={cat}
|
| 334 |
onClick={() => setActiveCategory(cat)}
|
| 335 |
className={`px-3 py-1.5 rounded-lg text-xs font-bold font-mono transition-all whitespace-nowrap border ${
|
| 336 |
activeCategory === cat
|
| 337 |
-
? 'bg-slate-800 text-cyan-400 border-cyan-500/50
|
| 338 |
: 'border-transparent text-slate-500 hover:text-slate-300 hover:bg-white/5'
|
| 339 |
}`}
|
| 340 |
>
|
|
@@ -342,10 +368,8 @@
|
|
| 342 |
</button>
|
| 343 |
))}
|
| 344 |
</div>
|
| 345 |
-
|
| 346 |
-
{/* Add Input */}
|
| 347 |
<form onSubmit={addAccount} className="flex w-full md:w-auto relative group">
|
| 348 |
-
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-slate-600
|
| 349 |
<Icon size={14}>{icons.plus}</Icon>
|
| 350 |
</div>
|
| 351 |
<input
|
|
@@ -353,9 +377,9 @@
|
|
| 353 |
value={input}
|
| 354 |
onChange={(e) => setInput(e.target.value)}
|
| 355 |
placeholder="Add target handle..."
|
| 356 |
-
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
|
| 357 |
/>
|
| 358 |
-
<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
|
| 359 |
ADD
|
| 360 |
</button>
|
| 361 |
</form>
|
|
@@ -378,22 +402,13 @@
|
|
| 378 |
</div>
|
| 379 |
) : (
|
| 380 |
<div className="flex flex-col items-center justify-center h-[50vh] text-slate-600">
|
| 381 |
-
<div className="p-4 bg-slate-900 rounded-full mb-4
|
| 382 |
<Icon size={48} className="opacity-50">{icons.search}</Icon>
|
| 383 |
</div>
|
| 384 |
<h2 className="text-xl font-bold text-slate-500">No Intelligence Found</h2>
|
| 385 |
-
<p className="text-sm">Select a different sector or add a new target.</p>
|
| 386 |
</div>
|
| 387 |
)}
|
| 388 |
</main>
|
| 389 |
-
|
| 390 |
-
{/* FOOTER */}
|
| 391 |
-
<footer className="border-t border-slate-800/50 bg-black py-4 text-center">
|
| 392 |
-
<p className="text-[10px] text-slate-600 font-mono uppercase tracking-widest">
|
| 393 |
-
Secure Environment • Encrypted Storage • No Tracking
|
| 394 |
-
</p>
|
| 395 |
-
</footer>
|
| 396 |
-
|
| 397 |
</div>
|
| 398 |
);
|
| 399 |
};
|
|
|
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 6 |
<title>Crypto Intel Fortress — Offline-First & Bulletproof</title>
|
| 7 |
+
<meta name="description" content="Real-time Crypto Security & On-chain Monitor.">
|
| 8 |
+
<!-- Fix referrer policy for Twitter Widgets -->
|
| 9 |
+
<meta name="referrer" content="no-referrer-when-downgrade">
|
| 10 |
|
| 11 |
<!-- Favicon SVG -->
|
| 12 |
<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>">
|
|
|
|
| 24 |
},
|
| 25 |
animation: {
|
| 26 |
'fade-in': 'fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
|
|
| 27 |
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 28 |
},
|
| 29 |
keyframes: {
|
| 30 |
+
fadeIn: { '0%': { opacity: '0' }, '100%': { opacity: '1' } }
|
|
|
|
| 31 |
},
|
| 32 |
colors: {
|
| 33 |
+
'fortress-bg': '#020617',
|
|
|
|
| 34 |
}
|
| 35 |
}
|
| 36 |
}
|
|
|
|
| 44 |
|
| 45 |
<style>
|
| 46 |
body { background-color: #000000; color: #e2e8f0; }
|
|
|
|
|
|
|
| 47 |
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 48 |
::-webkit-scrollbar-track { background: #020617; }
|
| 49 |
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
|
| 50 |
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
|
|
|
| 51 |
.glass-panel {
|
| 52 |
background: rgba(15, 23, 42, 0.6);
|
| 53 |
backdrop-filter: blur(16px);
|
|
|
|
| 55 |
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 56 |
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
| 57 |
}
|
|
|
|
| 58 |
.no-scrollbar::-webkit-scrollbar { display: none; }
|
| 59 |
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
| 60 |
+
|
| 61 |
+
/* Force twitter widget container height */
|
| 62 |
+
.twitter-container iframe {
|
| 63 |
+
width: 100% !important;
|
| 64 |
+
height: 100% !important;
|
| 65 |
+
border-radius: 8px;
|
| 66 |
+
}
|
| 67 |
</style>
|
| 68 |
</head>
|
| 69 |
<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">
|
| 70 |
<div id="root"></div>
|
| 71 |
|
| 72 |
<script type="text/babel">
|
| 73 |
+
const { useState, useEffect, useRef, useMemo } = React;
|
| 74 |
|
| 75 |
+
// ================== ICONS ==================
|
| 76 |
const Icon = ({ children, size = 20, className = "" }) => (
|
| 77 |
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
| 78 |
{children}
|
|
|
|
| 81 |
|
| 82 |
const icons = {
|
| 83 |
shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/>,
|
|
|
|
| 84 |
search: <><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></>,
|
| 85 |
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"/></>,
|
| 86 |
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"/>,
|
|
|
|
| 90 |
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"/></>
|
| 91 |
};
|
| 92 |
|
| 93 |
+
// ================== DATA ==================
|
| 94 |
const DEFAULT_ACCOUNTS = [
|
| 95 |
{ handle: "PeckShieldAlert", name: "PeckShield", cat: "Security" },
|
| 96 |
{ handle: "realScamSniffer", name: "Scam Sniffer", cat: "Security" },
|
|
|
|
| 109 |
|
| 110 |
// ================== COMPONENTS ==================
|
| 111 |
|
|
|
|
| 112 |
const Skeleton = () => (
|
| 113 |
<div className="w-full h-full p-4 flex flex-col gap-4 animate-pulse-slow">
|
| 114 |
<div className="flex gap-3">
|
|
|
|
| 122 |
</div>
|
| 123 |
);
|
| 124 |
|
| 125 |
+
// Improved Twitter Card specifically for Hugging Face / Strict Environments
|
| 126 |
const TwitterCard = React.memo(({ account, onRemove, isOnline }) => {
|
| 127 |
const containerRef = useRef(null);
|
| 128 |
const [status, setStatus] = useState('loading'); // loading | loaded | error | offline
|
|
|
|
| 135 |
|
| 136 |
setStatus('loading');
|
| 137 |
|
| 138 |
+
let isMounted = true;
|
| 139 |
+
const container = containerRef.current;
|
| 140 |
+
|
| 141 |
+
const initWidget = () => {
|
| 142 |
+
if (!window.twttr || !window.twttr.widgets) {
|
| 143 |
+
setTimeout(initWidget, 500);
|
| 144 |
+
return;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
if (!container) return;
|
| 148 |
+
container.innerHTML = ''; // Clean up
|
| 149 |
+
|
| 150 |
+
// Use createTimeline instead of scanning DOM (More reliable for React/SPA)
|
| 151 |
+
window.twttr.widgets.createTimeline(
|
| 152 |
+
{
|
| 153 |
+
sourceType: 'profile',
|
| 154 |
+
screenName: account.handle
|
| 155 |
+
},
|
| 156 |
+
container,
|
| 157 |
+
{
|
| 158 |
+
theme: 'dark',
|
| 159 |
+
height: 400,
|
| 160 |
+
chrome: 'noheader,nofooter,noborders,transparent,noscrollbar',
|
| 161 |
+
dnt: true // Do Not Track - helps with privacy blockers
|
| 162 |
+
}
|
| 163 |
+
).then((el) => {
|
| 164 |
+
if (isMounted) {
|
| 165 |
+
if (el) {
|
| 166 |
+
setStatus('loaded');
|
| 167 |
+
} else {
|
| 168 |
+
// Widget returned null (Blocked or handle invalid)
|
| 169 |
+
setStatus('error');
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}).catch(() => {
|
| 173 |
+
if (isMounted) setStatus('error');
|
| 174 |
+
});
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
// Observer for lazy loading
|
| 178 |
const observer = new IntersectionObserver((entries) => {
|
| 179 |
if (entries[0].isIntersecting) {
|
| 180 |
+
initWidget();
|
| 181 |
observer.disconnect();
|
| 182 |
}
|
| 183 |
+
}, { rootMargin: '100px' });
|
| 184 |
|
| 185 |
+
if (container) observer.observe(container);
|
| 186 |
|
| 187 |
+
// Timeout Safety: If widget takes too long (e.g. Hugging Face blocks it), show error/link
|
| 188 |
+
const timer = setTimeout(() => {
|
| 189 |
+
if (isMounted && status === 'loading') {
|
| 190 |
+
setStatus('error');
|
| 191 |
+
}
|
| 192 |
+
}, 4000); // 4 seconds timeout
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
return () => {
|
| 195 |
+
isMounted = false;
|
| 196 |
+
observer.disconnect();
|
| 197 |
+
clearTimeout(timer);
|
| 198 |
+
};
|
| 199 |
+
}, [account.handle, isOnline]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
const colorMap = {
|
| 202 |
Security: "text-red-400 border-red-500/30 bg-red-500/10",
|
|
|
|
| 210 |
<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">
|
| 211 |
|
| 212 |
{/* Card Header */}
|
| 213 |
+
<div className="p-4 border-b border-white/5 bg-slate-900/50 flex justify-between items-center z-10 shrink-0">
|
| 214 |
<div className="flex items-center gap-3 overflow-hidden">
|
| 215 |
<div className={`text-[10px] font-mono font-bold px-2 py-0.5 rounded border uppercase tracking-wider ${colorMap[account.cat] || colorMap.Custom}`}>
|
| 216 |
{account.cat.substring(0, 3)}
|
| 217 |
</div>
|
| 218 |
<div className="flex flex-col min-w-0">
|
| 219 |
<span className="font-bold text-slate-100 truncate text-sm leading-tight">{account.name}</span>
|
| 220 |
+
<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">
|
| 221 |
@{account.handle} <Icon size={10}>{icons.external}</Icon>
|
| 222 |
</a>
|
| 223 |
</div>
|
|
|
|
| 228 |
</div>
|
| 229 |
|
| 230 |
{/* Card Body */}
|
| 231 |
+
<div className="flex-1 relative bg-black/20 overflow-hidden">
|
| 232 |
+
{/* Twitter Container */}
|
| 233 |
+
<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>
|
| 234 |
|
| 235 |
{status === 'loading' && (
|
| 236 |
+
<div className="absolute inset-0 pointer-events-none">
|
| 237 |
<Skeleton />
|
| 238 |
</div>
|
| 239 |
)}
|
| 240 |
|
| 241 |
{(status === 'error' || status === 'offline') && (
|
| 242 |
+
<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">
|
| 243 |
<div className="p-3 bg-slate-800 rounded-full mb-3 text-slate-400">
|
| 244 |
<Icon size={24}>{status === 'offline' ? icons.wifiOff : icons.alert}</Icon>
|
| 245 |
</div>
|
| 246 |
<span className="text-slate-300 text-sm font-bold mb-1">
|
| 247 |
+
{status === 'offline' ? 'Offline' : 'Feed Unavailable'}
|
| 248 |
</span>
|
| 249 |
<p className="text-xs text-slate-500 mb-4 max-w-[200px]">
|
| 250 |
+
{status === 'offline'
|
| 251 |
+
? 'Reconnect to internet.'
|
| 252 |
+
: 'Twitter restricts embedding in this environment (HF/AdBlock).'}
|
| 253 |
</p>
|
| 254 |
+
<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">
|
| 255 |
+
OPEN ON X.COM <Icon size={12}>{icons.external}</Icon>
|
| 256 |
</a>
|
| 257 |
</div>
|
| 258 |
)}
|
|
|
|
| 273 |
const [input, setInput] = useState("");
|
| 274 |
const [online, setOnline] = useState(navigator.onLine);
|
| 275 |
|
|
|
|
| 276 |
useEffect(() => {
|
| 277 |
+
// Initialize Twitter Widget Script Globally
|
| 278 |
+
window.twttr = (function(d, s, id) {
|
| 279 |
+
var js, fjs = d.getElementsByTagName(s)[0],
|
| 280 |
+
t = window.twttr || {};
|
| 281 |
+
if (d.getElementById(id)) return t;
|
| 282 |
+
js = d.createElement(s);
|
| 283 |
+
js.id = id;
|
| 284 |
+
js.src = "https://platform.twitter.com/widgets.js";
|
| 285 |
+
fjs.parentNode.insertBefore(js, fjs);
|
| 286 |
+
t._e = [];
|
| 287 |
+
t.ready = function(f) {
|
| 288 |
+
t._e.push(f);
|
| 289 |
+
};
|
| 290 |
+
return t;
|
| 291 |
+
}(document, "script", "twitter-wjs"));
|
| 292 |
|
|
|
|
| 293 |
const updateOnline = () => setOnline(navigator.onLine);
|
| 294 |
window.addEventListener('online', updateOnline);
|
| 295 |
window.addEventListener('offline', updateOnline);
|
|
|
|
| 299 |
};
|
| 300 |
}, []);
|
| 301 |
|
|
|
|
| 302 |
useEffect(() => {
|
| 303 |
localStorage.setItem(STORAGE_KEY, JSON.stringify(accounts));
|
| 304 |
}, [accounts]);
|
|
|
|
| 307 |
e.preventDefault();
|
| 308 |
if (!input.trim()) return;
|
| 309 |
let handle = input.trim();
|
| 310 |
+
// Clean URL inputs
|
| 311 |
+
try {
|
| 312 |
+
const url = new URL(handle);
|
| 313 |
+
handle = url.pathname.split('/').filter(Boolean).pop();
|
| 314 |
+
} catch (e) {}
|
| 315 |
+
|
| 316 |
+
handle = handle.replace('@', '').replace('?', '').split('/')[0];
|
| 317 |
|
| 318 |
if (accounts.some(a => a.handle.toLowerCase() === handle.toLowerCase())) {
|
| 319 |
alert("Target already monitored.");
|
|
|
|
| 330 |
}, [accounts, activeCategory]);
|
| 331 |
|
| 332 |
return (
|
| 333 |
+
<div className="min-h-screen flex flex-col font-sans pb-10">
|
| 334 |
+
{/* HEADER */}
|
|
|
|
| 335 |
<header className="sticky top-0 z-50 glass-panel border-t-0 border-x-0">
|
| 336 |
<div className="max-w-[1920px] mx-auto">
|
|
|
|
| 337 |
<div className="px-4 py-4 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
| 338 |
<div className="flex items-center gap-4">
|
| 339 |
<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">
|
|
|
|
| 341 |
</div>
|
| 342 |
<div>
|
| 343 |
<h1 className="text-xl font-black tracking-tight text-white uppercase flex items-center gap-2">
|
| 344 |
+
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>
|
| 345 |
</h1>
|
| 346 |
<p className="text-xs text-slate-500 font-mono">Real-time Intel Command Center</p>
|
| 347 |
</div>
|
| 348 |
</div>
|
| 349 |
+
<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'}`}>
|
| 350 |
+
<Icon size={14}>{online ? icons.wifi : icons.wifiOff}</Icon>
|
| 351 |
+
<span>{online ? 'ONLINE' : 'OFFLINE'}</span>
|
|
|
|
|
|
|
|
|
|
| 352 |
</div>
|
| 353 |
</div>
|
| 354 |
|
|
|
|
| 355 |
<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">
|
| 356 |
+
<div className="flex gap-2 overflow-x-auto no-scrollbar w-full md:w-auto pb-1 md:pb-0">
|
|
|
|
| 357 |
{CATEGORIES.map(cat => (
|
| 358 |
<button
|
| 359 |
key={cat}
|
| 360 |
onClick={() => setActiveCategory(cat)}
|
| 361 |
className={`px-3 py-1.5 rounded-lg text-xs font-bold font-mono transition-all whitespace-nowrap border ${
|
| 362 |
activeCategory === cat
|
| 363 |
+
? 'bg-slate-800 text-cyan-400 border-cyan-500/50'
|
| 364 |
: 'border-transparent text-slate-500 hover:text-slate-300 hover:bg-white/5'
|
| 365 |
}`}
|
| 366 |
>
|
|
|
|
| 368 |
</button>
|
| 369 |
))}
|
| 370 |
</div>
|
|
|
|
|
|
|
| 371 |
<form onSubmit={addAccount} className="flex w-full md:w-auto relative group">
|
| 372 |
+
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none text-slate-600">
|
| 373 |
<Icon size={14}>{icons.plus}</Icon>
|
| 374 |
</div>
|
| 375 |
<input
|
|
|
|
| 377 |
value={input}
|
| 378 |
onChange={(e) => setInput(e.target.value)}
|
| 379 |
placeholder="Add target handle..."
|
| 380 |
+
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"
|
| 381 |
/>
|
| 382 |
+
<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">
|
| 383 |
ADD
|
| 384 |
</button>
|
| 385 |
</form>
|
|
|
|
| 402 |
</div>
|
| 403 |
) : (
|
| 404 |
<div className="flex flex-col items-center justify-center h-[50vh] text-slate-600">
|
| 405 |
+
<div className="p-4 bg-slate-900 rounded-full mb-4">
|
| 406 |
<Icon size={48} className="opacity-50">{icons.search}</Icon>
|
| 407 |
</div>
|
| 408 |
<h2 className="text-xl font-bold text-slate-500">No Intelligence Found</h2>
|
|
|
|
| 409 |
</div>
|
| 410 |
)}
|
| 411 |
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
</div>
|
| 413 |
);
|
| 414 |
};
|