157 lines
5.1 KiB
JavaScript
157 lines
5.1 KiB
JavaScript
/**
|
|
* SPA Router & app init.
|
|
*/
|
|
const App = (() => {
|
|
let _currentUser = null;
|
|
|
|
const NAV = [
|
|
{ id: 'dashboard', label: 'Dashboard', icon: '◈', page: DashboardPage },
|
|
{ id: 'projects', label: 'Projects', icon: '▤', page: ProjectsPage },
|
|
{ id: 'live', label: 'Live Feed', icon: '⚡', page: LivePage },
|
|
{ id: 'keys', label: 'API Keys', icon: '⌘', page: KeysPage },
|
|
{ id: 'settings', label: 'Settings', icon: '⚙', page: SettingsPage },
|
|
];
|
|
|
|
async function init() {
|
|
if (!Api.isLoggedIn()) {
|
|
renderLogin();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
_currentUser = await Api.get('/api/auth/me');
|
|
} catch {
|
|
renderLogin();
|
|
return;
|
|
}
|
|
|
|
renderShell();
|
|
SSE.connect();
|
|
|
|
// Route from hash
|
|
const hash = location.hash.replace('#', '') || 'dashboard';
|
|
const [page, param] = hash.split('/');
|
|
navigate(page, param);
|
|
}
|
|
|
|
function renderLogin() {
|
|
document.getElementById('app').innerHTML = `
|
|
<div id="login-page">
|
|
<div class="login-box">
|
|
<div class="logo">CC<span class="accent">.</span>Dashboard</div>
|
|
<div id="login-error" class="error-msg"></div>
|
|
<form id="login-form" autocomplete="on" onsubmit="return false">
|
|
<div class="form-group">
|
|
<label>Email</label>
|
|
<input type="email" id="login-email" placeholder="you@example.com" autocomplete="email">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" id="login-password" placeholder="••••••••" autocomplete="current-password">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;margin-top:8px" id="btn-login">
|
|
Sign In
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const doLogin = async () => {
|
|
const email = document.getElementById('login-email').value.trim();
|
|
const password = document.getElementById('login-password').value;
|
|
const errEl = document.getElementById('login-error');
|
|
errEl.style.display = 'none';
|
|
try {
|
|
await Api.login(email, password);
|
|
init();
|
|
} catch (e) {
|
|
errEl.textContent = e.message;
|
|
errEl.style.display = 'block';
|
|
}
|
|
};
|
|
|
|
document.getElementById('btn-login').onclick = doLogin;
|
|
document.getElementById('login-form').addEventListener('submit', doLogin);
|
|
}
|
|
|
|
function renderShell() {
|
|
const isAdmin = _currentUser?.role === 'admin';
|
|
const navItems = [...NAV, ...(isAdmin ? [{ id: 'admin', label: 'Admin', icon: '⚀', page: AdminPage }] : [])];
|
|
|
|
document.getElementById('app').innerHTML = `
|
|
<div class="sidebar">
|
|
<div class="sidebar-logo">
|
|
<span>CC<span class="accent">.</span>Dashboard</span>
|
|
</div>
|
|
<nav class="sidebar-nav" id="sidebar-nav">
|
|
${navItems.map(n => `
|
|
<button class="nav-item" data-page="${n.id}" onclick="App.navigate('${n.id}')">
|
|
<span class="icon">${n.icon}</span>
|
|
${n.label}
|
|
</button>
|
|
`).join('')}
|
|
</nav>
|
|
<div class="sidebar-bottom">
|
|
<div class="sidebar-user">
|
|
<strong>${_currentUser?.username || ''}</strong>
|
|
${_currentUser?.email || ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="main">
|
|
<div class="topbar">
|
|
<h1 id="page-title">Dashboard</h1>
|
|
<span class="sse-dot" id="sse-dot"></span>
|
|
</div>
|
|
<div class="content" id="page-content"></div>
|
|
</div>
|
|
`;
|
|
|
|
SSE.setDot(document.getElementById('sse-dot'));
|
|
}
|
|
|
|
function navigate(pageId, param) {
|
|
const isAdmin = _currentUser?.role === 'admin';
|
|
const allPages = { dashboard: DashboardPage, projects: ProjectsPage, live: LivePage, keys: KeysPage, settings: SettingsPage };
|
|
if (isAdmin) allPages.admin = AdminPage;
|
|
// Special: project detail
|
|
if (pageId === 'project' && param) {
|
|
location.hash = `project/${param}`;
|
|
const content = document.getElementById('page-content');
|
|
if (!content) return;
|
|
document.getElementById('page-title').textContent = 'Project Detail';
|
|
_setActiveNav(null);
|
|
ProjectDetailPage.render(content, param);
|
|
return;
|
|
}
|
|
|
|
const page = allPages[pageId] || DashboardPage;
|
|
const navDef = [...NAV, { id: 'admin' }].find(n => n.id === pageId) || NAV[0];
|
|
|
|
location.hash = pageId;
|
|
const content = document.getElementById('page-content');
|
|
if (!content) return;
|
|
|
|
document.getElementById('page-title').textContent =
|
|
NAV.find(n => n.id === pageId)?.label || (pageId === 'admin' ? 'Admin' : 'Dashboard');
|
|
|
|
_setActiveNav(pageId);
|
|
page.render(content, param);
|
|
}
|
|
|
|
function _setActiveNav(pageId) {
|
|
document.querySelectorAll('.nav-item').forEach(el => {
|
|
el.classList.toggle('active', el.dataset.page === pageId);
|
|
});
|
|
}
|
|
|
|
return { init, navigate };
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', () => App.init());
|
|
window.addEventListener('hashchange', () => {
|
|
const hash = location.hash.replace('#', '') || 'dashboard';
|
|
const [page, param] = hash.split('/');
|
|
App.navigate(page, param);
|
|
});
|