feat: redesign NixServer (WHM) and NixClient (cPanel) UIs

NixServer now has a dark orange-accented WHM-style layout with
collapsible sidebar grouped by function, stat bar, service manager
table with actions, quick-action icon grid, server info bar, and
a branded split-panel login page.

NixClient now has a cPanel-style feature icon grid with category
sections, live search, account sidebar with usage bars, and a
blue-themed login page.
This commit is contained in:
NixReaper
2026-04-25 15:51:47 -05:00
parent 0f28e879f0
commit a257b5571c
2 changed files with 1162 additions and 156 deletions
+470 -69
View File
@@ -1,107 +1,508 @@
import { useState } from 'react'
export default function App() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
/* ── Types ──────────────────────────────────────────────────────────────── */
type Page =
| 'home' | 'domains' | 'subdomains' | 'redirects'
| 'email' | 'emailAccounts' | 'webmail' | 'spamFilter'
| 'databases' | 'mysql' | 'phpmyadmin'
| 'files' | 'fileManager' | 'ftp' | 'backups'
| 'security' | 'ssl' | 'passwords' | 'ssh'
| 'software' | 'php' | 'wordpress'
| 'stats' | 'logs'
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (email && password) {
setIsLoggedIn(true)
}
/* ── Feature groups (cPanel icon grid style) ────────────────────────────── */
type Feature = { id: Page; label: string; icon: string; desc: string }
type FeatureGroup = { label: string; color: string; features: Feature[] }
const FEATURE_GROUPS: FeatureGroup[] = [
{
label: 'Domains',
color: 'blue',
features: [
{ id: 'domains', label: 'Domains', icon: '🌐', desc: 'Addon & parked domains' },
{ id: 'subdomains', label: 'Subdomains', icon: '🔗', desc: 'Create subdomains' },
{ id: 'redirects', label: 'Redirects', icon: '↪️', desc: '301 / 302 redirects' },
],
},
{
label: 'Email',
color: 'emerald',
features: [
{ id: 'emailAccounts', label: 'Email Accounts', icon: '✉️', desc: 'Create & manage mailboxes' },
{ id: 'webmail', label: 'Webmail', icon: '📬', desc: 'Open Roundcube webmail' },
{ id: 'spamFilter', label: 'Spam Filters', icon: '🚫', desc: 'SpamAssassin rules' },
],
},
{
label: 'Databases',
color: 'purple',
features: [
{ id: 'mysql', label: 'MySQL Databases', icon: '🗄️', desc: 'Create & manage DBs' },
{ id: 'phpmyadmin', label: 'phpMyAdmin', icon: '🖥️', desc: 'Database admin GUI' },
],
},
{
label: 'Files',
color: 'orange',
features: [
{ id: 'fileManager', label: 'File Manager', icon: '📁', desc: 'Browse & edit files' },
{ id: 'ftp', label: 'FTP Accounts', icon: '📡', desc: 'Manage FTP users' },
{ id: 'backups', label: 'Backups', icon: '💾', desc: 'Download & restore' },
],
},
{
label: 'Security',
color: 'red',
features: [
{ id: 'ssl', label: 'SSL / TLS', icon: '🔒', desc: "Let's Encrypt certs" },
{ id: 'passwords', label: 'Password', icon: '🔑', desc: 'Change cPanel password' },
{ id: 'ssh', label: 'SSH Keys', icon: '🗝️', desc: 'Manage SSH key pairs' },
],
},
{
label: 'Software',
color: 'cyan',
features: [
{ id: 'php', label: 'PHP Version', icon: '🐘', desc: 'Select PHP 8.x' },
{ id: 'wordpress', label: 'WordPress', icon: '🌀', desc: 'Auto-install WP' },
],
},
{
label: 'Metrics',
color: 'yellow',
features: [
{ id: 'stats', label: 'Statistics', icon: '📊', desc: 'Bandwidth & visitors' },
{ id: 'logs', label: 'Logs', icon: '📜', desc: 'Error & access logs' },
],
},
]
const COLOR_MAP: Record<string, { tile: string; icon: string; badge: string }> = {
blue: { tile: 'hover:border-blue-500/40 hover:bg-blue-500/5', icon: 'bg-blue-500/15 text-blue-400', badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30' },
emerald: { tile: 'hover:border-emerald-500/40 hover:bg-emerald-500/5', icon: 'bg-emerald-500/15 text-emerald-400', badge: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' },
purple: { tile: 'hover:border-purple-500/40 hover:bg-purple-500/5', icon: 'bg-purple-500/15 text-purple-400', badge: 'bg-purple-500/20 text-purple-400 border-purple-500/30' },
orange: { tile: 'hover:border-orange-500/40 hover:bg-orange-500/5', icon: 'bg-orange-500/15 text-orange-400', badge: 'bg-orange-500/20 text-orange-400 border-orange-500/30' },
red: { tile: 'hover:border-red-500/40 hover:bg-red-500/5', icon: 'bg-red-500/15 text-red-400', badge: 'bg-red-500/20 text-red-400 border-red-500/30' },
cyan: { tile: 'hover:border-cyan-500/40 hover:bg-cyan-500/5', icon: 'bg-cyan-500/15 text-cyan-400', badge: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30' },
yellow: { tile: 'hover:border-yellow-500/40 hover:bg-yellow-500/5', icon: 'bg-yellow-500/15 text-yellow-400', badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30' },
}
/* ── Usage bar ──────────────────────────────────────────────────────────── */
function UsageBar({ label, used, total, unit, color }: { label: string; used: number; total: number; unit: string; color: string }) {
const pct = total > 0 ? Math.min((used / total) * 100, 100) : 0
const barColor = pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-yellow-500' : color
return (
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-gray-400 font-medium">{label}</span>
<span className="text-gray-500">{used === 0 ? '0' : used} / {total === 0 ? '∞' : total + ' ' + unit}</span>
</div>
<div className="h-1.5 bg-[#2a3044] rounded-full overflow-hidden">
<div className={`h-full rounded-full transition-all ${barColor}`} style={{ width: `${total === 0 ? 5 : pct}%` }} />
</div>
</div>
)
}
/* ── Account info sidebar ───────────────────────────────────────────────── */
function AccountSidebar({ domain }: { domain: string }) {
return (
<aside className="w-64 flex-shrink-0 space-y-4">
{/* Account card */}
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-xl overflow-hidden">
<div className="bg-gradient-to-r from-blue-700 to-blue-600 px-4 py-4">
<div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center text-white text-lg font-black mb-2">
{domain.charAt(0).toUpperCase()}
</div>
<p className="text-white font-bold text-sm leading-tight">{domain}</p>
<p className="text-blue-200 text-xs mt-0.5">Hosting Account</p>
</div>
<div className="px-4 py-3 space-y-3">
<UsageBar label="Disk Space" used={0} total={0} unit="MB" color="bg-blue-500" />
<UsageBar label="Bandwidth" used={0} total={0} unit="MB" color="bg-emerald-500" />
<UsageBar label="Email" used={0} total={0} unit="" color="bg-purple-500" />
<UsageBar label="Databases" used={0} total={0} unit="" color="bg-orange-500" />
</div>
</div>
{/* Quick stats */}
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-xl p-4 space-y-2">
<p className="text-[10px] font-bold text-gray-600 uppercase tracking-widest mb-3">Account Info</p>
{[
{ label: 'PHP Version', value: '8.3' },
{ label: 'Server', value: 'Apache 2.4' },
{ label: 'IP Address', value: '—' },
{ label: 'Home Dir', value: '/home/user' },
].map(r => (
<div key={r.label} className="flex justify-between items-center text-xs py-1 border-b border-[#252b3d] last:border-0">
<span className="text-gray-600">{r.label}</span>
<span className="text-gray-300 font-medium">{r.value}</span>
</div>
))}
</div>
{/* Quick links */}
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-xl overflow-hidden">
<p className="px-4 py-2.5 text-[10px] font-bold text-gray-600 uppercase tracking-widest border-b border-[#2a3044]">
Quick Links
</p>
{[
{ label: '🌐 Visit Website', href: '#' },
{ label: '📬 Open Webmail', href: '#' },
{ label: '🖥️ phpMyAdmin', href: '#' },
{ label: '📊 View Statistics', href: '#' },
].map(l => (
<a
key={l.label}
href={l.href}
className="flex items-center px-4 py-2.5 text-xs text-gray-400 hover:bg-[#252b3d] hover:text-white transition-colors border-b border-[#252b3d] last:border-0"
>
{l.label}
</a>
))}
</div>
</aside>
)
}
/* ── Home / feature grid ────────────────────────────────────────────────── */
function Home({ onNav }: { onNav: (p: Page) => void }) {
const [search, setSearch] = useState('')
const allFeatures = FEATURE_GROUPS.flatMap(g =>
g.features.map(f => ({ ...f, groupColor: g.color, groupLabel: g.label }))
)
const filtered = search.trim()
? allFeatures.filter(
f =>
f.label.toLowerCase().includes(search.toLowerCase()) ||
f.desc.toLowerCase().includes(search.toLowerCase())
)
: null
return (
<div className="space-y-6">
{/* Search bar */}
<div className="relative">
<span className="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-600 text-sm">🔍</span>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search features…"
className="w-full bg-[#1a1f2e] border border-[#2a3044] focus:border-blue-500/60 text-white rounded-xl pl-9 pr-4 py-3 text-sm placeholder-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-500/20 transition-all"
/>
{search && (
<button onClick={() => setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-600 hover:text-gray-400 text-xs">✕</button>
)}
</div>
{/* Search results */}
{filtered ? (
<div>
<p className="text-xs text-gray-600 mb-3">{filtered.length} result{filtered.length !== 1 ? 's' : ''} for "{search}"</p>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
{filtered.map(f => {
const c = COLOR_MAP[f.groupColor]
return (
<button
key={f.id}
onClick={() => onNav(f.id)}
className={`flex flex-col items-center text-center p-4 bg-[#1a1f2e] border border-[#2a3044] rounded-xl transition-all group ${c.tile}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-xl mb-2.5 ${c.icon}`}>
{f.icon}
</div>
<span className="text-xs font-semibold text-gray-300 group-hover:text-white leading-tight">{f.label}</span>
<span className="text-[10px] text-gray-600 group-hover:text-gray-400 mt-0.5 leading-tight">{f.desc}</span>
</button>
)
})}
</div>
{filtered.length === 0 && (
<div className="text-center py-12 text-gray-600 text-sm">No features found for "{search}"</div>
)}
</div>
) : (
/* Normal grouped view */
<div className="space-y-6">
{FEATURE_GROUPS.map(group => {
const c = COLOR_MAP[group.color]
return (
<div key={group.label}>
<div className="flex items-center gap-2 mb-3">
<h2 className="text-xs font-bold text-gray-500 uppercase tracking-widest">{group.label}</h2>
<div className="flex-1 h-px bg-[#2a3044]" />
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{group.features.map(f => (
<button
key={f.id}
onClick={() => onNav(f.id)}
className={`flex flex-col items-center text-center p-4 bg-[#1a1f2e] border border-[#2a3044] rounded-xl transition-all group ${c.tile}`}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-xl mb-2.5 ${c.icon}`}>
{f.icon}
</div>
<span className="text-xs font-semibold text-gray-300 group-hover:text-white leading-tight">{f.label}</span>
<span className="text-[10px] text-gray-600 group-hover:text-gray-400 mt-1 leading-tight">{f.desc}</span>
</button>
))}
</div>
</div>
)
})}
</div>
)}
</div>
)
}
/* ── Placeholder ────────────────────────────────────────────────────────── */
function Placeholder({ title, icon }: { title: string; icon: string }) {
return (
<div className="flex flex-col items-center justify-center h-72 text-center">
<div className="text-5xl mb-4">{icon}</div>
<h2 className="text-lg font-bold text-white mb-2">{title}</h2>
<p className="text-gray-600 text-sm max-w-sm">
This module is under active development and will be available in an upcoming release.
</p>
<div className="mt-4 px-3 py-1.5 bg-blue-500/10 border border-blue-500/20 rounded text-xs text-blue-400 font-semibold">
Coming Soon
</div>
</div>
)
}
/* ── Page meta ──────────────────────────────────────────────────────────── */
const PAGE_META: Record<Page, { title: string; icon: string }> = {
home: { title: 'My Control Panel', icon: '▦' },
domains: { title: 'Domains', icon: '🌐' },
subdomains: { title: 'Subdomains', icon: '🔗' },
redirects: { title: 'Redirects', icon: '↪️' },
email: { title: 'Email', icon: '✉️' },
emailAccounts:{ title: 'Email Accounts', icon: '✉️' },
webmail: { title: 'Webmail', icon: '📬' },
spamFilter: { title: 'Spam Filters', icon: '🚫' },
databases: { title: 'Databases', icon: '🗄️' },
mysql: { title: 'MySQL Databases', icon: '🗄️' },
phpmyadmin: { title: 'phpMyAdmin', icon: '🖥️' },
files: { title: 'Files', icon: '📁' },
fileManager: { title: 'File Manager', icon: '📁' },
ftp: { title: 'FTP Accounts', icon: '📡' },
backups: { title: 'Backups', icon: '💾' },
security: { title: 'Security', icon: '🔒' },
ssl: { title: 'SSL / TLS', icon: '🔒' },
passwords: { title: 'Password', icon: '🔑' },
ssh: { title: 'SSH Keys', icon: '🗝️' },
software: { title: 'Software', icon: '🐘' },
php: { title: 'PHP Version', icon: '🐘' },
wordpress: { title: 'WordPress', icon: '🌀' },
stats: { title: 'Statistics', icon: '📊' },
logs: { title: 'Logs', icon: '📜' },
}
/* ── Client panel shell ─────────────────────────────────────────────────── */
function ClientPanel() {
const [page, setPage] = useState<Page>('home')
const [loggedIn, setLoggedIn] = useState(true)
if (!loggedIn) return null
const meta = PAGE_META[page]
const domain = 'yourdomain.com'
function renderPage() {
if (page === 'home') return <Home onNav={setPage} />
return <Placeholder title={meta.title} icon={meta.icon} />
}
if (isLoggedIn) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 text-white">
<nav className="border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<h1 className="text-xl font-bold">Nix<span className="text-emerald-500">Client</span></h1>
return (
<div className="min-h-screen bg-[#0f1520] text-white flex flex-col">
{/* Top nav */}
<header className="bg-[#1a1f2e] border-b border-[#2a3044] sticky top-0 z-40 flex-shrink-0">
<div className="h-14 max-w-screen-2xl mx-auto px-6 flex items-center justify-between gap-4">
{/* Logo */}
<div className="flex items-center gap-3 flex-shrink-0">
<div className="w-7 h-7 rounded-lg bg-blue-600 flex items-center justify-center text-white text-sm font-black">N</div>
<span className="text-base font-black text-white hidden sm:block">
Nix<span className="text-blue-400">Client</span>
</span>
</div>
{/* Center nav */}
<nav className="hidden md:flex items-center gap-1">
{[
{ id: 'home' as Page, label: 'Home' },
{ id: 'email' as Page, label: 'Email' },
{ id: 'mysql' as Page, label: 'Databases' },
{ id: 'ssl' as Page, label: 'Security' },
{ id: 'stats' as Page, label: 'Stats' },
].map(l => (
<button
key={l.id}
onClick={() => setPage(l.id)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
page === l.id
? 'bg-blue-600 text-white'
: 'text-gray-400 hover:text-white hover:bg-[#252b3d]'
}`}
>
{l.label}
</button>
))}
</nav>
{/* Right: account / logout */}
<div className="flex items-center gap-3">
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 bg-[#0f1520] border border-[#2a3044] rounded-lg">
<span className="w-5 h-5 rounded-full bg-blue-600 flex items-center justify-center text-white text-[10px] font-bold">
{domain.charAt(0).toUpperCase()}
</span>
<span className="text-xs text-gray-300 font-medium max-w-[120px] truncate">{domain}</span>
</div>
<button
onClick={() => setIsLoggedIn(false)}
className="px-4 py-2 rounded-lg border border-gray-700 text-gray-300 hover:border-gray-500 hover:text-white transition-colors text-sm"
onClick={() => setLoggedIn(false)}
className="px-3 py-1.5 rounded-lg border border-[#2a3044] text-gray-400 hover:border-gray-600 hover:text-white transition-colors text-xs font-medium"
>
Sign out
</button>
</div>
</nav>
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[
{ title: 'My Domains', icon: '🌐', count: '0' },
{ title: 'Email Accounts', icon: '📧', count: '0' },
{ title: 'Databases', icon: '💾', count: '0' },
{ title: 'Backups', icon: '💾', count: '0' },
].map((item) => (
<div key={item.title} className="bg-gray-900/50 border border-gray-800 rounded-xl p-6">
<div className="text-4xl mb-3">{item.icon}</div>
<h3 className="font-semibold text-white mb-2">{item.title}</h3>
<p className="text-3xl font-bold text-emerald-500">{item.count}</p>
</div>
))}
</div>
<div className="mt-12 bg-emerald-900/20 border border-emerald-500/20 rounded-2xl p-8">
<h2 className="text-2xl font-bold text-white mb-2">Welcome to Your Account</h2>
<p className="text-gray-400">
Manage your domains, emails, databases, and more — all from one dashboard.
</p>
</div>
</main>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div className="w-full max-w-md px-4">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
Nix<span className="text-emerald-500">Client</span>
</h1>
<p className="text-gray-400">Your Hosting Control Panel</p>
</div>
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-8">
<form onSubmit={handleLogin} className="space-y-4">
{/* Breadcrumb */}
{page !== 'home' && (
<div className="border-t border-[#252b3d] px-6 py-1.5 flex items-center gap-1.5 text-[11px] text-gray-600 max-w-screen-2xl mx-auto">
<button onClick={() => setPage('home')} className="hover:text-blue-400 transition-colors">Home</button>
<span>/</span>
<span className="text-gray-400">{meta.title}</span>
</div>
)}
</header>
{/* Body */}
<div className="flex-1 max-w-screen-2xl mx-auto w-full px-6 py-6 flex gap-6">
{/* Sidebar */}
<AccountSidebar domain={domain} />
{/* Main content */}
<main className="flex-1 min-w-0">
{renderPage()}
</main>
</div>
{/* Footer */}
<footer className="border-t border-[#2a3044] px-6 py-3 text-center text-[10px] text-gray-700">
NixPanel Client v0.1.0 · {domain} · Powered by NixServer
</footer>
</div>
)
}
/* ── Login ──────────────────────────────────────────────────────────────── */
export default function App() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (!username || !password) {
setError('Please enter your username and password.')
return
}
setLoading(true)
setError('')
setTimeout(() => {
setLoading(false)
setIsLoggedIn(true)
}, 600)
}
if (isLoggedIn) return <ClientPanel />
return (
<div className="min-h-screen flex items-center justify-center bg-[#0f1520] relative overflow-hidden">
{/* Background radial */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-950/40 via-[#0f1520] to-[#0f1520]" />
<div className="relative w-full max-w-sm px-4">
{/* Logo */}
<div className="text-center mb-8">
<div className="inline-flex items-center gap-3 mb-3">
<div className="w-12 h-12 rounded-xl bg-blue-600 flex items-center justify-center text-white text-2xl font-black shadow-lg shadow-blue-900/50">N</div>
<div className="text-left">
<h1 className="text-2xl font-black text-white leading-none">Nix<span className="text-blue-400">Client</span></h1>
<p className="text-xs text-gray-600 font-medium">Hosting Control Panel</p>
</div>
</div>
</div>
{/* Card */}
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-2xl shadow-2xl overflow-hidden">
{/* Header band */}
<div className="bg-gradient-to-r from-blue-700 to-blue-600 px-6 py-4">
<h2 className="text-white font-bold text-base">Sign in to your account</h2>
<p className="text-blue-200 text-xs mt-0.5">NixClient Hosting Control Panel</p>
</div>
<form onSubmit={handleLogin} className="p-6 space-y-4">
{error && (
<div className="px-4 py-3 bg-red-950/60 border border-red-800/50 rounded-lg text-red-400 text-sm flex items-center gap-2">
<span>⚠️</span> {error}
</div>
)}
<div>
<label className="block text-sm text-gray-400 mb-1.5">Username</label>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Username</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
value={username}
onChange={e => setUsername(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-emerald-500 transition-colors"
autoFocus
autoComplete="username"
placeholder="username"
className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-700 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-all"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1.5">Password</label>
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1.5">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={e => setPassword(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-emerald-500 transition-colors"
autoComplete="current-password"
placeholder="••••••••"
className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-700 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-all"
/>
</div>
<button
type="submit"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-semibold text-sm transition-colors mt-6"
disabled={loading}
className="w-full py-2.5 rounded-lg bg-blue-600 hover:bg-blue-700 active:bg-blue-800 disabled:opacity-60 text-white font-bold text-sm transition-all shadow-lg shadow-blue-900/40 flex items-center justify-center gap-2"
>
Sign in
{loading ? (
<>
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Signing in…
</>
) : (
'Sign in'
)}
</button>
</form>
</div>
<p className="text-center text-sm text-gray-500 mt-6">
NixPanel Client — Manage your hosting account
</p>
<div className="border-t border-[#2a3044] px-6 py-3 bg-[#0f1520] text-[10px] text-gray-700 text-center">
NixPanel Client v0.1.0 · Manage your hosting account
</div>
</div>
</div>
</div>
)
+692 -87
View File
@@ -1,109 +1,714 @@
import { useState } from 'react'
export default function App() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
/* ── Types ──────────────────────────────────────────────────────────────── */
type Page =
| 'dashboard' | 'accounts' | 'createAccount' | 'listAccounts'
| 'domains' | 'dns' | 'databases' | 'email'
| 'ssl' | 'backups' | 'firewall'
| 'apache' | 'php' | 'services'
| 'settings' | 'logs' | 'packages'
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (email && password) {
setIsLoggedIn(true)
}
}
/* ── Sidebar nav definition ─────────────────────────────────────────────── */
type NavGroup = { label: string; items: { id: Page; label: string; icon: string }[] }
if (isLoggedIn) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 text-white">
<nav className="border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<h1 className="text-xl font-bold">Nix<span className="text-blue-500">Server</span></h1>
<button
onClick={() => setIsLoggedIn(false)}
className="px-4 py-2 rounded-lg border border-gray-700 text-gray-300 hover:border-gray-500 hover:text-white transition-colors text-sm"
>
Sign out
</button>
</div>
</nav>
const NAV_GROUPS: NavGroup[] = [
{
label: 'Overview',
items: [
{ id: 'dashboard', label: 'Dashboard', icon: '▦' },
],
},
{
label: 'Account Management',
items: [
{ id: 'listAccounts', label: 'List Accounts', icon: '📋' },
{ id: 'createAccount',label: 'Create Account', icon: '➕' },
{ id: 'packages', label: 'Feature Packages', icon: '📦' },
],
},
{
label: 'Domains & DNS',
items: [
{ id: 'domains', label: 'Zone Manager', icon: '🌐' },
{ id: 'dns', label: 'DNS Records', icon: '🔗' },
],
},
{
label: 'Databases',
items: [
{ id: 'databases', label: 'MySQL / MariaDB', icon: '🗄️' },
],
},
{
label: 'Email',
items: [
{ id: 'email', label: 'Email Routing', icon: '✉️' },
],
},
{
label: 'Security',
items: [
{ id: 'ssl', label: 'SSL / TLS', icon: '🔒' },
{ id: 'firewall', label: 'Firewall (CSF)', icon: '🛡️' },
],
},
{
label: 'Server',
items: [
{ id: 'apache', label: 'Apache Config', icon: '⚙️' },
{ id: 'php', label: 'PHP Manager', icon: '🐘' },
{ id: 'services', label: 'Service Manager', icon: '🔧' },
{ id: 'backups', label: 'Backup Manager', icon: '💾' },
{ id: 'logs', label: 'System Logs', icon: '📜' },
{ id: 'settings', label: 'Server Config', icon: '🔩' },
],
},
]
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ title: 'Accounts', icon: '👥', count: '0' },
{ title: 'Domains', icon: '🌐', count: '0' },
{ title: 'Databases', icon: '💾', count: '0' },
{ title: 'Email Accounts', icon: '📧', count: '0' },
{ title: 'SSL Certificates', icon: '🔒', count: '0' },
{ title: 'Backups', icon: '💾', count: '0' },
].map((item) => (
<div key={item.title} className="bg-gray-900/50 border border-gray-800 rounded-xl p-6">
<div className="text-4xl mb-3">{item.icon}</div>
<h3 className="font-semibold text-white mb-2">{item.title}</h3>
<p className="text-3xl font-bold text-blue-500">{item.count}</p>
</div>
/* ── Sidebar ────────────────────────────────────────────────────────────── */
function Sidebar({
current, onNav, onLogout, collapsed, onToggle,
}: {
current: Page; onNav: (p: Page) => void; onLogout: () => void
collapsed: boolean; onToggle: () => void
}) {
return (
<aside
className={`flex-shrink-0 bg-[#1a1f2e] border-r border-[#2a3044] flex flex-col min-h-screen transition-all duration-200 ${
collapsed ? 'w-14' : 'w-56'
}`}
>
{/* Logo row */}
<div className="h-14 flex items-center justify-between px-3 border-b border-[#2a3044]">
{!collapsed && (
<span className="text-base font-black tracking-tight text-white">
Nix<span className="text-orange-400">Server</span>
<span className="ml-1.5 text-[9px] font-bold bg-orange-500 text-white rounded px-1 py-0.5 uppercase tracking-wide align-middle">WHM</span>
</span>
)}
<button
onClick={onToggle}
className="text-gray-500 hover:text-white p-1 rounded transition-colors ml-auto"
title={collapsed ? 'Expand' : 'Collapse'}
>
{collapsed ? '▶' : '◀'}
</button>
</div>
{/* Nav groups */}
<nav className="flex-1 overflow-y-auto py-2 scrollbar-thin">
{NAV_GROUPS.map(group => (
<div key={group.label} className="mb-1">
{!collapsed && (
<p className="px-3 pt-3 pb-1 text-[9px] font-bold text-gray-600 uppercase tracking-widest">
{group.label}
</p>
)}
{group.items.map(item => (
<button
key={item.id}
onClick={() => onNav(item.id)}
title={collapsed ? item.label : undefined}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium transition-all ${
current === item.id
? 'bg-orange-500/20 text-orange-400 border-r-2 border-orange-400'
: 'text-gray-400 hover:bg-[#252b3d] hover:text-white'
}`}
>
<span className="text-sm w-4 text-center flex-shrink-0">{item.icon}</span>
{!collapsed && <span className="truncate">{item.label}</span>}
</button>
))}
</div>
))}
</nav>
<div className="mt-12 bg-blue-900/20 border border-blue-500/20 rounded-2xl p-8">
<h2 className="text-2xl font-bold text-white mb-2">Welcome to NixServer</h2>
<p className="text-gray-400">
This is a placeholder dashboard. Full server management features are coming soon.
</p>
{/* User / logout */}
<div className="border-t border-[#2a3044] p-2">
{!collapsed && (
<div className="flex items-center gap-2 px-1 py-1.5 mb-1">
<div className="w-7 h-7 rounded-full bg-orange-500 flex items-center justify-center text-white text-xs font-bold flex-shrink-0">A</div>
<div className="min-w-0">
<p className="text-xs font-semibold text-white truncate">admin</p>
<p className="text-[10px] text-gray-500">root</p>
</div>
</div>
</main>
)}
<button
onClick={onLogout}
title="Sign out"
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded text-xs text-gray-500 hover:bg-[#252b3d] hover:text-red-400 transition-colors ${
collapsed ? 'justify-center' : ''
}`}
>
<span>⏻</span>
{!collapsed && 'Sign out'}
</button>
</div>
)
}
</aside>
)
}
/* ── Stat bar (top of dashboard) ────────────────────────────────────────── */
function StatBar() {
const stats = [
{ label: 'Hosting Accounts', value: '0', unit: '', color: 'text-orange-400' },
{ label: 'Domains', value: '0', unit: '', color: 'text-blue-400' },
{ label: 'Databases', value: '0', unit: '', color: 'text-purple-400' },
{ label: 'Email Accounts', value: '0', unit: '', color: 'text-cyan-400' },
{ label: 'Disk Used', value: '—', unit: '', color: 'text-emerald-400' },
{ label: 'RAM Used', value: '—', unit: '', color: 'text-yellow-400' },
]
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div className="w-full max-w-md px-4">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
Nix<span className="text-blue-500">Server</span>
</h1>
<p className="text-gray-400">Admin Control Panel</p>
<div className="grid grid-cols-3 lg:grid-cols-6 gap-0 border border-[#2a3044] rounded-lg overflow-hidden">
{stats.map((s, i) => (
<div
key={s.label}
className={`bg-[#1a1f2e] px-4 py-3 ${i < stats.length - 1 ? 'border-r border-[#2a3044]' : ''}`}
>
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">{s.label}</p>
<p className={`text-xl font-black mt-0.5 ${s.color}`}>{s.value}<span className="text-xs font-normal text-gray-500 ml-1">{s.unit}</span></p>
</div>
))}
</div>
)
}
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-8">
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1.5">Username</label>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
placeholder="admin"
/>
</div>
/* ── Service status table ───────────────────────────────────────────────── */
type SvcStatus = 'running' | 'stopped' | 'warning'
function ServiceTable() {
const services: { name: string; status: SvcStatus; pid?: string; uptime?: string }[] = [
{ name: 'Apache HTTP Server', status: 'running', pid: '1234', uptime: '2d 4h' },
{ name: 'MariaDB', status: 'running', pid: '1238', uptime: '2d 4h' },
{ name: 'PowerDNS', status: 'running', pid: '1242', uptime: '2d 4h' },
{ name: 'Exim (SMTP)', status: 'running', pid: '1251', uptime: '2d 4h' },
{ name: 'Dovecot (IMAP)', status: 'running', pid: '1265', uptime: '2d 4h' },
{ name: 'SpamAssassin', status: 'running', pid: '1280', uptime: '2d 4h' },
{ name: 'Fail2ban', status: 'running', pid: '1291', uptime: '2d 4h' },
{ name: 'ProFTPD', status: 'stopped' },
]
const badge: Record<SvcStatus, string> = {
running: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30',
stopped: 'bg-red-500/20 text-red-400 border border-red-500/30',
warning: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30',
}
return (
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#2a3044] flex items-center justify-between">
<h3 className="text-sm font-bold text-white">Service Manager</h3>
<span className="text-xs text-gray-500">7 / 8 running</span>
</div>
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#2a3044]">
<th className="text-left px-4 py-2 text-[10px] font-bold text-gray-500 uppercase tracking-wider">Service</th>
<th className="text-left px-4 py-2 text-[10px] font-bold text-gray-500 uppercase tracking-wider">Status</th>
<th className="text-left px-4 py-2 text-[10px] font-bold text-gray-500 uppercase tracking-wider hidden md:table-cell">PID</th>
<th className="text-left px-4 py-2 text-[10px] font-bold text-gray-500 uppercase tracking-wider hidden md:table-cell">Uptime</th>
<th className="text-left px-4 py-2 text-[10px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{services.map(svc => (
<tr key={svc.name} className="border-b border-[#252b3d] hover:bg-[#252b3d]/50 transition-colors">
<td className="px-4 py-2.5 text-gray-300 font-medium">{svc.name}</td>
<td className="px-4 py-2.5">
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-semibold ${badge[svc.status]}`}>
<span className={`w-1.5 h-1.5 rounded-full ${svc.status === 'running' ? 'bg-emerald-400' : svc.status === 'stopped' ? 'bg-red-400' : 'bg-yellow-400'}`} />
{svc.status.charAt(0).toUpperCase() + svc.status.slice(1)}
</span>
</td>
<td className="px-4 py-2.5 text-gray-500 text-xs hidden md:table-cell">{svc.pid || '—'}</td>
<td className="px-4 py-2.5 text-gray-500 text-xs hidden md:table-cell">{svc.uptime || '—'}</td>
<td className="px-4 py-2.5">
<div className="flex gap-1.5">
<button className="px-2 py-0.5 rounded text-[11px] bg-[#2a3044] hover:bg-emerald-500/20 hover:text-emerald-400 text-gray-400 transition-colors">Start</button>
<button className="px-2 py-0.5 rounded text-[11px] bg-[#2a3044] hover:bg-red-500/20 hover:text-red-400 text-gray-400 transition-colors">Stop</button>
<button className="px-2 py-0.5 rounded text-[11px] bg-[#2a3044] hover:bg-orange-500/20 hover:text-orange-400 text-gray-400 transition-colors">Restart</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
/* ── Quick action tiles (cPanel icon-grid style) ────────────────────────── */
function QuickActions({ onNav }: { onNav: (p: Page) => void }) {
const actions: { label: string; icon: string; page: Page; desc: string }[] = [
{ label: 'Create Account', icon: '👤', page: 'createAccount', desc: 'Add hosting account' },
{ label: 'Zone Manager', icon: '🌐', page: 'domains', desc: 'Manage DNS zones' },
{ label: 'New Database', icon: '🗄️', page: 'databases', desc: 'Create MariaDB DB' },
{ label: 'Issue SSL', icon: '🔒', page: 'ssl', desc: "Let's Encrypt cert" },
{ label: 'Run Backup', icon: '💾', page: 'backups', desc: 'Full server backup' },
{ label: 'PHP Manager', icon: '🐘', page: 'php', desc: 'Switch PHP version' },
{ label: 'Firewall', icon: '🛡️', page: 'firewall', desc: 'Edit CSF rules' },
{ label: 'System Logs', icon: '📜', page: 'logs', desc: 'Apache, error logs' },
]
return (
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#2a3044]">
<h3 className="text-sm font-bold text-white">Quick Actions</h3>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-0">
{actions.map((a, i) => (
<button
key={a.label}
onClick={() => onNav(a.page)}
className={`flex flex-col items-center text-center p-4 hover:bg-orange-500/10 hover:border-orange-500/20 transition-all group
${i % 4 !== 3 ? 'border-r border-[#2a3044]' : ''}
${i < actions.length - 4 ? 'border-b border-[#2a3044]' : ''}
`}
>
<span className="text-2xl mb-2 group-hover:scale-110 transition-transform">{a.icon}</span>
<span className="text-xs font-semibold text-gray-300 group-hover:text-white leading-tight">{a.label}</span>
<span className="text-[10px] text-gray-600 group-hover:text-gray-400 mt-0.5 leading-tight">{a.desc}</span>
</button>
))}
</div>
</div>
)
}
/* ── Server info bar ────────────────────────────────────────────────────── */
function ServerInfoBar() {
const info = [
{ label: 'Hostname', value: 'server.example.com' },
{ label: 'OS', value: 'Ubuntu 24.04 LTS' },
{ label: 'Kernel', value: '6.8.0-51-generic' },
{ label: 'PHP', value: '8.3 / 8.2' },
{ label: 'MariaDB', value: '10.11' },
{ label: 'Apache', value: '2.4' },
{ label: 'NixPanel', value: 'v0.1.0' },
]
return (
<div className="bg-[#0f1520] border border-[#2a3044] rounded-lg px-4 py-3 flex flex-wrap gap-x-6 gap-y-2">
{info.map(i => (
<div key={i.label} className="flex items-center gap-2">
<span className="text-[10px] font-bold text-gray-600 uppercase tracking-wider">{i.label}:</span>
<span className="text-xs text-gray-300 font-medium">{i.value}</span>
</div>
))}
</div>
)
}
/* ── Dashboard page ─────────────────────────────────────────────────────── */
function Dashboard({ onNav }: { onNav: (p: Page) => void }) {
return (
<div className="space-y-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-black text-white">Server Overview</h1>
<p className="text-xs text-gray-500 mt-0.5">Welcome back, admin — NixServer WHM</p>
</div>
<span className="flex items-center gap-1.5 text-xs text-emerald-400 font-semibold bg-emerald-500/10 border border-emerald-500/20 px-3 py-1.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
All systems operational
</span>
</div>
<StatBar />
<ServerInfoBar />
<div className="grid grid-cols-1 xl:grid-cols-2 gap-5">
<ServiceTable />
<QuickActions onNav={onNav} />
</div>
</div>
)
}
/* ── Account list page ──────────────────────────────────────────────────── */
function ListAccounts() {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-lg font-black text-white">Hosting Accounts</h1>
<button className="px-3 py-1.5 bg-orange-500 hover:bg-orange-600 text-white text-sm font-semibold rounded transition-colors">
+ Create Account
</button>
</div>
{/* Filter bar */}
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-lg p-3 flex gap-3">
<input
type="text"
placeholder="Search accounts, domains, usernames…"
className="flex-1 bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/50"
/>
<select className="bg-[#0f1520] border border-[#2a3044] text-gray-300 rounded px-3 py-2 text-sm focus:outline-none">
<option>All Packages</option>
</select>
<select className="bg-[#0f1520] border border-[#2a3044] text-gray-300 rounded px-3 py-2 text-sm focus:outline-none">
<option>All Status</option>
<option>Active</option>
<option>Suspended</option>
</select>
</div>
{/* Table */}
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[#2a3044] bg-[#0f1520]">
{['Username', 'Domain', 'Package', 'Disk', 'BW', 'Status', 'Actions'].map(h => (
<th key={h} className="text-left px-4 py-2.5 text-[10px] font-bold text-gray-500 uppercase tracking-wider">{h}</th>
))}
</tr>
</thead>
<tbody>
<tr>
<td colSpan={7} className="px-4 py-16 text-center">
<div className="text-3xl mb-3">👤</div>
<p className="text-gray-500 text-sm">No accounts yet. <button className="text-orange-400 hover:underline">Create your first account →</button></p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}
/* ── Create account form ────────────────────────────────────────────────── */
function CreateAccount() {
return (
<div className="max-w-2xl space-y-5">
<h1 className="text-lg font-black text-white">Create Hosting Account</h1>
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-lg divide-y divide-[#2a3044]">
{/* Domain */}
<div className="p-5 space-y-4">
<h2 className="text-xs font-bold text-orange-400 uppercase tracking-widest">Domain Information</h2>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Domain Name</label>
<input type="text" placeholder="example.com" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
</div>
</div>
{/* Credentials */}
<div className="p-5 space-y-4">
<h2 className="text-xs font-bold text-orange-400 uppercase tracking-widest">Account Credentials</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Username</label>
<input type="text" placeholder="username" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
<div>
<label className="block text-sm text-gray-400 mb-1.5">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
placeholder="••••••••"
/>
</div>
<button
type="submit"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm transition-colors mt-6"
>
Sign in
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Password</label>
<input type="password" placeholder="••••••••" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
<div className="col-span-2">
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Email Address</label>
<input type="email" placeholder="[email protected]" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
</div>
</div>
{/* Package & limits */}
<div className="p-5 space-y-4">
<h2 className="text-xs font-bold text-orange-400 uppercase tracking-widest">Resource Limits</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Package</label>
<select className="w-full bg-[#0f1520] border border-[#2a3044] text-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:border-orange-500/60">
<option>Default</option>
</select>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Disk Quota (MB)</label>
<input type="number" placeholder="10240" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Bandwidth (MB/mo)</label>
<input type="number" placeholder="Unlimited" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 mb-1.5">Max Databases</label>
<input type="number" placeholder="Unlimited" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
</div>
</div>
</div>
<div className="p-5 flex gap-3">
<button className="px-5 py-2 bg-orange-500 hover:bg-orange-600 text-white text-sm font-bold rounded transition-colors">
Create Account
</button>
<button className="px-5 py-2 bg-[#0f1520] border border-[#2a3044] hover:border-gray-500 text-gray-400 hover:text-white text-sm font-medium rounded transition-colors">
Cancel
</button>
</div>
</div>
</div>
)
}
/* ── Generic placeholder for unbuilt modules ────────────────────────────── */
function Placeholder({ title, icon }: { title: string; icon: string }) {
return (
<div className="flex flex-col items-center justify-center h-72 text-center">
<div className="text-5xl mb-4">{icon}</div>
<h2 className="text-lg font-bold text-white mb-2">{title}</h2>
<p className="text-gray-600 text-sm max-w-sm">
This module is under active development and will be available in an upcoming release.
</p>
<div className="mt-4 px-3 py-1.5 bg-orange-500/10 border border-orange-500/20 rounded text-xs text-orange-400 font-semibold">
Coming Soon
</div>
</div>
)
}
/* ── Page icon map ──────────────────────────────────────────────────────── */
const PAGE_META: Record<Page, { title: string; icon: string }> = {
dashboard: { title: 'Server Dashboard', icon: '▦' },
accounts: { title: 'Accounts', icon: '👤' },
listAccounts: { title: 'List Accounts', icon: '📋' },
createAccount: { title: 'Create Account', icon: '➕' },
packages: { title: 'Feature Packages', icon: '📦' },
domains: { title: 'Zone Manager', icon: '🌐' },
dns: { title: 'DNS Records', icon: '🔗' },
databases: { title: 'MySQL / MariaDB', icon: '🗄️' },
email: { title: 'Email Routing', icon: '✉️' },
ssl: { title: 'SSL / TLS Manager', icon: '🔒' },
backups: { title: 'Backup Manager', icon: '💾' },
firewall: { title: 'Firewall (CSF)', icon: '🛡️' },
apache: { title: 'Apache Config', icon: '⚙️' },
php: { title: 'PHP Manager', icon: '🐘' },
services: { title: 'Service Manager', icon: '🔧' },
settings: { title: 'Server Config', icon: '🔩' },
logs: { title: 'System Logs', icon: '📜' },
}
/* ── Admin panel shell ──────────────────────────────────────────────────── */
function AdminPanel() {
const [page, setPage] = useState<Page>('dashboard')
const [loggedIn, setLoggedIn] = useState(true)
const [collapsed, setCollapsed] = useState(false)
if (!loggedIn) return null
const meta = PAGE_META[page]
function renderPage() {
switch (page) {
case 'dashboard': return <Dashboard onNav={setPage} />
case 'listAccounts': return <ListAccounts />
case 'createAccount': return <CreateAccount />
default: return <Placeholder title={meta.title} icon={meta.icon} />
}
}
return (
<div className="flex min-h-screen bg-[#0f1520] text-white">
<Sidebar
current={page}
onNav={setPage}
onLogout={() => setLoggedIn(false)}
collapsed={collapsed}
onToggle={() => setCollapsed(c => !c)}
/>
<div className="flex-1 flex flex-col min-w-0">
{/* Top bar */}
<header className="h-14 border-b border-[#2a3044] bg-[#1a1f2e] flex items-center px-6 justify-between gap-4 flex-shrink-0">
<div className="flex items-center gap-2 min-w-0">
<span className="text-gray-600 text-sm">{meta.icon}</span>
<h2 className="font-semibold text-gray-200 text-sm truncate">{meta.title}</h2>
</div>
<div className="flex items-center gap-3">
{/* Server info pills */}
<div className="hidden lg:flex items-center gap-2">
<span className="text-xs bg-[#0f1520] border border-[#2a3044] text-gray-500 px-2 py-1 rounded">
Ubuntu 24.04
</span>
<span className="text-xs bg-[#0f1520] border border-[#2a3044] text-gray-500 px-2 py-1 rounded">
Apache 2.4
</span>
<span className="text-xs bg-[#0f1520] border border-[#2a3044] text-gray-500 px-2 py-1 rounded">
PHP 8.3
</span>
</div>
{/* Alerts */}
<button className="relative text-gray-500 hover:text-white p-1.5 transition-colors">
🔔
<span className="absolute top-0.5 right-0.5 w-2 h-2 bg-orange-500 rounded-full" />
</button>
</form>
{/* Avatar */}
<div className="flex items-center gap-2 pl-3 border-l border-[#2a3044]">
<div className="w-7 h-7 rounded-full bg-orange-500 flex items-center justify-center text-white text-xs font-bold">A</div>
<span className="text-sm font-semibold text-gray-300 hidden sm:block">admin</span>
</div>
</div>
</header>
{/* Breadcrumb */}
{page !== 'dashboard' && (
<div className="px-6 py-2 border-b border-[#2a3044] bg-[#0f1520] flex items-center gap-1.5 text-xs text-gray-600">
<button onClick={() => setPage('dashboard')} className="hover:text-orange-400 transition-colors">Dashboard</button>
<span>/</span>
<span className="text-gray-400">{meta.title}</span>
</div>
)}
{/* Page content */}
<main className="flex-1 p-6 overflow-auto">
{renderPage()}
</main>
{/* Footer */}
<footer className="border-t border-[#2a3044] px-6 py-2 flex items-center justify-between text-[10px] text-gray-700">
<span>NixPanel WHM v0.1.0 · Ubuntu 24.04 LTS</span>
<span>© 2025 NixPanel</span>
</footer>
</div>
</div>
)
}
/* ── Login page ─────────────────────────────────────────────────────────── */
export default function App() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (!username || !password) {
setError('Username and password are required.')
return
}
setLoading(true)
setError('')
setTimeout(() => {
setLoading(false)
setIsLoggedIn(true)
}, 600)
}
if (isLoggedIn) return <AdminPanel />
return (
<div className="min-h-screen flex bg-[#0f1520]">
{/* Left branding panel */}
<div className="hidden lg:flex flex-col justify-between w-80 bg-[#1a1f2e] border-r border-[#2a3044] p-10">
<div>
<div className="flex items-center gap-3 mb-10">
<div className="w-10 h-10 rounded-lg bg-orange-500 flex items-center justify-center text-white text-xl font-black shadow-lg shadow-orange-900/40">N</div>
<div>
<span className="text-xl font-black text-white">Nix<span className="text-orange-400">Server</span></span>
<div className="text-[10px] font-bold text-gray-600 uppercase tracking-wider">WHM Control Panel</div>
</div>
</div>
<div className="space-y-5">
{[
{ icon: '🌐', title: 'Full DNS Control', desc: 'PowerDNS-backed zone management' },
{ icon: '🔒', title: 'Auto SSL/TLS', desc: "Let's Encrypt for all domains" },
{ icon: '🛡️', title: 'Built-in Firewall', desc: 'CSF/Fail2ban integration' },
{ icon: '💾', title: 'Automated Backups', desc: 'Scheduled full & incremental' },
{ icon: '🐘', title: 'Multi-PHP Support', desc: 'PHP 8.0 – 8.3 per account' },
].map(f => (
<div key={f.title} className="flex items-start gap-3">
<span className="text-lg mt-0.5">{f.icon}</span>
<div>
<p className="text-sm font-semibold text-gray-200">{f.title}</p>
<p className="text-xs text-gray-600">{f.desc}</p>
</div>
</div>
))}
</div>
</div>
<p className="text-[10px] text-gray-700">NixPanel WHM v0.1.0 · Secured by Fail2ban</p>
</div>
{/* Login form */}
<div className="flex-1 flex items-center justify-center p-6">
<div className="w-full max-w-sm">
{/* Mobile logo */}
<div className="lg:hidden text-center mb-8">
<span className="text-3xl font-black text-white">Nix<span className="text-orange-400">Server</span></span>
<p className="text-xs text-gray-600 mt-1">WHM Control Panel</p>
</div>
<p className="text-center text-sm text-gray-500 mt-6">
NixPanel Server Admin — Login with your credentials
</p>
<div className="bg-[#1a1f2e] border border-[#2a3044] rounded-xl shadow-2xl overflow-hidden">
{/* Card header */}
<div className="bg-orange-500 px-6 py-4">
<h1 className="text-white font-bold text-base">Administrator Login</h1>
<p className="text-orange-200 text-xs mt-0.5">NixServer WHM · Root Access</p>
</div>
<form onSubmit={handleLogin} className="p-6 space-y-4">
{error && (
<div className="px-4 py-3 bg-red-950/60 border border-red-800/50 rounded-lg text-red-400 text-sm flex items-center gap-2">
<span>⚠️</span> {error}
</div>
)}
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5">
Username
</label>
<input
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
required
autoFocus
autoComplete="username"
placeholder="admin"
className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-700 focus:outline-none focus:border-orange-500/60 focus:ring-1 focus:ring-orange-500/20 transition-all"
/>
</div>
<div>
<label className="block text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1.5">
Password
</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
autoComplete="current-password"
placeholder="••••••••••••"
className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-700 focus:outline-none focus:border-orange-500/60 focus:ring-1 focus:ring-orange-500/20 transition-all"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 rounded-lg bg-orange-500 hover:bg-orange-600 active:bg-orange-700 disabled:opacity-60 text-white font-bold text-sm transition-all shadow-lg shadow-orange-900/40 flex items-center justify-center gap-2"
>
{loading ? (
<>
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Authenticating…
</>
) : (
'Sign in to WHM'
)}
</button>
</form>
<div className="border-t border-[#2a3044] px-6 py-3 bg-[#0f1520] flex items-center justify-between text-[10px] text-gray-700">
<span>Secured by Fail2ban</span>
<span>NixPanel v0.1</span>
</div>
</div>
</div>
</div>
</div>
)