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