feat: add NixServer and NixClient React UIs, wire into installer
- nixserver/: React 18 + TypeScript + Vite + Tailwind - Login page + placeholder admin dashboard with card grid - Blue accent color (#3b82f6) - Full Tailwind + PostCSS + TypeScript setup - nixclient/: identical structure, user-facing panel - Green accent color (#10b981) - Same build tooling as NixServer - install.sh: new 'Building NixServer & NixClient UIs' step - npm install + npm run build for each - Outputs to /opt/nixpanel/nixserver/dist and /opt/nixpanel/nixclient/dist - Both panels immediately accessible via Apache on 2087 and 2083 - Skips gracefully if source not present (early dev)
This commit is contained in:
+32
-3
@@ -190,19 +190,48 @@ if ! step_done "source"; then
|
||||
fi
|
||||
success "Source ready"
|
||||
|
||||
# ── 8. Build panel binary ─────────────────────────────────────────────────────
|
||||
# ── 8. Build panel UIs (React apps) ───────────────────────────────────────────
|
||||
step "Building NixServer & NixClient UIs"
|
||||
if ! step_done "build_uis"; then
|
||||
cd "$SRC_DIR"
|
||||
|
||||
# Build NixServer (WHM admin panel)
|
||||
if [[ -f "nixserver/package.json" ]]; then
|
||||
info "Building NixServer…"
|
||||
cd "$SRC_DIR/nixserver"
|
||||
npm install --legacy-peer-deps --quiet 2>/dev/null || true
|
||||
npm run build --silent 2>/dev/null
|
||||
mkdir -p "$INSTALL_DIR/nixserver/dist"
|
||||
cp -r dist/* "$INSTALL_DIR/nixserver/dist/" 2>/dev/null || true
|
||||
success "NixServer built"
|
||||
fi
|
||||
|
||||
# Build NixClient (cPanel user panel)
|
||||
if [[ -f "nixclient/package.json" ]]; then
|
||||
info "Building NixClient…"
|
||||
cd "$SRC_DIR/nixclient"
|
||||
npm install --legacy-peer-deps --quiet 2>/dev/null || true
|
||||
npm run build --silent 2>/dev/null
|
||||
mkdir -p "$INSTALL_DIR/nixclient/dist"
|
||||
cp -r dist/* "$INSTALL_DIR/nixclient/dist/" 2>/dev/null || true
|
||||
success "NixClient built"
|
||||
fi
|
||||
|
||||
mark_done "build_uis"
|
||||
fi
|
||||
|
||||
# ── 9. Build panel binary ─────────────────────────────────────────────────────
|
||||
step "Building nixpanel (Rust)"
|
||||
if ! step_done "build_panel"; then
|
||||
if [[ -f "$SRC_DIR/Cargo.toml" ]]; then
|
||||
cd "$SRC_DIR"
|
||||
$CARGO build --release --quiet
|
||||
# Copy any produced binaries
|
||||
[[ -f "target/release/nixpanel" ]] && \
|
||||
cp target/release/nixpanel "$BIN_DIR/nixpanel" && chmod 755 "$BIN_DIR/nixpanel"
|
||||
mark_done "build_panel"
|
||||
success "Panel binary built"
|
||||
else
|
||||
warn "Panel Rust source not yet present in repo — skipping build"
|
||||
warn "Panel Rust backend not yet present in repo — skipping build"
|
||||
warn "This is expected during early development. Re-run installer when source is ready."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NixClient — Your Hosting Account</title>
|
||||
</head>
|
||||
<body class="bg-gray-950">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "nixpanel-nixclient",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function App() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (email && password) {
|
||||
setIsLoggedIn(true)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1.5">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
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-emerald-500 transition-colors"
|
||||
placeholder="[email protected]"
|
||||
/>
|
||||
</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-emerald-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-emerald-600 hover:bg-emerald-700 text-white font-semibold text-sm transition-colors mt-6"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
NixPanel Client — Manage your hosting account
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: rgb(3, 7, 30);
|
||||
color: rgb(229, 231, 235);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
minify: 'terser',
|
||||
},
|
||||
server: {
|
||||
port: 3001,
|
||||
strictPort: true,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NixServer — NixPanel Admin</title>
|
||||
</head>
|
||||
<body class="bg-gray-950">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "nixpanel-nixserver",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function App() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
|
||||
const handleLogin = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (email && password) {
|
||||
setIsLoggedIn(true)
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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-blue-500">Server</span>
|
||||
</h1>
|
||||
<p className="text-gray-400">Admin 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">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1.5">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
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="[email protected]"
|
||||
/>
|
||||
</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
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-6">
|
||||
NixPanel Server Admin — Login with your credentials
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: rgb(3, 7, 30);
|
||||
color: rgb(229, 231, 235);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
minify: 'terser',
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
strictPort: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user