Revamp navigation and styling; enhance API URL handling.

Replaced `Link` components with `NavLink` for active state support and added new sidebar navigation styling. Enhanced API URL handling to prevent mixed content when using HTTPS. Updated layout and CSS for better responsiveness and consistent design.
This commit is contained in:
2026-02-12 10:07:22 +01:00
parent f12dd46c21
commit 7997773129
4 changed files with 116 additions and 11 deletions

View File

@@ -25,4 +25,5 @@ INIT_ADMIN_PASSWORD=ChangeMe123!
# Frontend
FRONTEND_PORT=5173
VITE_API_URL=http://localhost:8000/api/v1
# For reverse proxy + SSL prefer relative path to avoid mixed-content.
VITE_API_URL=/api/v1

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Link, Navigate, Route, Routes, useLocation } from "react-router-dom";
import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom";
import { useAuth } from "./state";
import { LoginPage } from "./pages/LoginPage";
import { DashboardPage } from "./pages/DashboardPage";
@@ -17,20 +17,36 @@ function Protected({ children }) {
function Layout({ children }) {
const { me, logout } = useAuth();
const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`;
return (
<div className="shell">
<aside className="sidebar">
<h1>NexaPG</h1>
<nav>
<Link to="/">Dashboard</Link>
<Link to="/targets">Targets</Link>
<Link to="/query-insights">Query Insights</Link>
{me?.role === "admin" && <Link to="/admin/users">Admin</Link>}
<nav className="sidebar-nav">
<NavLink to="/" end className={navClass}>
<span className="nav-icon">DB</span>
<span className="nav-label">Dashboard</span>
</NavLink>
<NavLink to="/targets" className={navClass}>
<span className="nav-icon">TG</span>
<span className="nav-label">Targets</span>
</NavLink>
<NavLink to="/query-insights" className={navClass}>
<span className="nav-icon">QI</span>
<span className="nav-label">Query Insights</span>
</NavLink>
{me?.role === "admin" && (
<NavLink to="/admin/users" className={navClass}>
<span className="nav-icon">AD</span>
<span className="nav-label">Admin</span>
</NavLink>
)}
</nav>
<div className="profile">
<div>{me?.email}</div>
<div className="role">{me?.role}</div>
<button onClick={logout}>Logout</button>
<button className="logout-btn" onClick={logout}>Logout</button>
</div>
</aside>
<main className="main">{children}</main>

View File

@@ -1,4 +1,21 @@
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000/api/v1";
function resolveApiUrl() {
const raw = (import.meta.env.VITE_API_URL || "").trim();
const fallback = "/api/v1";
if (!raw) return fallback;
try {
const parsed = new URL(raw, window.location.origin);
if (window.location.protocol === "https:" && parsed.protocol === "http:") {
// Avoid mixed-content when UI is served over HTTPS.
parsed.protocol = "https:";
}
return parsed.toString().replace(/\/$/, "");
} catch {
return fallback;
}
}
const API_URL = resolveApiUrl();
export async function apiFetch(path, options = {}, tokens, onUnauthorized) {
const headers = {

View File

@@ -17,6 +17,7 @@ body {
font-family: "Space Grotesk", "Segoe UI", sans-serif;
color: var(--text);
background: radial-gradient(circle at top right, #1d335f, #0b1020 55%);
overflow: hidden;
}
a {
@@ -27,7 +28,8 @@ a {
.shell {
display: grid;
grid-template-columns: 260px 1fr;
min-height: 100vh;
height: 100vh;
overflow: hidden;
}
.sidebar {
@@ -37,14 +39,60 @@ a {
display: flex;
flex-direction: column;
gap: 20px;
height: 100vh;
position: sticky;
top: 0;
overflow: hidden;
}
.sidebar nav {
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 10px;
}
.nav-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid #223056;
border-radius: 10px;
background: linear-gradient(180deg, #101b3a, #0d1530);
color: #c6d5ef;
transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}
.nav-btn:hover {
border-color: #2e4f98;
background: linear-gradient(180deg, #13224b, #101b3a);
transform: translateY(-1px);
}
.nav-btn.active {
border-color: #38bdf8;
box-shadow: inset 0 0 0 1px #38bdf860;
background: linear-gradient(180deg, #16305f, #101f43);
color: #ecf5ff;
}
.nav-icon {
width: 26px;
height: 26px;
border-radius: 8px;
display: grid;
place-items: center;
font-size: 11px;
font-weight: 700;
border: 1px solid #2b3f74;
background: #0d1631;
}
.nav-label {
font-size: 15px;
font-weight: 600;
}
.profile {
margin-top: auto;
border-top: 1px solid #223056;
@@ -58,6 +106,8 @@ a {
.main {
padding: 24px;
height: 100vh;
overflow-y: auto;
}
.card {
@@ -120,6 +170,17 @@ button {
cursor: pointer;
}
.logout-btn {
width: 100%;
margin-top: 8px;
border-color: #374f8f;
background: linear-gradient(180deg, #13224b, #101b3a);
}
.logout-btn:hover {
border-color: #38bdf8;
}
table {
width: 100%;
border-collapse: collapse;
@@ -215,16 +276,26 @@ td {
}
@media (max-width: 980px) {
body {
overflow: auto;
}
.shell {
grid-template-columns: 1fr;
height: auto;
overflow: visible;
}
.sidebar {
position: sticky;
top: 0;
z-index: 2;
height: auto;
}
.grid.two,
.grid.three {
grid-template-columns: 1fr;
}
.main {
height: auto;
overflow: visible;
}
}