feat: wire up /admin/* routing

Adds AdminApp as a lazy-loaded route subtree in App.tsx, mounting
ProtectedRoute, AdminLogin, AdminLayout, and all five CRUD pages built
in Tasks 3-11. Mounted as a sibling of the public Layout route (not
nested under it), since the admin panel has its own AdminLayout
sidebar rather than the public nav/footer.

Manually verified end-to-end against the local Supabase stack: sign-in
redirect guard, all five pages rendering real seeded data, creating and
cancelling a real booking through the UI (confirming the booked_slots
cleanup trigger from the merged Phase 2A follow-up fixes correctly
frees the slot), and sign-out. No console errors across the flow.

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-12 12:52:25 +02:00
parent 71b812d5d4
commit 540b8b407b
4 changed files with 82 additions and 0 deletions

3
.gitignore vendored
View File

@@ -32,3 +32,6 @@ dist-ssr
# Supabase CLI local artifacts
supabase/.branches
supabase/.temp
# Playwright MCP scratch output (screenshots, console logs, page snapshots)
.playwright-mcp/

View File

@@ -6,6 +6,7 @@ import Book from './pages/Book'
import Rules from './pages/Rules'
const Home = lazy(() => import('./pages/Home'))
const AdminApp = lazy(() => import('./pages/admin/AdminApp'))
interface AppProps {
initialEntries?: string[]
@@ -27,6 +28,14 @@ function AppRoutes() {
<Route path="/book" element={<Book />} />
<Route path="/rules" element={<Rules />} />
</Route>
<Route
path="/admin/*"
element={
<Suspense fallback={null}>
<AdminApp />
</Suspense>
}
/>
</Routes>
)
}

View File

@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import { describe, it, expect, vi } from 'vitest'
vi.mock('../../hooks/useAdminAuth', () => ({
default: () => ({ session: { user: { id: 'admin' } }, loading: false }),
}))
vi.mock('../../lib/supabase', () => ({
supabase: {
auth: { signOut: vi.fn() },
from: () => ({
select: () => ({ order: () => Promise.resolve({ data: [], error: null }) }),
}),
},
}))
import AdminApp from './AdminApp'
describe('AdminApp routing', () => {
it('renders the login page at /admin/login', () => {
render(
<MemoryRouter initialEntries={['/admin/login']}>
<Routes>
<Route path="/admin/*" element={<AdminApp />} />
</Routes>
</MemoryRouter>,
)
expect(screen.getByRole('heading', { name: /admin login/i })).toBeInTheDocument()
})
it('renders the products page (with layout) at /admin/products when authenticated', async () => {
render(
<MemoryRouter initialEntries={['/admin/products']}>
<Routes>
<Route path="/admin/*" element={<AdminApp />} />
</Routes>
</MemoryRouter>,
)
expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /bookings/i })).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,28 @@
import { Routes, Route } from 'react-router-dom'
import ProtectedRoute from '../../components/ProtectedRoute'
import AdminLogin from './AdminLogin'
import AdminLayout from './AdminLayout'
import AdminProducts from './AdminProducts'
import AdminServices from './AdminServices'
import AdminSchedule from './AdminSchedule'
import AdminBookings from './AdminBookings'
import AdminInquiries from './AdminInquiries'
function AdminApp() {
return (
<Routes>
<Route path="login" element={<AdminLogin />} />
<Route element={<ProtectedRoute />}>
<Route element={<AdminLayout />}>
<Route path="products" element={<AdminProducts />} />
<Route path="services" element={<AdminServices />} />
<Route path="schedule" element={<AdminSchedule />} />
<Route path="bookings" element={<AdminBookings />} />
<Route path="inquiries" element={<AdminInquiries />} />
</Route>
</Route>
</Routes>
)
}
export default AdminApp