Files
lunarfront-manager/frontend/index.html
Ryan Moon ceacd9b459
Some checks failed
Build & Release / build (push) Failing after 5s
feat: redesign UI with sidebar, centered login, account page with password reset
2026-04-03 15:13:46 -05:00

339 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LunarFront Manager</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f0f0f; color: #e0e0e0; min-height: 100vh; }
/* ── Login ── */
#login-view {
display: none;
min-height: 100vh;
align-items: center;
justify-content: center;
}
.login-box {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 10px;
padding: 2.5rem;
width: 100%;
max-width: 360px;
}
.login-box h1 { font-size: 1.2rem; color: #fff; margin-bottom: 0.4rem; }
.login-box p { font-size: 0.85rem; color: #666; margin-bottom: 1.8rem; }
/* ── App shell ── */
#app-view { display: none; height: 100vh; }
.shell { display: flex; height: 100%; }
/* ── Sidebar ── */
.sidebar {
width: 220px;
background: #141414;
border-right: 1px solid #222;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-brand {
padding: 1.4rem 1.2rem 1rem;
font-size: 0.9rem;
font-weight: 600;
color: #fff;
letter-spacing: 0.03em;
border-bottom: 1px solid #222;
}
.sidebar-nav { flex: 1; padding: 0.8rem 0.5rem; }
.nav-item {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.6rem 0.8rem;
border-radius: 6px;
font-size: 0.875rem;
color: #aaa;
cursor: pointer;
transition: background 0.15s, color 0.15s;
user-select: none;
}
.nav-item:hover { background: #1f1f1f; color: #fff; }
.nav-item.active { background: #1f1f1f; color: #fff; }
.nav-icon { font-size: 1rem; width: 1.2rem; text-align: center; }
.sidebar-user {
padding: 0.8rem;
border-top: 1px solid #222;
}
.user-btn {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.6rem 0.8rem;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
width: 100%;
background: none;
border: none;
color: #aaa;
text-align: left;
}
.user-btn:hover { background: #1f1f1f; color: #fff; }
.user-menu {
display: none;
flex-direction: column;
gap: 2px;
padding: 0 0.5rem 0.4rem;
}
.user-menu.open { display: flex; }
.user-menu-item {
font-size: 0.8rem;
padding: 0.45rem 0.8rem;
border-radius: 6px;
cursor: pointer;
color: #888;
transition: background 0.15s, color 0.15s;
}
.user-menu-item:hover { background: #1f1f1f; color: #fff; }
.user-avatar {
width: 28px; height: 28px;
border-radius: 50%;
background: #2563eb;
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 600; color: #fff;
flex-shrink: 0;
}
.user-name { font-size: 0.85rem; }
/* ── Main content ── */
.main { flex: 1; overflow-y: auto; padding: 2rem; }
.page { display: none; }
.page.active { display: block; }
.page-title { font-size: 1.1rem; font-weight: 600; color: #fff; margin-bottom: 1.5rem; }
/* ── Cards / forms ── */
.card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 1.5rem;
max-width: 480px;
}
label { display: block; font-size: 0.8rem; color: #888; margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.04em; }
input[type=text], input[type=password] {
width: 100%; padding: 0.6rem 0.8rem;
background: #111; border: 1px solid #333; border-radius: 6px;
color: #fff; font-size: 0.9rem; margin-bottom: 1.2rem;
}
input:focus { outline: none; border-color: #555; }
.btn {
padding: 0.6rem 1.2rem;
background: #2563eb; border: none; border-radius: 6px;
color: #fff; font-size: 0.9rem; cursor: pointer;
}
.btn:hover { background: #1d4ed8; }
.btn:disabled { background: #333; cursor: not-allowed; }
.btn-ghost {
background: none; border: 1px solid #333; color: #aaa;
}
.btn-ghost:hover { background: #1a1a1a; color: #fff; }
.status { margin-top: 1rem; font-size: 0.85rem; padding: 0.6rem 0.8rem; border-radius: 6px; display: none; }
.status.success { background: #14532d; color: #86efac; display: block; }
.status.error { background: #450a0a; color: #fca5a5; display: block; }
</style>
</head>
<body>
<!-- Login -->
<div id="login-view">
<div class="login-box">
<h1>LunarFront Manager</h1>
<p>Sign in to continue</p>
<label for="login-username">Username</label>
<input id="login-username" type="text" autocomplete="username" />
<label for="login-password">Password</label>
<input id="login-password" type="password" autocomplete="current-password" onkeydown="if(event.key==='Enter')login()" />
<button class="btn" style="width:100%" onclick="login()">Sign In</button>
<div id="login-status" class="status"></div>
</div>
</div>
<!-- App -->
<div id="app-view">
<div class="shell">
<aside class="sidebar">
<div class="sidebar-brand">LunarFront</div>
<nav class="sidebar-nav">
<div class="nav-item active" onclick="showPage('create-env')">
<span class="nav-icon"></span>
<span>Create Env</span>
</div>
</nav>
<div class="sidebar-user">
<button class="user-btn" onclick="toggleUserMenu()">
<div class="user-avatar" id="user-initials">?</div>
<span class="user-name" id="user-name-label">...</span>
</button>
<div class="user-menu" id="user-menu">
<div class="user-menu-item" onclick="showPage('account');toggleUserMenu()">Account</div>
<div class="user-menu-item" onclick="logout()">Sign Out</div>
</div>
</div>
</aside>
<main class="main">
<!-- Create Env -->
<div id="page-create-env" class="page active">
<div class="page-title">Create Environment</div>
<div class="card">
<label for="slug">Customer Slug</label>
<input id="slug" type="text" placeholder="acme-shop" pattern="[a-z0-9-]+" />
<button class="btn" id="provision-btn" onclick="provision()">Provision</button>
<div id="provision-status" class="status"></div>
</div>
</div>
<!-- Account -->
<div id="page-account" class="page">
<div class="page-title">Account</div>
<div class="card">
<label for="new-password">New Password</label>
<input id="new-password" type="password" autocomplete="new-password" />
<label for="confirm-password">Confirm Password</label>
<input id="confirm-password" type="password" autocomplete="new-password" />
<div style="display:flex;gap:0.8rem;align-items:center">
<button class="btn" onclick="changePassword()">Update Password</button>
</div>
<div id="account-status" class="status"></div>
</div>
</div>
</main>
</div>
</div>
<script>
let currentUser = null;
async function checkAuth() {
const res = await fetch('/api/auth/me');
if (res.ok) {
const data = await res.json();
currentUser = data.username;
document.getElementById('user-name-label').textContent = data.username;
document.getElementById('user-initials').textContent = data.username[0].toUpperCase();
document.getElementById('app-view').style.display = 'block';
} else {
document.getElementById('login-view').style.display = 'flex';
}
}
async function login() {
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value;
const status = document.getElementById('login-status');
status.className = 'status';
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? 'Login failed');
currentUser = data.username;
document.getElementById('user-name-label').textContent = data.username;
document.getElementById('user-initials').textContent = data.username[0].toUpperCase();
document.getElementById('login-view').style.display = 'none';
document.getElementById('app-view').style.display = 'block';
} catch (err) {
status.textContent = err.message;
status.className = 'status error';
}
}
async function logout() {
await fetch('/api/auth/logout', { method: 'POST' });
document.getElementById('app-view').style.display = 'none';
document.getElementById('login-view').style.display = 'flex';
document.getElementById('login-password').value = '';
}
function toggleUserMenu() {
document.getElementById('user-menu').classList.toggle('open');
}
function showPage(name) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('page-' + name).classList.add('active');
if (name === 'create-env') document.querySelector('.nav-item').classList.add('active');
}
async function provision() {
const slug = document.getElementById('slug').value.trim();
const btn = document.getElementById('provision-btn');
const status = document.getElementById('provision-status');
if (!slug) return;
btn.disabled = true;
btn.textContent = 'Provisioning...';
status.className = 'status';
try {
const res = await fetch('/api/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: slug }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? 'Unknown error');
status.textContent = `${data.slug} provisioned successfully`;
status.className = 'status success';
document.getElementById('slug').value = '';
} catch (err) {
status.textContent = `${err.message}`;
status.className = 'status error';
} finally {
btn.disabled = false;
btn.textContent = 'Provision';
}
}
async function changePassword() {
const pw = document.getElementById('new-password').value;
const confirm = document.getElementById('confirm-password').value;
const status = document.getElementById('account-status');
status.className = 'status';
if (pw !== confirm) {
status.textContent = 'Passwords do not match';
status.className = 'status error';
return;
}
try {
const res = await fetch('/api/auth/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.message ?? 'Failed to update password');
status.textContent = 'Password updated';
status.className = 'status success';
document.getElementById('new-password').value = '';
document.getElementById('confirm-password').value = '';
} catch (err) {
status.textContent = err.message;
status.className = 'status error';
}
}
checkAuth();
</script>
</body>
</html>