File size: 16,815 Bytes
07381d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Keys — NSFWStudio</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300..800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script>
tailwind.config = { theme: { extend: { fontFamily: { sans: ['Inter','system-ui','sans-serif'], mono: ['JetBrains Mono','monospace'] }, colors: { bg: 'oklch(0.07 0.006 285)', surface: 'oklch(0.12 0.02 285)', 'surface-2': 'oklch(0.16 0.025 285)', 'surface-3': 'oklch(0.20 0.028 285)', fg: 'oklch(0.97 0.005 285)', muted: 'oklch(0.55 0.02 285)', primary: 'oklch(0.70 0.28 350)', 'primary-foreground': 'oklch(0.98 0.005 285)', secondary: 'oklch(0.75 0.15 215)', accent: 'oklch(0.80 0.18 85)', border: 'oklch(0.22 0.02 285)', ring: 'oklch(0.70 0.28 350)', destructive: 'oklch(0.55 0.22 25)', success: 'oklch(0.65 0.2 145)' } } } }
</script>
<style>
body { background-color: oklch(0.07 0.006 285); color: oklch(0.97 0.005 285); font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
.glass { background: oklch(0.14 0.015 285 / 0.6); backdrop-filter: blur(24px) saturate(1.2); -webkit-backdrop-filter: blur(24px) saturate(1.2); border: 1px solid oklch(0.25 0.02 285 / 0.5); }
::selection { background: oklch(0.70 0.28 350 / 0.3); }
body::after { content:''; position:fixed; inset:0; pointer-events:none; z-index:9999; opacity:0.025; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); background-repeat:repeat; background-size:256px 256px; }
.no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.tabular-nums { font-variant-numeric: tabular-nums; }
</style>
</head>
<body class="antialiased">
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
const Icon = ({ name, size = 20, className = "", strokeWidth = 2 }) => {
const ref = React.useRef(null);
React.useEffect(() => { if (ref.current) { const existing = ref.current.querySelector('svg'); if (existing) existing.remove(); const svg = lucide.createElement(lucide.icons[name]); if (svg) { svg.setAttribute('width',size); svg.setAttribute('height',size); svg.setAttribute('stroke-width',strokeWidth); ref.current.appendChild(svg); } } }, [name, size, className, strokeWidth]);
return <span ref={ref} className={`inline-flex items-center justify-center ${className}`} style={{ width: size, height: size }} />;
};
const navItems = [
{ name: 'Dashboard', icon: 'layout-dashboard', href: 'dashboard.html' },
{ name: 'Generator', icon: 'sparkles', href: 'generator.html' },
{ name: 'Playground', icon: 'code', href: 'playground.html' },
{ name: 'Models', icon: 'box', href: 'models.html' },
{ name: 'Workflows', icon: 'workflow', href: 'workflows.html' },
{ name: 'Marketplace', icon: 'shopping-bag', href: 'marketplace.html' },
{ name: 'API Keys', icon: 'key', href: 'api-keys.html', active: true },
{ divider: true },
{ name: 'Profile', icon: 'user', href: 'profile.html' },
{ name: 'Settings', icon: 'settings', href: 'settings.html' },
];
const Sidebar = ({ isOpen, setIsOpen }) => (
<>
<div className={`fixed inset-0 z-40 lg:hidden transition-all ${isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setIsOpen(false)} />
</div>
<aside className={`fixed top-0 left-0 bottom-0 z-50 w-64 bg-surface border-r border-border flex flex-col transition-transform duration-300 lg:translate-x-0 ${isOpen ? 'translate-x-0' : '-translate-x-full'}`}>
<div className="p-4 flex items-center gap-2.5 border-b border-border"><div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center"><Icon name="sparkles" size={16} className="text-primary-foreground" /></div><span className="text-lg font-bold tracking-tight">NSFW<span className="text-muted">Studio</span></span></div>
<nav className="flex-1 p-3 space-y-1 overflow-y-auto no-scrollbar">{navItems.map((item, i) => item.divider ? <div key={i} className="my-3 border-t border-border" /> : <a key={i} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${item.active ? 'bg-primary/10 text-primary border border-primary/20' : 'text-muted hover:text-fg hover:bg-surface-2 border border-transparent'}`}><Icon name={item.icon} size={18} />{item.name}</a>)}</nav>
<div className="p-4 border-t border-border"><a href="documentation.html" className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-muted hover:text-fg hover:bg-surface-2 transition-colors"><Icon name="book-open" size={18} />Documentation</a><a href="api-reference.html" className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-muted hover:text-fg hover:bg-surface-2 transition-colors"><Icon name="file-code" size={18} />API Reference</a></div>
</aside>
</>
);
const App = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [keys, setKeys] = useState([
{ id: 1, name: 'Production Key', key: 'nfw_prod_sk_7f3a9b2c4d5e6f1a8b9c0d1e', created: 'Jan 15, 2025', lastUsed: '2 min ago', status: 'active' },
{ id: 2, name: 'Development Key', key: 'nfw_dev_sk_2e4b6c8d0f1a3b5c7d9e1f2a', created: 'Feb 3, 2025', lastUsed: '3 days ago', status: 'active' },
{ id: 3, name: 'Staging Environment', key: 'nfw_stg_sk_9a1b2c3d4e5f6a7b8c9d0e1f', created: 'Mar 22, 2025', lastUsed: 'Never', status: 'inactive' },
]);
const [showKey, setShowKey] = useState({});
const [showCreate, setShowCreate] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const toggleShowKey = (id) => setShowKey(prev => ({ ...prev, [id]: !prev[id] }));
const maskKey = (key) => key.substring(0, 12) + '••••••••••••••••••••';
return (
<div className="min-h-screen bg-bg">
<Sidebar isOpen={sidebarOpen} setIsOpen={setSidebarOpen} />
<div className="lg:ml-64">
<header className="sticky top-0 z-30 glass border-b border-border">
<div className="flex items-center justify-between px-4 sm:px-6 h-16">
<div className="flex items-center gap-4">
<button onClick={() => setSidebarOpen(true)} className="lg:hidden p-2 rounded-lg hover:bg-surface-2 text-muted"><Icon name="menu" size={20} /></button>
<h1 className="text-xl font-bold">API Keys</h1>
</div>
<div className="flex items-center gap-3">
<a href="profile.html" className="w-9 h-9 rounded-lg overflow-hidden border border-border"><img src="http://static.photos/people/100x100/10" alt="" className="w-full h-full object-cover" /></a>
</div>
</div>
</header>
<main className="p-4 sm:p-6 lg:p-8 max-w-5xl">
{/* Danger Zone Notice */}
<div className="rounded-xl p-4 bg-destructive/10 border border-destructive/20 mb-6 flex items-start gap-3">
<Icon name="alert-triangle" size={20} className="text-destructive shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-fg text-sm">Keep your API keys secure</h3>
<p className="text-xs text-muted mt-0.5">Never share your secret keys in public repositories, client-side code, or public forums. If a key is compromised, revoke it immediately.</p>
</div>
</div>
<div className="flex items-center justify-between mb-6">
<div>
<p className="text-sm text-muted">Manage your API keys for programmatic access.</p>
</div>
<button onClick={() => setShowCreate(true)} className="px-4 py-2.5 rounded-xl bg-primary text-primary-foreground font-semibold text-sm hover:brightness-110 transition-all flex items-center gap-2">
<Icon name="plus" size={16} />Create Key
</button>
</div>
{/* Create dialog */}
{showCreate && (
<div className="rounded-2xl p-6 bg-surface border border-border mb-6">
<h3 className="font-semibold mb-4">Create New API Key</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-fg mb-2">Key Name</label>
<input type="text" value={newKeyName} onChange={e => setNewKeyName(e.target.value)} placeholder="e.g., Production Key" className="w-full max-w-md px-4 py-2.5 rounded-xl bg-surface-2 border border-border text-sm text-fg placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-primary/50" />
</div>
<div className="flex gap-3">
<button className="px-4 py-2.5 rounded-xl bg-primary text-primary-foreground font-semibold text-sm hover:brightness-110 transition-all" onClick={() => { if(newKeyName.trim()) { setKeys([...keys, { id: Date.now(), name: newKeyName, key: 'nfw_new_sk_' + Math.random().toString(36).substring(2, 18), created: 'Just now', lastUsed: 'Never', status: 'active' }]); setNewKeyName(''); setShowCreate(false); } }}>Create</button>
<button onClick={() => { setShowCreate(false); setNewKeyName(''); }} className="px-4 py-2.5 rounded-xl border border-border text-sm font-medium text-muted hover:text-fg hover:bg-surface-2 transition-colors">Cancel</button>
</div>
</div>
</div>
)}
{/* Keys list */}
<div className="space-y-4">
{keys.map(k => (
<div key={k.id} className="rounded-2xl p-5 bg-surface border border-border hover:border-surface-3 transition-colors">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center"><Icon name="key" size={18} className="text-primary" /></div>
<div>
<div className="font-semibold text-fg">{k.name}</div>
<div className="text-xs text-muted">Created {k.created}</div>
</div>
</div>
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold border ${k.status === 'active' ? 'bg-success/10 text-success border-success/20' : 'bg-muted/10 text-muted border-border'}`}>
<span className="w-1.5 h-1.5 rounded-full bg-current" />
{k.status === 'active' ? 'Active' : 'Inactive'}
</span>
</div>
<div className="flex items-center gap-2 p-3 rounded-xl bg-surface-2 border border-border mb-4">
<code className="flex-1 text-sm font-mono text-muted truncate">{showKey[k.id] ? k.key : maskKey(k.key)}</code>
<button onClick={() => toggleShowKey(k.id)} className="p-1.5 rounded-lg hover:bg-surface-3 text-muted hover:text-fg transition-colors shrink-0"><Icon name={showKey[k.id] ? 'eye-off' : 'eye'} size={16} /></button>
<button onClick={() => { navigator.clipboard?.writeText(k.key); }} className="p-1.5 rounded-lg hover:bg-surface-3 text-muted hover:text-fg transition-colors shrink-0"><Icon name="copy" size={16} /></button>
</div>
<div className="flex items-center justify-between text-xs text-muted">
<span>Last used: {k.lastUsed}</span>
<div className="flex items-center gap-2">
<button className="px-3 py-1.5 rounded-lg border border-border text-muted hover:text-fg hover:bg-surface-2 transition-colors">Regenerate</button>
<button className="px-3 py-1.5 rounded-lg border border-destructive/30 text-destructive hover:bg-destructive/10 transition-colors">Revoke</button>
</div>
</div>
</div>
))}
</div>
{/* Usage Stats */}
<div className="mt-8 rounded-2xl p-6 bg-surface border border-border">
<h2 className="font-semibold mb-4">Usage This Month</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{[
{ label: 'Total Requests', value: '1.2M', change: '+23%' },
{ label: 'Tokens Used', value: '45.8M', change: '+18%' },
{ label: 'Avg Latency', value: '142ms', change: '-5%' },
{ label: 'Error Rate', value: '0.03%', change: '-12%' },
].map((s, i) => (
<div key={i} className="p-4 rounded-xl bg-surface-2 border border-border">
<div className="text-xs text-muted mb-1">{s.label}</div>
<div className="text-xl font-bold text-fg tabular-nums">{s.value}</div>
<div className="text-xs text-success mt-1">{s.change}</div>
</div>
))}
</div>
</div>
</main>
</div>
</div>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html> |