feat: flip routing — customer portal at /, admin at /admin

Move all admin dashboard routes under /admin prefix and mount the
customer portal at root (/). This gives customers clean, shareable
URLs while staff bookmark /admin.

- Admin routes: /admin, /admin/clients, /admin/services, etc.
- Customer portal: / (root)
- Admin nav "Customer Portal" link points to / for staff preview
- Updated tests for new route structure and fixed React 19 act compat

Closes #56

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Groom Book CTO
2026-03-19 02:12:43 +00:00
parent 1136824fe3
commit b12d83c4f2
2 changed files with 44 additions and 30 deletions
+19 -13
View File
@@ -10,14 +10,14 @@ import { GroupBookingPage } from "./pages/GroupBooking.js";
import { CustomerPortal } from "./portal/CustomerPortal.js"; import { CustomerPortal } from "./portal/CustomerPortal.js";
const NAV_LINKS = [ const NAV_LINKS = [
{ to: "/", label: "Appointments" }, { to: "/admin", label: "Appointments" },
{ to: "/clients", label: "Clients" }, { to: "/admin/clients", label: "Clients" },
{ to: "/services", label: "Services" }, { to: "/admin/services", label: "Services" },
{ to: "/staff", label: "Staff" }, { to: "/admin/staff", label: "Staff" },
{ to: "/invoices", label: "Invoices" }, { to: "/admin/invoices", label: "Invoices" },
{ to: "/group-bookings", label: "Group Bookings" }, { to: "/admin/group-bookings", label: "Group Bookings" },
{ to: "/reports", label: "Reports" }, { to: "/admin/reports", label: "Reports" },
{ to: "/portal", label: "Customer Portal" }, { to: "/", label: "Customer Portal" },
]; ];
function AdminLayout() { function AdminLayout() {
@@ -36,7 +36,7 @@ function AdminLayout() {
> >
<strong style={{ marginRight: "1rem", fontSize: 16 }}>Groom Book</strong> <strong style={{ marginRight: "1rem", fontSize: 16 }}>Groom Book</strong>
<Link <Link
to="/book" to="/admin/book"
style={{ style={{
padding: "0.35rem 0.75rem", padding: "0.35rem 0.75rem",
borderRadius: 4, borderRadius: 4,
@@ -52,7 +52,9 @@ function AdminLayout() {
</Link> </Link>
{NAV_LINKS.map(({ to, label }) => { {NAV_LINKS.map(({ to, label }) => {
const active = const active =
to === "/" ? location.pathname === "/" : location.pathname.startsWith(to); to === "/admin"
? location.pathname === "/admin"
: location.pathname.startsWith(to);
return ( return (
<Link <Link
key={to} key={to}
@@ -91,9 +93,13 @@ function AdminLayout() {
export function App() { export function App() {
const location = useLocation(); const location = useLocation();
if (location.pathname.startsWith("/portal")) { if (location.pathname.startsWith("/admin")) {
return <CustomerPortal />; return (
<Routes>
<Route path="/admin/*" element={<AdminLayout />} />
</Routes>
);
} }
return <AdminLayout />; return <CustomerPortal />;
} }
+25 -17
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, within, act } from "@testing-library/react"; import { render, screen, within } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { App } from "../App.js"; import { App } from "../App.js";
@@ -11,30 +11,28 @@ beforeEach(() => {
} as unknown as Response); } as unknown as Response);
}); });
async function renderApp(route = "/") { function renderApp(route = "/admin") {
await act(async () => { render(
render( <MemoryRouter initialEntries={[route]}>
<MemoryRouter initialEntries={[route]}> <App />
<App /> </MemoryRouter>
</MemoryRouter> );
);
});
return screen.getByRole("navigation"); return screen.getByRole("navigation");
} }
describe("App navigation", () => { describe("App navigation", () => {
it("renders the Groom Book brand", async () => { it("renders the Groom Book brand", () => {
const nav = await renderApp(); const nav = renderApp();
expect(within(nav).getByText("Groom Book")).toBeInTheDocument(); expect(within(nav).getByText("Groom Book")).toBeInTheDocument();
}); });
it("renders the Book CTA button", async () => { it("renders the Book CTA button", () => {
const nav = await renderApp(); const nav = renderApp();
expect(within(nav).getByText("Book")).toBeInTheDocument(); expect(within(nav).getByText("Book")).toBeInTheDocument();
}); });
it("renders all primary nav links", async () => { it("renders all primary nav links", () => {
const nav = await renderApp(); const nav = renderApp();
const expectedLinks = [ const expectedLinks = [
"Appointments", "Appointments",
"Clients", "Clients",
@@ -49,10 +47,20 @@ describe("App navigation", () => {
}); });
}); });
it("highlights the active route link", async () => { it("highlights the active route link", () => {
const nav = await renderApp("/clients"); const nav = renderApp("/admin/clients");
const clientsLink = within(nav).getByText("Clients"); const clientsLink = within(nav).getByText("Clients");
// Active links use fontWeight 600 // Active links use fontWeight 600
expect(clientsLink).toHaveStyle({ fontWeight: "600" }); expect(clientsLink).toHaveStyle({ fontWeight: "600" });
}); });
it("renders customer portal at root", () => {
render(
<MemoryRouter initialEntries={["/"]}>
<App />
</MemoryRouter>
);
// Customer portal should render at root - no admin nav present
expect(screen.queryByText("Groom Book")).not.toBeInTheDocument();
});
}); });