From fd36c256369a2aa7980d7d29e6daf461d4f13939 Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Sat, 11 Apr 2026 09:43:24 -0600 Subject: [PATCH] feat: replace settings modal with full-page sidebar layout Convert the users/admin modal into a dedicated settings page with left sidebar navigation and spacious content panels. Hash-based routing (#settings/users, #settings/smtp, etc.) enables browser back-button support and direct URL access. Admin-only tabs are hidden for non-admin users. --- public/css/style.css | 124 ++++++++++++++++++- public/index.html | 285 +++++++++++++++++++++++++------------------ public/js/app.js | 114 +++++++++++++---- 3 files changed, 373 insertions(+), 150 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index c7c9f0e..5257a3a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -22,11 +22,17 @@ body { background: var(--bg); color: var(--text); height: 100vh; - display: flex; - flex-direction: column; overflow: hidden; } +#main-app { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} +#main-app[hidden] { display: none; } + /* ── Header ── */ header { background: var(--header-bg); @@ -879,3 +885,117 @@ input[type="file"] { @media (max-width: 768px), (orientation: portrait) { #btn-layout-editor { display: none !important; } } + +/* ── Settings Page ── */ +.settings-page { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} +.settings-page[hidden] { display: none; } + +.settings-header { + background: var(--header-bg); + color: var(--header-fg); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1.5rem; + height: 44px; + flex-shrink: 0; +} +.settings-header-left { display: flex; align-items: center; gap: 12px; } +.settings-header-right { display: flex; align-items: center; gap: 10px; } +.settings-back-link { + color: rgba(255,255,255,0.7); + text-decoration: none; + font-size: 13px; + font-weight: 500; + transition: color 0.15s; +} +.settings-back-link:hover { color: #fff; } + +.settings-layout { + display: flex; + flex: 1; + overflow: hidden; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +.settings-sidebar { + width: 220px; + flex-shrink: 0; + padding: 20px 0; + border-right: 1px solid var(--border); + overflow-y: auto; + background: var(--surface); +} +.settings-sidebar-title { + font-size: 15px; + font-weight: 700; + padding: 4px 20px 16px; + color: var(--text); +} +.settings-nav-group { margin-bottom: 8px; } +.settings-nav-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + padding: 12px 20px 6px; +} +.settings-nav-item { + display: block; + padding: 7px 20px; + font-size: 13px; + color: var(--text); + text-decoration: none; + border-radius: 6px; + margin: 1px 8px; + transition: background 0.12s; +} +.settings-nav-item:hover { background: var(--bg); } +.settings-nav-item.active { + background: var(--primary); + color: #fff; + font-weight: 500; +} + +.settings-content { + flex: 1; + padding: 32px 48px; + overflow-y: auto; + max-width: 780px; +} + +.settings-panel h2 { + font-size: 20px; + font-weight: 600; + margin-bottom: 4px; + color: var(--text); +} +.settings-panel h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text); +} +.settings-desc { + font-size: 13px; + color: var(--text-muted); + margin-bottom: 20px; + line-height: 1.5; +} +.settings-section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 24px; + margin-bottom: 20px; +} +.settings-section .form-row { gap: 16px; } +.settings-section .form-group { margin-bottom: 4px; } diff --git a/public/index.html b/public/index.html index 62212b4..18b2ce8 100644 --- a/public/index.html +++ b/public/index.html @@ -87,6 +87,169 @@ + + + +
ezcheck @@ -657,128 +820,8 @@
+ - - - diff --git a/public/js/app.js b/public/js/app.js index 509ccc4..6742d78 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -65,7 +65,8 @@ async function checkAuth() { return false; } if (location.hash === '#oidc-linked') { - history.replaceState(null, '', location.pathname); + // After OIDC link callback, navigate to SSO settings panel + location.hash = '#settings/sso'; // Fall through to normal auth check — user is still logged in } @@ -75,6 +76,8 @@ async function checkAuth() { state.user = await res.json(); hideLoginOverlay(); applyRoleUI(); + // Route to settings page if hash says so + if (location.hash.startsWith('#settings')) handleHashRoute(); return true; } // No session — check if this is first-run (no users at all) @@ -165,6 +168,9 @@ async function logout() { document.getElementById('login-error').hidden = true; document.getElementById('login-setup-section').hidden = true; document.getElementById('login-form-section').hidden = false; + // Ensure settings page is hidden and main app restored + document.getElementById('settings-page').hidden = true; + document.getElementById('main-app').hidden = false; showLoginOverlay(); } @@ -176,8 +182,9 @@ function applyRoleUI() { const isEditor = state.accountRole === 'editor' || (!state.accountRole && (role === 'admin' || role === 'editor')); document.getElementById('header-username').textContent = state.user ? state.user.username : ''; + document.getElementById('settings-username').textContent = state.user ? state.user.username : ''; - // Admin-only elements + // Admin-only elements (main app + settings sidebar) document.querySelectorAll('[data-admin-only]').forEach(el => { el.hidden = !isAdmin; }); // Editor+ elements (hide for viewers) @@ -191,28 +198,62 @@ function applyRoleUI() { let usersState = { users: [], editingId: null }; -function openUsersModal() { +// ── Settings page navigation ──────────────────────────────────────────────── + +function navigateToSettings(tab) { const isAdmin = state.user && state.user.role === 'admin'; - document.getElementById('user-form-error').hidden = true; - document.getElementById('users-title').textContent = isAdmin ? 'Manage Users' : 'My Account'; - document.getElementById('users-overlay').classList.add('open'); - document.getElementById('users-modal').classList.add('open'); - // Admin-only sections - document.getElementById('users-list').hidden = !isAdmin; - document.getElementById('user-form-section').hidden = !isAdmin; - document.getElementById('smtp-settings-section').hidden = !isAdmin; - if (isAdmin) { - loadUsers(); - renderUfAccountCheckboxes(); - loadSmtpSettings(); + const defaultTab = isAdmin ? 'users' : 'password'; + const resolved = tab || defaultTab; + + // Guard non-admin from admin tabs + if (!isAdmin && (resolved === 'users' || resolved === 'smtp')) { + location.hash = '#settings/password'; + return; } + + document.getElementById('main-app').hidden = true; + const sp = document.getElementById('settings-page'); + sp.hidden = false; + document.getElementById('settings-username').textContent = state.user ? state.user.username : ''; + + // Check OIDC status to show/hide SSO tab loadOidcLinkStatus(); + + activateSettingsTab(resolved); } -function closeUsersModal() { - document.getElementById('users-overlay').classList.remove('open'); - document.getElementById('users-modal').classList.remove('open'); - cancelUserEdit(); +function activateSettingsTab(tab) { + // Hide all panels, show the target + document.querySelectorAll('.settings-panel').forEach(p => { p.hidden = true; }); + const panel = document.getElementById('settings-panel-' + tab); + if (panel) panel.hidden = false; + + // Update sidebar active state + document.querySelectorAll('.settings-nav-item').forEach(a => { + a.classList.toggle('active', a.dataset.settingsTab === tab); + }); + + // Load data for the activated tab + if (tab === 'users') { loadUsers(); renderUfAccountCheckboxes(); } + if (tab === 'smtp') { loadSmtpSettings(); } + if (tab === 'sso') { loadOidcLinkStatus(); } +} + +function showMainApp() { + document.getElementById('settings-page').hidden = true; + document.getElementById('main-app').hidden = false; +} + +function handleHashRoute() { + const hash = location.hash; + if (hash.startsWith('#settings/')) { + const tab = hash.split('/')[1]; + navigateToSettings(tab); + } else if (hash.startsWith('#settings')) { + navigateToSettings(); + } else { + showMainApp(); + } } async function loadUsers() { @@ -1664,9 +1705,12 @@ async function saveSmtpSettings() { async function loadOidcLinkStatus() { try { const cfg = await fetch('/api/auth/oidc/config').then(r => r.json()); - const section = document.getElementById('oidc-link-section'); - if (!cfg.enabled) { section.hidden = true; return; } - section.hidden = false; + const ssoNavItem = document.querySelector('[data-settings-tab="sso"]'); + if (!cfg.enabled) { + if (ssoNavItem) ssoNavItem.hidden = true; + return; + } + if (ssoNavItem) ssoNavItem.hidden = false; const me = await apiFetch('GET', '/api/auth/me'); const statusEl = document.getElementById('oidc-link-status'); @@ -1886,11 +1930,27 @@ async function init() { document.getElementById('btn-reset-submit').addEventListener('click', submitResetPassword); document.getElementById('reset-password2').addEventListener('keydown', e => { if (e.key === 'Enter') submitResetPassword(); }); - // User management - document.getElementById('btn-users').addEventListener('click', openUsersModal); - document.getElementById('header-username').addEventListener('click', openUsersModal); - document.getElementById('btn-close-users').addEventListener('click', closeUsersModal); - document.getElementById('users-overlay').addEventListener('click', closeUsersModal); + // User management / settings page + document.getElementById('btn-users').addEventListener('click', () => { location.hash = '#settings/users'; }); + document.getElementById('header-username').addEventListener('click', () => { + const isAdmin = state.user && state.user.role === 'admin'; + location.hash = isAdmin ? '#settings/users' : '#settings/password'; + }); + document.getElementById('settings-back-link').addEventListener('click', e => { + e.preventDefault(); + location.hash = ''; + }); + document.getElementById('btn-settings-logout').addEventListener('click', () => { + location.hash = ''; + logout(); + }); + // Sidebar tab navigation + document.querySelectorAll('.settings-nav-item').forEach(a => { + a.addEventListener('click', e => { e.preventDefault(); location.hash = a.getAttribute('href'); }); + }); + window.addEventListener('hashchange', () => { + if (state.user) handleHashRoute(); + }); document.getElementById('users-list').addEventListener('click', e => { const editBtn = e.target.closest('.user-btn-edit'); const deleteBtn = e.target.closest('.user-btn-delete');