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; + } }