From b12d83c4f279f1f00ea0fc3692973e1db72158f1 Mon Sep 17 00:00:00 2001 From: Groom Book CTO Date: Thu, 19 Mar 2026 02:12:43 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20flip=20routing=20=E2=80=94=20custom?= =?UTF-8?q?er=20portal=20at=20/,=20admin=20at=20/admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/web/src/App.tsx | 32 +++++++++++++--------- apps/web/src/__tests__/App.test.tsx | 42 +++++++++++++++++------------ 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index f61a751..b3ef7fc 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -10,14 +10,14 @@ import { GroupBookingPage } from "./pages/GroupBooking.js"; import { CustomerPortal } from "./portal/CustomerPortal.js"; const NAV_LINKS = [ - { to: "/", label: "Appointments" }, - { to: "/clients", label: "Clients" }, - { to: "/services", label: "Services" }, - { to: "/staff", label: "Staff" }, - { to: "/invoices", label: "Invoices" }, - { to: "/group-bookings", label: "Group Bookings" }, - { to: "/reports", label: "Reports" }, - { to: "/portal", label: "Customer Portal" }, + { to: "/admin", label: "Appointments" }, + { to: "/admin/clients", label: "Clients" }, + { to: "/admin/services", label: "Services" }, + { to: "/admin/staff", label: "Staff" }, + { to: "/admin/invoices", label: "Invoices" }, + { to: "/admin/group-bookings", label: "Group Bookings" }, + { to: "/admin/reports", label: "Reports" }, + { to: "/", label: "Customer Portal" }, ]; function AdminLayout() { @@ -36,7 +36,7 @@ function AdminLayout() { > Groom Book {NAV_LINKS.map(({ to, label }) => { const active = - to === "/" ? location.pathname === "/" : location.pathname.startsWith(to); + to === "/admin" + ? location.pathname === "/admin" + : location.pathname.startsWith(to); return ( ; + if (location.pathname.startsWith("/admin")) { + return ( + + } /> + + ); } - return ; + return ; } diff --git a/apps/web/src/__tests__/App.test.tsx b/apps/web/src/__tests__/App.test.tsx index 4854db3..cfd422d 100644 --- a/apps/web/src/__tests__/App.test.tsx +++ b/apps/web/src/__tests__/App.test.tsx @@ -1,5 +1,5 @@ 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 { App } from "../App.js"; @@ -11,30 +11,28 @@ beforeEach(() => { } as unknown as Response); }); -async function renderApp(route = "/") { - await act(async () => { - render( - - - - ); - }); +function renderApp(route = "/admin") { + render( + + + + ); return screen.getByRole("navigation"); } describe("App navigation", () => { - it("renders the Groom Book brand", async () => { - const nav = await renderApp(); + it("renders the Groom Book brand", () => { + const nav = renderApp(); expect(within(nav).getByText("Groom Book")).toBeInTheDocument(); }); - it("renders the Book CTA button", async () => { - const nav = await renderApp(); + it("renders the Book CTA button", () => { + const nav = renderApp(); expect(within(nav).getByText("Book")).toBeInTheDocument(); }); - it("renders all primary nav links", async () => { - const nav = await renderApp(); + it("renders all primary nav links", () => { + const nav = renderApp(); const expectedLinks = [ "Appointments", "Clients", @@ -49,10 +47,20 @@ describe("App navigation", () => { }); }); - it("highlights the active route link", async () => { - const nav = await renderApp("/clients"); + it("highlights the active route link", () => { + const nav = renderApp("/admin/clients"); const clientsLink = within(nav).getByText("Clients"); // Active links use fontWeight 600 expect(clientsLink).toHaveStyle({ fontWeight: "600" }); }); + + it("renders customer portal at root", () => { + render( + + + + ); + // Customer portal should render at root - no admin nav present + expect(screen.queryByText("Groom Book")).not.toBeInTheDocument(); + }); }); -- 2.52.0 From 8328f9a7767a420943ca2ff9794702415dca6516 Mon Sep 17 00:00:00 2001 From: Groom Book CTO Date: Thu, 19 Mar 2026 02:25:40 +0000 Subject: [PATCH 2/3] =?UTF-8?q?fix(e2e):=20update=20tests=20for=20routing?= =?UTF-8?q?=20flip=20=E2=80=94=20admin=20at=20/admin,=20portal=20at=20/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All E2E tests now use /admin prefix for admin routes (clients, services, staff, invoices, reports, book). Adds customer portal smoke test at /. Co-Authored-By: Claude Opus 4.6 --- apps/e2e/tests/book.spec.ts | 6 +++--- apps/e2e/tests/clients.spec.ts | 6 +++--- apps/e2e/tests/navigation.spec.ts | 32 ++++++++++++++++++------------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/e2e/tests/book.spec.ts b/apps/e2e/tests/book.spec.ts index 4a76f43..ecec404 100644 --- a/apps/e2e/tests/book.spec.ts +++ b/apps/e2e/tests/book.spec.ts @@ -46,7 +46,7 @@ test("complete booking flow", async ({ page }) => { // ── Step 1: Select a service ────────────────────────────────────────────── - await page.goto("/book"); + await page.goto("/admin/book"); await expect(page.getByText("Book an Appointment")).toBeVisible(); await expect(page.getByText("Choose a service")).toBeVisible(); @@ -99,7 +99,7 @@ test("booking form validation — required fields", async ({ page }) => { route.fulfill({ json: [MOCK_SLOT] }) ); - await page.goto("/book"); + await page.goto("/admin/book"); await page.getByText("Full Groom").click(); await page.getByRole("button", { name: /\d{1,2}:\d{2}/ }).first().click(); await page.getByRole("button", { name: "Continue" }).click(); @@ -115,6 +115,6 @@ test("no services available — shows message", async ({ page }) => { route.fulfill({ json: [] }) ); - await page.goto("/book"); + await page.goto("/admin/book"); await expect(page.getByText("No services available")).toBeVisible(); }); diff --git a/apps/e2e/tests/clients.spec.ts b/apps/e2e/tests/clients.spec.ts index 8e94e1a..4e681da 100644 --- a/apps/e2e/tests/clients.spec.ts +++ b/apps/e2e/tests/clients.spec.ts @@ -40,18 +40,18 @@ test.beforeEach(async ({ page }) => { }); test("clients page shows client list", async ({ page }) => { - await page.goto("/clients"); + await page.goto("/admin/clients"); await expect(page.getByText("Alice Johnson")).toBeVisible(); await expect(page.getByText("Bob Williams")).toBeVisible(); }); test("clients page shows search input", async ({ page }) => { - await page.goto("/clients"); + await page.goto("/admin/clients"); await expect(page.getByPlaceholder(/search/i)).toBeVisible(); }); test("clicking a client shows their details", async ({ page }) => { - await page.goto("/clients"); + await page.goto("/admin/clients"); await expect(page.getByText("Alice Johnson")).toBeVisible(); await page.getByText("Alice Johnson").click(); // Email appears in both the list row and the detail panel once selected diff --git a/apps/e2e/tests/navigation.spec.ts b/apps/e2e/tests/navigation.spec.ts index 1e7388a..f26c6e4 100644 --- a/apps/e2e/tests/navigation.spec.ts +++ b/apps/e2e/tests/navigation.spec.ts @@ -40,45 +40,51 @@ test.beforeEach(async ({ page }) => { }); }); -test("appointments page loads", async ({ page }) => { +test("customer portal loads at root", async ({ page }) => { await page.goto("/"); + await expect(page.getByText("Paws & Reflect")).toBeVisible(); + await expect(page.locator("nav")).toBeVisible(); +}); + +test("admin appointments page loads", async ({ page }) => { + await page.goto("/admin"); await expect(page.getByText("Groom Book")).toBeVisible(); // Calendar/appointments view renders await expect(page.locator("nav")).toBeVisible(); }); -test("clients page loads", async ({ page }) => { - await page.goto("/clients"); +test("admin clients page loads", async ({ page }) => { + await page.goto("/admin/clients"); await expect(page.getByText("Groom Book")).toBeVisible(); await expect(page.getByRole("link", { name: "Clients" })).toBeVisible(); }); -test("services page loads", async ({ page }) => { - await page.goto("/services"); +test("admin services page loads", async ({ page }) => { + await page.goto("/admin/services"); await expect(page.getByText("Groom Book")).toBeVisible(); await expect(page.getByRole("link", { name: "Services" })).toBeVisible(); }); -test("staff page loads", async ({ page }) => { - await page.goto("/staff"); +test("admin staff page loads", async ({ page }) => { + await page.goto("/admin/staff"); await expect(page.getByText("Groom Book")).toBeVisible(); await expect(page.getByRole("link", { name: "Staff" })).toBeVisible(); }); -test("invoices page loads", async ({ page }) => { - await page.goto("/invoices"); +test("admin invoices page loads", async ({ page }) => { + await page.goto("/admin/invoices"); await expect(page.getByText("Groom Book")).toBeVisible(); await expect(page.getByRole("link", { name: "Invoices" })).toBeVisible(); }); -test("reports page loads", async ({ page }) => { - await page.goto("/reports"); +test("admin reports page loads", async ({ page }) => { + await page.goto("/admin/reports"); await expect(page.getByText("Groom Book")).toBeVisible(); await expect(page.getByRole("link", { name: "Reports" })).toBeVisible(); }); -test("booking portal loads", async ({ page }) => { - await page.goto("/book"); +test("admin booking portal loads", async ({ page }) => { + await page.goto("/admin/book"); await expect(page.getByText("Book an Appointment")).toBeVisible(); await expect(page.getByText("Choose a service")).toBeVisible(); }); -- 2.52.0 From 76238e377b2e13d40c2cf41dd957ac5c5452799f Mon Sep 17 00:00:00 2001 From: Groom Book CTO Date: Thu, 19 Mar 2026 02:28:36 +0000 Subject: [PATCH 3/3] fix(e2e): use specific locator for customer portal test getByText('Paws & Reflect') matched 3 elements causing strict mode violation. Scope to navigation role for unique match. Co-Authored-By: Claude Opus 4.6 --- apps/e2e/tests/navigation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/e2e/tests/navigation.spec.ts b/apps/e2e/tests/navigation.spec.ts index f26c6e4..0b8e78f 100644 --- a/apps/e2e/tests/navigation.spec.ts +++ b/apps/e2e/tests/navigation.spec.ts @@ -42,7 +42,7 @@ test.beforeEach(async ({ page }) => { test("customer portal loads at root", async ({ page }) => { await page.goto("/"); - await expect(page.getByText("Paws & Reflect")).toBeVisible(); + await expect(page.getByRole("navigation").getByText("Paws & Reflect")).toBeVisible(); await expect(page.locator("nav")).toBeVisible(); }); -- 2.52.0