From 55481b69c0c76e4544ecc6e3808e5275d71dc47a Mon Sep 17 00:00:00 2001
From: belviskhoremk
Date: Wed, 12 Aug 2026 11:06:40 +0200
Subject: [PATCH] feat: add admin layout with sidebar nav and sign out
---
src/pages/admin/AdminLayout.module.css | 53 +++++++++++++++++++++++++
src/pages/admin/AdminLayout.test.tsx | 54 ++++++++++++++++++++++++++
src/pages/admin/AdminLayout.tsx | 47 ++++++++++++++++++++++
3 files changed, 154 insertions(+)
create mode 100644 src/pages/admin/AdminLayout.module.css
create mode 100644 src/pages/admin/AdminLayout.test.tsx
create mode 100644 src/pages/admin/AdminLayout.tsx
diff --git a/src/pages/admin/AdminLayout.module.css b/src/pages/admin/AdminLayout.module.css
new file mode 100644
index 0000000..8ed3a4e
--- /dev/null
+++ b/src/pages/admin/AdminLayout.module.css
@@ -0,0 +1,53 @@
+.layout {
+ display: flex;
+ min-height: 100vh;
+}
+
+.sidebar {
+ width: 220px;
+ flex-shrink: 0;
+ background: var(--color-charcoal);
+ color: var(--color-cream);
+ padding: var(--space-lg) var(--space-md);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-lg);
+}
+
+.brand {
+ font-family: var(--font-script);
+ font-size: 1.5rem;
+ color: var(--color-gold);
+}
+
+.sidebar nav {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.navLink {
+ color: var(--color-cream);
+ text-decoration: none;
+ padding: var(--space-sm);
+}
+
+.navLinkActive {
+ color: var(--color-gold);
+ font-weight: bold;
+}
+
+.signOut {
+ margin-top: auto;
+ background: none;
+ border: 1px solid var(--color-gold);
+ color: var(--color-cream);
+ padding: var(--space-sm);
+ cursor: pointer;
+}
+
+.content {
+ flex: 1;
+ padding: var(--space-xl) var(--space-lg);
+ background: var(--color-cream-alt);
+}
diff --git a/src/pages/admin/AdminLayout.test.tsx b/src/pages/admin/AdminLayout.test.tsx
new file mode 100644
index 0000000..f8f0d6b
--- /dev/null
+++ b/src/pages/admin/AdminLayout.test.tsx
@@ -0,0 +1,54 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { MemoryRouter, Routes, Route } from 'react-router-dom'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+const signOutMock = vi.fn()
+const navigateMock = vi.fn()
+
+vi.mock('../../lib/supabase', () => ({
+ supabase: { auth: { signOut: (...args: unknown[]) => signOutMock(...args) } },
+}))
+
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom')
+ return { ...actual, useNavigate: () => navigateMock }
+})
+
+import AdminLayout from './AdminLayout'
+
+function renderLayout() {
+ return render(
+
+
+ }>
+ Products content
} />
+
+
+ ,
+ )
+}
+
+describe('AdminLayout', () => {
+ beforeEach(() => {
+ signOutMock.mockReset().mockResolvedValue({ error: null })
+ navigateMock.mockReset()
+ })
+
+ it('renders nav links for every admin section and the outlet content', () => {
+ renderLayout()
+ expect(screen.getByRole('link', { name: /products/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /services/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /schedule/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /bookings/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /inquiries/i })).toBeInTheDocument()
+ expect(screen.getByText('Products content')).toBeInTheDocument()
+ })
+
+ it('signs out and navigates to the login page', async () => {
+ renderLayout()
+ await userEvent.click(screen.getByRole('button', { name: /sign out/i }))
+ expect(signOutMock).toHaveBeenCalled()
+ expect(navigateMock).toHaveBeenCalledWith('/admin/login')
+ })
+})
diff --git a/src/pages/admin/AdminLayout.tsx b/src/pages/admin/AdminLayout.tsx
new file mode 100644
index 0000000..1bb1094
--- /dev/null
+++ b/src/pages/admin/AdminLayout.tsx
@@ -0,0 +1,47 @@
+import { NavLink, Outlet, useNavigate } from 'react-router-dom'
+import { supabase } from '../../lib/supabase'
+import styles from './AdminLayout.module.css'
+
+const NAV_ITEMS = [
+ { to: '/admin/products', label: 'Products' },
+ { to: '/admin/services', label: 'Services' },
+ { to: '/admin/schedule', label: 'Schedule' },
+ { to: '/admin/bookings', label: 'Bookings' },
+ { to: '/admin/inquiries', label: 'Inquiries' },
+]
+
+function AdminLayout() {
+ const navigate = useNavigate()
+
+ async function handleSignOut() {
+ await supabase.auth.signOut()
+ navigate('/admin/login')
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default AdminLayout