feat: implement auth, projects, and frontend foundation

This commit is contained in:
2026-05-17 20:21:55 +00:00
parent e7819bfc82
commit 71d9fe6406
88 changed files with 10936 additions and 47 deletions
+56
View File
@@ -0,0 +1,56 @@
import { Link, NavLink, Outlet } from "react-router-dom";
import { useAuth } from "../state/auth";
const NAV_ITEMS = [
{ to: "/", label: "Dashboard" },
{ to: "/projects", label: "Projects" },
{ to: "/repositories", label: "Repositories" },
{ to: "/ssh-keys", label: "SSH Keys" },
{ to: "/settings", label: "Settings" }
];
export const AppShell = () => {
const { user, logout } = useAuth();
return (
<div className="shell">
<header className="shell-header">
<Link className="brand" to="/">
Headquarter
</Link>
<div className="header-actions">
<div className="user-chip">{user?.name ?? "User"}</div>
<button
className="ghost-button"
onClick={() => {
void logout();
}}
type="button"
>
Logout
</button>
</div>
</header>
<div className="shell-body">
<aside className="shell-nav" aria-label="Primary navigation">
{NAV_ITEMS.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
end={item.to === "/"}
>
{item.label}
</NavLink>
))}
</aside>
<main className="shell-content">
<Outlet />
</main>
</div>
</div>
);
};
@@ -0,0 +1,49 @@
import { render, screen } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { describe, expect, it, vi } from "vitest";
import { ProtectedRoute } from "./protected-route";
const mockUseAuth = vi.fn();
vi.mock("../state/auth", () => ({
useAuth: () => mockUseAuth()
}));
describe("ProtectedRoute", () => {
it("shows loading while session is resolving", () => {
mockUseAuth.mockReturnValue({ state: "loading" });
render(
<MemoryRouter initialEntries={["/"]}>
<ProtectedRoute>
<div>private content</div>
</ProtectedRoute>
</MemoryRouter>
);
expect(screen.getByText("Checking session...")).toBeInTheDocument();
});
it("redirects unauthenticated users to login", () => {
mockUseAuth.mockReturnValue({ state: "unauthenticated" });
render(
<MemoryRouter initialEntries={["/settings"]}>
<Routes>
<Route
path="/settings"
element={
<ProtectedRoute>
<div>private content</div>
</ProtectedRoute>
}
/>
<Route path="/login" element={<div>login page</div>} />
</Routes>
</MemoryRouter>
);
expect(screen.getByText("login page")).toBeInTheDocument();
});
});
@@ -0,0 +1,19 @@
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../state/auth";
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { state } = useAuth();
const location = useLocation();
if (state === "loading") {
return <div className="center-screen">Checking session...</div>;
}
if (state === "unauthenticated") {
const nextPath = encodeURIComponent(location.pathname);
return <Navigate to={`/login?next=${nextPath}`} replace />;
}
return <>{children}</>;
};