diff --git a/.env.example b/.env.example
index 1ffd1a5..f85269e 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 51d539c..bce6bd1 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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 (
{children}
diff --git a/frontend/src/api.js b/frontend/src/api.js
index a97e22f..5ee9bfb 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -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 = {
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index e93b982..10061c6 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -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;
+ }
}