From ffc33ed95e2b068004c685dbd444a40fabae49d9 Mon Sep 17 00:00:00 2001 From: Bobban Rydh Date: Sun, 6 Sep 2026 02:38:04 +0200 Subject: [PATCH] reworked navigation --- frontend/src/App.css | 120 +++++++++++++++++- frontend/src/App.js | 296 ++++++++++++++++++++++++++----------------- 2 files changed, 295 insertions(+), 121 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index d2350a4..64daacc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -44,12 +44,123 @@ body { .app-header { background: var(--surface); border-bottom: 1px solid var(--border); - padding: 14px 24px; + padding: 0 20px; + display: flex; + align-items: center; + gap: 16px; + height: 52px; + flex-shrink: 0; +} + +.app-header h1 { + font-size: 16px; + font-weight: 700; + white-space: nowrap; + flex-shrink: 0; +} + +/* Primary nav tabs in the top bar */ +.nav-tabs { + display: flex; + align-items: stretch; + gap: 2px; + flex: 1; + height: 100%; +} + +.nav-tab { + background: none; + border: none; + border-bottom: 3px solid transparent; + padding: 0 14px; + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + white-space: nowrap; + transition: color 0.15s, border-color 0.15s; + height: 100%; display: flex; align-items: center; } -.app-header h1 { font-size: 18px; font-weight: 600; } +.nav-tab:hover { color: var(--text); } + +.nav-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +/* User menu dropdown */ +.user-menu-wrapper { + position: relative; +} + +.user-menu-trigger { + display: flex; + align-items: center; + gap: 7px; + background: none; + border: 1px solid var(--border); + border-radius: 20px; + padding: 4px 10px 4px 5px; + cursor: pointer; + color: var(--text); + font-size: 13px; + transition: background 0.15s; +} +.user-menu-trigger:hover { background: var(--hover); } + +.user-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: var(--accent); + color: #fff; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.user-menu-caret { font-size: 9px; color: var(--text-muted); } + +.user-menu-dropdown { + position: absolute; + top: calc(100% + 6px); + right: 0; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0,0,0,0.12); + min-width: 160px; + z-index: 200; + overflow: hidden; + padding: 4px 0; +} + +.user-menu-item { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + padding: 8px 14px; + font-size: 13px; + color: var(--text); + cursor: pointer; +} +.user-menu-item:hover { background: var(--hover); } +.user-menu-item--danger { color: #e05252; } +.user-menu-item--danger:hover { background: rgba(224, 82, 82, 0.08); } + +.user-menu-divider { + height: 1px; + background: var(--border); + margin: 4px 0; +} .layout { display: flex; @@ -57,6 +168,11 @@ body { overflow: hidden; } +/* When no sidebar is present (Dashboard, Secrets, IPAM) */ +.layout.no-sidebar .main { + max-width: 1200px; +} + /* ===== Sidebar ===== */ .sidebar { width: 220px; diff --git a/frontend/src/App.js b/frontend/src/App.js index ff04ce2..2466fdc 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { getProviders, getZones, getRecords, syncRecords, addRecord, updateRecord, deleteRecord, getMe, setToken } from './api/dns'; import RecordsTable from './components/RecordsTable'; import AddRecordForm from './components/AddRecordForm'; @@ -29,6 +29,12 @@ function formatSyncedAt(iso) { // ─── Main app (shown when authenticated) ───────────────────────────────────── function AppShell({ currentUser, onLogout }) { + // ── Top-level section nav ──────────────────────────────────────────────── + const [section, setSection] = useState('dashboard'); // dashboard|dns|secrets|ipam|admin + const [adminView, setAdminView] = useState('settings'); // settings|audit|diag|profile + const [dnsView, setDnsView] = useState('domains'); // domains | (provider is selected) + + // ── DNS state ──────────────────────────────────────────────────────────── const [providers, setProviders] = useState([]); const [selectedProvider, setSelectedProvider] = useState(null); const [zones, setZones] = useState([]); @@ -38,16 +44,40 @@ function AppShell({ currentUser, onLogout }) { const [loadingZones, setLoadingZones] = useState(false); const [loadingRecords, setLoadingRecords] = useState(false); const [syncing, setSyncing] = useState(false); - const [view, setView] = useState('dashboard'); const [showForm, setShowForm] = useState(false); const [editingRecord, setEditingRecord] = useState(null); - const { colors: providerColors } = useProviderColors(); - const { theme, toggleTheme } = useTheme(); const [deletingId, setDeletingId] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); - const [error, setError] = useState(''); const [filter, setFilter] = useState(''); + const { colors: providerColors } = useProviderColors(); + const { theme, toggleTheme } = useTheme(); + const [error, setError] = useState(''); + const [userMenuOpen, setUserMenuOpen] = useState(false); + const userMenuRef = useRef(null); + + // Close user menu on outside click + useEffect(() => { + function handler(e) { + if (userMenuRef.current && !userMenuRef.current.contains(e.target)) { + setUserMenuOpen(false); + } + } + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + function switchSection(s) { + setSection(s); + setSelectedProvider(null); + setSelectedZone(null); + setDnsView('domains'); + setFilter(''); + setError(''); + } + + const hasSidebar = section === 'dns' || section === 'admin'; + useEffect(() => { getProviders() .then(setProviders) @@ -83,6 +113,7 @@ function AppShell({ currentUser, onLogout }) { function selectZone(zone) { setSelectedZone(zone); setFilter(''); + setDnsView('provider'); loadRecords(selectedProvider, zone); } @@ -140,123 +171,146 @@ function AppShell({ currentUser, onLogout }) { return (
+ {/* ── Top bar ─────────────────────────────────────────────────────── */}

🦥 Sloth Manager

+ + +
- {currentUser.username} - +
+ + {userMenuOpen && ( +
+ +
+ +
+ )} +
-
- + )} + {/* Sidebar — Admin */} + {section === 'admin' && ( + + )} + + {/* ── Main content ───────────────────────────────────────────────── */}
{error && (
@@ -265,25 +319,25 @@ function AppShell({ currentUser, onLogout }) {
)} - {!selectedProvider && view === 'settings' && } - {!selectedProvider && view === 'profile' && } - {!selectedProvider && view === 'domains' && ( + {/* Dashboard */} + {section === 'dashboard' && } + + {/* Secrets */} + {section === 'secrets' && } + + {/* IPAM */} + {section === 'ipam' && } + + {/* DNS */} + {section === 'dns' && !selectedProvider && dnsView === 'domains' && ( { setSelectedProvider(provider); - setView('dashboard'); - // wait for zones to load then select + setDnsView('provider'); setSelectedZone({ id: zoneId, name: zoneName }); loadRecords(provider, { id: zoneId, name: zoneName }); }} /> )} - {!selectedProvider && view === 'secrets' && } - {!selectedProvider && view === 'ipam' && } - {!selectedProvider && view === 'audit' && } - {!selectedProvider && view === 'diag' && } - {!selectedProvider && view === 'dashboard' && } - {!selectedProvider && view === 'privacy' && } - - {selectedProvider && !selectedZone && ( + {section === 'dns' && selectedProvider && !selectedZone && ( p.id === selectedProvider)} @@ -291,8 +345,7 @@ function AppShell({ currentUser, onLogout }) { onSelectZone={selectZone} /> )} - - {selectedZone && ( + {section === 'dns' && selectedZone && ( <>
@@ -317,7 +370,6 @@ function AppShell({ currentUser, onLogout }) {
- {loadingRecords ?

Loading…

: !syncedAt @@ -326,6 +378,12 @@ function AppShell({ currentUser, onLogout }) { } )} + + {/* Admin */} + {section === 'admin' && adminView === 'settings' && } + {section === 'admin' && adminView === 'audit' && } + {section === 'admin' && adminView === 'diag' && } + {section === 'admin' && adminView === 'profile' && }