feat: add admin layout with sidebar nav and sign out

This commit is contained in:
belviskhoremk
2026-08-12 11:06:40 +02:00
parent 16d5503193
commit 55481b69c0
3 changed files with 154 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -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(
<MemoryRouter initialEntries={['/admin/products']}>
<Routes>
<Route element={<AdminLayout />}>
<Route path="/admin/products" element={<p>Products content</p>} />
</Route>
</Routes>
</MemoryRouter>,
)
}
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')
})
})

View File

@@ -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 (
<div className={styles.layout}>
<aside className={styles.sidebar}>
<p className={styles.brand}>Braids by Mira</p>
<nav>
{NAV_ITEMS.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) => (isActive ? `${styles.navLink} ${styles.navLinkActive}` : styles.navLink)}
>
{item.label}
</NavLink>
))}
</nav>
<button type="button" className={styles.signOut} onClick={handleSignOut}>
Sign out
</button>
</aside>
<main className={styles.content}>
<Outlet />
</main>
</div>
)
}
export default AdminLayout