feat: add account navigation and footer

Move Inbox into a top-right account area, add a profile page for the authenticated user, and introduce a configurable sitemap and useful-links footer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Daniel Noulet 2026-09-20 07:21:10 +02:00
parent 0d8a25e73f
commit 83d14d23a2
9 changed files with 388 additions and 36 deletions

View file

@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BrowserRouter, MemoryRouter } from 'react-router-dom'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@ -220,6 +220,37 @@ describe('Go Guess frontend', () => {
expect(screen.queryByRole('link', { name: 'Senior Go Engineer' })).not.toBeInTheDocument()
})
it('moves account links to the top and renders the logged-in user profile and footer', async () => {
authenticate()
renderApp('/profile')
expect(await screen.findByRole('heading', { level: 1, name: 'Admin' })).toBeInTheDocument()
expect(screen.getByText('admin@example.com')).toBeInTheDocument()
const accountNavigation = screen.getByRole('navigation', { name: 'Account navigation' })
expect(within(accountNavigation).getByRole('link', { name: 'Inbox' })).toHaveAttribute(
'href',
'/inbox',
)
expect(within(accountNavigation).getByRole('link', { name: 'Admin' })).toHaveAttribute(
'href',
'/profile',
)
const primaryNavigation = screen.getByRole('navigation', { name: 'Primary navigation' })
expect(within(primaryNavigation).queryByRole('link', { name: 'Inbox' })).not.toBeInTheDocument()
const footerNavigation = screen.getByRole('navigation', { name: 'Footer navigation' })
expect(within(footerNavigation).getByRole('link', { name: 'Jobs' })).toHaveAttribute(
'href',
'/jobs',
)
expect(within(footerNavigation).getByRole('link', { name: 'Your profile' })).toHaveAttribute(
'href',
'/profile',
)
})
it('adds multiple choice options with the accessible plus button', async () => {
authenticate()
const user = userEvent.setup()

View file

@ -16,7 +16,6 @@ import {
Plus,
Search,
Send,
Inbox,
Sparkles,
Trash2,
Upload,
@ -52,6 +51,9 @@ import {
ScheduledInterviewPage,
} from '../features/interviews'
import { UsersPage } from '../features/users'
import { ProfilePage } from '../features/users'
import { AppFooter } from '../components/layout/AppFooter'
import { WorkspaceUtilities } from '../components/layout/WorkspaceUtilities'
import type {
CandidateMatch,
CreateJobInput,
@ -292,12 +294,6 @@ function AppLayout({ onLogout }: { onLogout: () => void }) {
icon={<CircleHelp size={20} />}
close={() => setOpen(false)}
/>
<NavItem
to="/inbox"
label="Inbox"
icon={<Inbox size={20} />}
close={() => setOpen(false)}
/>
<NavItem
to="/calendar"
label="Calendar"
@ -312,35 +308,34 @@ function AppLayout({ onLogout }: { onLogout: () => void }) {
/>
</nav>
<div className="sidebar-bottom">
<div className="user-chip">
<span className="avatar tiny">{user?.email.slice(0, 2).toUpperCase() ?? 'U'}</span>
<span>
<strong>{user?.email ?? 'Signed in'}</strong>
<small>{user?.role ?? 'User'}</small>
</span>
</div>
<button className="icon-button" onClick={onLogout} aria-label="Sign out">
<LogOut size={19} />
<button className="sidebar-signout" onClick={onLogout}>
<LogOut size={18} />
Sign out
</button>
</div>
</aside>
<main className="main-content">
<Routes>
<Route index element={<Navigate to="/jobs" replace />} />
<Route path="/jobs" element={<JobsPage />} />
<Route path="/jobs/new" element={<CreateJobPage />} />
<Route path="/jobs/:jobId" element={<JobDetailPage />} />
<Route path="/jobs/:jobId/questions/new" element={<QuestionPage />} />
<Route path="/questions" element={<QuestionLibraryPage />} />
<Route path="/participants" element={<ParticipantsPage />} />
<Route path="/participants/new" element={<CreateParticipantPage />} />
<Route path="/participants/:participantId" element={<ParticipantDetailPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/inbox" element={<InboxPage />} />
<Route path="/calendar" element={<CalendarPage />} />
<Route path="/scheduled-interviews/:id" element={<ScheduledInterviewPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
<WorkspaceUtilities user={user} />
<div className="workspace-content">
<Routes>
<Route index element={<Navigate to="/jobs" replace />} />
<Route path="/jobs" element={<JobsPage />} />
<Route path="/jobs/new" element={<CreateJobPage />} />
<Route path="/jobs/:jobId" element={<JobDetailPage />} />
<Route path="/jobs/:jobId/questions/new" element={<QuestionPage />} />
<Route path="/questions" element={<QuestionLibraryPage />} />
<Route path="/participants" element={<ParticipantsPage />} />
<Route path="/participants/new" element={<CreateParticipantPage />} />
<Route path="/participants/:participantId" element={<ParticipantDetailPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/inbox" element={<InboxPage />} />
<Route path="/calendar" element={<CalendarPage />} />
<Route path="/scheduled-interviews/:id" element={<ScheduledInterviewPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
<AppFooter />
</main>
</div>
)