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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user