Update index.html
Browse files- index.html +405 -19
index.html
CHANGED
|
@@ -1,19 +1,405 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="vi" class="scroll-smooth">
|
| 3 |
+
<head>
|
| 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. 70 years of experience applied.">
|
| 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>">
|
| 11 |
+
|
| 12 |
+
<!-- Tailwind CSS -->
|
| 13 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 14 |
+
<script>
|
| 15 |
+
tailwind.config = {
|
| 16 |
+
darkMode: 'class',
|
| 17 |
+
theme: {
|
| 18 |
+
extend: {
|
| 19 |
+
fontFamily: {
|
| 20 |
+
sans: ['Inter', 'system-ui', 'sans-serif'],
|
| 21 |
+
mono: ['JetBrains Mono', 'monospace'],
|
| 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', // Slate 950
|
| 34 |
+
'fortress-card': '#0f172a', // Slate 900
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
</script>
|
| 40 |
+
|
| 41 |
+
<!-- React Dependencies -->
|
| 42 |
+
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
| 43 |
+
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
| 44 |
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| 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);
|
| 58 |
+
-webkit-backdrop-filter: blur(16px);
|
| 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, useCallback, useMemo } = React;
|
| 72 |
+
|
| 73 |
+
// ================== ICONS (SVG Optimized) ==================
|
| 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}
|
| 77 |
+
</svg>
|
| 78 |
+
);
|
| 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"/>,
|
| 86 |
+
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"/></>,
|
| 87 |
+
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"/></>,
|
| 88 |
+
plus: <><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>,
|
| 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 & CONFIG ==================
|
| 93 |
+
const DEFAULT_ACCOUNTS = [
|
| 94 |
+
{ handle: "PeckShieldAlert", name: "PeckShield", cat: "Security" },
|
| 95 |
+
{ handle: "realScamSniffer", name: "Scam Sniffer", cat: "Security" },
|
| 96 |
+
{ handle: "ZachXBT", name: "ZachXBT", cat: "Security" },
|
| 97 |
+
{ handle: "CertiKAlert", name: "CertiK", cat: "Security" },
|
| 98 |
+
{ handle: "lookonchain", name: "Lookonchain", cat: "On-chain" },
|
| 99 |
+
{ handle: "whale_alert", name: "Whale Alert", cat: "On-chain" },
|
| 100 |
+
{ handle: "ArkhamIntel", name: "Arkham", cat: "On-chain" },
|
| 101 |
+
{ handle: "WuBlockchain", name: "Wu Blockchain", cat: "News" },
|
| 102 |
+
{ handle: "Tier10k", name: "DB (Tier10k)", cat: "News" },
|
| 103 |
+
{ handle: "DeFiLlama", name: "DeFiLlama", cat: "Data" }
|
| 104 |
+
];
|
| 105 |
+
|
| 106 |
+
const CATEGORIES = ["All", "Security", "On-chain", "News", "Data", "Custom"];
|
| 107 |
+
const STORAGE_KEY = 'CRYPTO_FORTRESS_V70';
|
| 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">
|
| 115 |
+
<div className="w-10 h-10 rounded-full bg-slate-800/50"></div>
|
| 116 |
+
<div className="flex-1 space-y-2">
|
| 117 |
+
<div className="h-3 bg-slate-800/50 rounded w-1/3"></div>
|
| 118 |
+
<div className="h-2 bg-slate-800/30 rounded w-1/4"></div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
<div className="flex-1 bg-slate-800/20 rounded-lg border border-slate-800/30"></div>
|
| 122 |
+
</div>
|
| 123 |
+
);
|
| 124 |
+
|
| 125 |
+
// 2. Twitter Card (The Core Logic)
|
| 126 |
+
const TwitterCard = React.memo(({ account, onRemove, isOnline }) => {
|
| 127 |
+
const containerRef = useRef(null);
|
| 128 |
+
const [status, setStatus] = useState('loading'); // loading | loaded | error | offline
|
| 129 |
+
|
| 130 |
+
useEffect(() => {
|
| 131 |
+
if (!isOnline) {
|
| 132 |
+
setStatus('offline');
|
| 133 |
+
return;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
setStatus('loading');
|
| 137 |
+
|
| 138 |
+
// Observer for lazy loading (Performance Optimization)
|
| 139 |
+
const observer = new IntersectionObserver((entries) => {
|
| 140 |
+
if (entries[0].isIntersecting) {
|
| 141 |
+
loadWidget();
|
| 142 |
+
observer.disconnect();
|
| 143 |
+
}
|
| 144 |
+
}, { rootMargin: '50px' });
|
| 145 |
+
|
| 146 |
+
if (containerRef.current) observer.observe(containerRef.current);
|
| 147 |
+
|
| 148 |
+
return () => observer.disconnect();
|
| 149 |
+
}, [account.handle, isOnline]);
|
| 150 |
+
|
| 151 |
+
const loadWidget = () => {
|
| 152 |
+
if (!window.twttr) {
|
| 153 |
+
// Retry if script not ready
|
| 154 |
+
setTimeout(loadWidget, 1000);
|
| 155 |
+
return;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Clear container
|
| 159 |
+
const container = containerRef.current;
|
| 160 |
+
if (!container) return;
|
| 161 |
+
container.innerHTML = '';
|
| 162 |
+
|
| 163 |
+
// Create link for widget
|
| 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",
|
| 182 |
+
"On-chain": "text-cyan-400 border-cyan-500/30 bg-cyan-500/10",
|
| 183 |
+
News: "text-emerald-400 border-emerald-500/30 bg-emerald-500/10",
|
| 184 |
+
Data: "text-purple-400 border-purple-500/30 bg-purple-500/10",
|
| 185 |
+
Custom: "text-amber-400 border-amber-500/30 bg-amber-500/10"
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
return (
|
| 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>
|
| 203 |
+
</div>
|
| 204 |
+
<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">
|
| 205 |
+
<Icon size={16}>{icons.trash}</Icon>
|
| 206 |
+
</button>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
{/* Card Body */}
|
| 210 |
+
<div className="flex-1 relative bg-black/20">
|
| 211 |
+
<div ref={containerRef} className={`w-full h-full overflow-y-auto scrollbar-thin ${status !== 'loaded' ? 'hidden' : 'block'}`}></div>
|
| 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/80 backdrop-blur-sm z-20">
|
| 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 Mode' : 'Widget Blocked'}
|
| 226 |
+
</span>
|
| 227 |
+
<p className="text-xs text-slate-500 mb-4 max-w-[200px]">
|
| 228 |
+
{status === 'offline' ? 'Reconnect to view live intel.' : 'Browser privacy settings or local file restrictions.'}
|
| 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 |
+
)}
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
);
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// ================== MAIN APP ==================
|
| 241 |
+
const App = () => {
|
| 242 |
+
const [accounts, setAccounts] = useState(() => {
|
| 243 |
+
try {
|
| 244 |
+
const saved = localStorage.getItem(STORAGE_KEY);
|
| 245 |
+
return saved ? JSON.parse(saved) : DEFAULT_ACCOUNTS;
|
| 246 |
+
} catch { return DEFAULT_ACCOUNTS; }
|
| 247 |
+
});
|
| 248 |
+
const [activeCategory, setActiveCategory] = useState("All");
|
| 249 |
+
const [input, setInput] = useState("");
|
| 250 |
+
const [online, setOnline] = useState(navigator.onLine);
|
| 251 |
+
|
| 252 |
+
// Init
|
| 253 |
+
useEffect(() => {
|
| 254 |
+
// Twitter Script Inject
|
| 255 |
+
if (!document.getElementById('twitter-wjs')) {
|
| 256 |
+
const script = document.createElement("script");
|
| 257 |
+
script.id = 'twitter-wjs';
|
| 258 |
+
script.src = "https://platform.twitter.com/widgets.js";
|
| 259 |
+
script.async = true;
|
| 260 |
+
document.body.appendChild(script);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// Listeners
|
| 264 |
+
const updateOnline = () => setOnline(navigator.onLine);
|
| 265 |
+
window.addEventListener('online', updateOnline);
|
| 266 |
+
window.addEventListener('offline', updateOnline);
|
| 267 |
+
return () => {
|
| 268 |
+
window.removeEventListener('online', updateOnline);
|
| 269 |
+
window.removeEventListener('offline', updateOnline);
|
| 270 |
+
};
|
| 271 |
+
}, []);
|
| 272 |
+
|
| 273 |
+
// Persistence
|
| 274 |
+
useEffect(() => {
|
| 275 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(accounts));
|
| 276 |
+
}, [accounts]);
|
| 277 |
+
|
| 278 |
+
const addAccount = (e) => {
|
| 279 |
+
e.preventDefault();
|
| 280 |
+
if (!input.trim()) return;
|
| 281 |
+
let handle = input.trim();
|
| 282 |
+
if (handle.includes('/')) handle = handle.split('/').pop().split('?')[0];
|
| 283 |
+
handle = handle.replace('@', '');
|
| 284 |
+
|
| 285 |
+
if (accounts.some(a => a.handle.toLowerCase() === handle.toLowerCase())) {
|
| 286 |
+
alert("Target already monitored.");
|
| 287 |
+
return;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
setAccounts([{ handle, name: handle, cat: "Custom" }, ...accounts]);
|
| 291 |
+
setInput("");
|
| 292 |
+
};
|
| 293 |
+
|
| 294 |
+
const filteredAccounts = useMemo(() => {
|
| 295 |
+
if (activeCategory === "All") return accounts;
|
| 296 |
+
return accounts.filter(acc => acc.cat === activeCategory);
|
| 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">
|
| 309 |
+
<Icon size={24} className="text-white">{icons.shield}</Icon>
|
| 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">v2.0</span>
|
| 314 |
+
</h1>
|
| 315 |
+
<p className="text-xs text-slate-500 font-mono">Real-time Intel Command Center</p>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
|
| 319 |
+
<div className="flex items-center gap-3">
|
| 320 |
+
<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'}`}>
|
| 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 |
+
{/* Categories */}
|
| 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 shadow-[0_0_15px_rgba(6,182,212,0.15)]'
|
| 338 |
+
: 'border-transparent text-slate-500 hover:text-slate-300 hover:bg-white/5'
|
| 339 |
+
}`}
|
| 340 |
+
>
|
| 341 |
+
{cat.toUpperCase()}
|
| 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 group-focus-within:text-cyan-500 transition-colors">
|
| 349 |
+
<Icon size={14}>{icons.plus}</Icon>
|
| 350 |
+
</div>
|
| 351 |
+
<input
|
| 352 |
+
type="text"
|
| 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 focus:ring-1 focus:ring-cyan-500 placeholder-slate-600 font-mono transition-all"
|
| 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 hover:border-cyan-600 transition-all">
|
| 359 |
+
ADD
|
| 360 |
+
</button>
|
| 361 |
+
</form>
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
</header>
|
| 365 |
+
|
| 366 |
+
{/* MAIN GRID */}
|
| 367 |
+
<main className="flex-1 p-4 md:p-6 w-full max-w-[1920px] mx-auto">
|
| 368 |
+
{filteredAccounts.length > 0 ? (
|
| 369 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
| 370 |
+
{filteredAccounts.map(acc => (
|
| 371 |
+
<TwitterCard
|
| 372 |
+
key={acc.handle}
|
| 373 |
+
account={acc}
|
| 374 |
+
onRemove={(h) => setAccounts(accounts.filter(a => a.handle !== h))}
|
| 375 |
+
isOnline={online}
|
| 376 |
+
/>
|
| 377 |
+
))}
|
| 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 animate-pulse-slow">
|
| 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 |
+
};
|
| 400 |
+
|
| 401 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 402 |
+
root.render(<App />);
|
| 403 |
+
</script>
|
| 404 |
+
</body>
|
| 405 |
+
</html>
|