diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b5b4595..f844141 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import { Suspense } from 'react' -import { BrowserRouter, Routes, Route, Navigate, Outlet, useLocation, useParams } from 'react-router-dom' +import { BrowserRouter, Routes, Route, Navigate, Outlet, Link, useLocation, useParams } from 'react-router-dom' import { AuthProvider, useAuth } from './context/AuthContext' import { ThemeProvider } from './context/ThemeContext' import Navbar from './components/Navbar' @@ -75,12 +75,39 @@ function AppLayout() { ) } -// Guard: redirect to /home if not logged in, or to / if not moderator +/** + * Somewhere this account cannot go. + * + * It used to be a silent redirect to the dashboard, which leaves you looking + * at a page you did not ask for with no idea why — the link was dead, or you + * mistyped, or somebody sent you a page only an educator can open. It says + * which, and offers the two places you are actually likely to want. + */ +function Forbidden() { + return ( +
+

Not yours to open

+

+ This page is for educators and administrators. Your account is not one, + which is why the link went nowhere rather than because it is broken. +

+
+ Dashboard + Settings +
+
+ ) +} + +// Guard: sign-in is a redirect, because there is nothing to say — you simply +// are not signed in. A role you do not have is worth saying out loud. +export function RequireAuthForTest(props) { return RequireAuth(props) } + function RequireAuth({ moderator = false }) { const { user, loading } = useAuth() if (loading) return if (!user) return - if (moderator && user.role !== 'admin' && user.role !== 'moderator') return + if (moderator && user.role !== 'admin' && user.role !== 'moderator') return return } diff --git a/frontend/src/App.test.jsx b/frontend/src/App.test.jsx new file mode 100644 index 0000000..a65bc60 --- /dev/null +++ b/frontend/src/App.test.jsx @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' + +let currentUser = { id: 1, role: 'user' } +vi.mock('./context/AuthContext', () => ({ + useAuth: () => ({ user: currentUser, loading: false }), + AuthProvider: ({ children }) => children, +})) + +const { RequireAuthForTest } = await import('./App') + +const mount = (path = '/editorial') => render( + + + }> + Editorial} /> + + Dashboard} /> + + ) + +describe('a page this account cannot open', () => { + it('says so, and offers the way out', () => { + currentUser = { id: 1, role: 'user' } + mount() + // It used to redirect in silence, which leaves you on a page you did not + // ask for with no idea whether the link was broken or simply not yours. + expect(screen.getByRole('heading', { name: 'Not yours to open' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Settings' })).toHaveAttribute('href', '/settings') + expect(screen.getByRole('link', { name: 'Dashboard' })).toHaveAttribute('href', '/') + }) + + it('lets an educator through', () => { + currentUser = { id: 2, role: 'moderator' } + mount() + expect(screen.getByRole('heading', { name: 'Editorial' })).toBeInTheDocument() + }) +}) diff --git a/frontend/src/pages/DocumentDetailPage.css b/frontend/src/pages/DocumentDetailPage.css new file mode 100644 index 0000000..6748faf --- /dev/null +++ b/frontend/src/pages/DocumentDetailPage.css @@ -0,0 +1,16 @@ + +/* ── A section and what can be done to it ───────────────────────────── + The name took whatever width was left after a button group that would + not wrap, so "NBME 7 STEP 3" broke to one word a line while Extract, + Create Cards and Delete ran off the right of the card. On a narrow + screen the name takes the row and the buttons take the next one. */ +.doc-section { flex-wrap: wrap; gap: 10px; } +.doc-section-name { flex: 1 1 220px; min-width: 0; } +.doc-section-name strong { overflow-wrap: anywhere; } +.doc-section-actions { display: flex; gap: 8px; flex-wrap: wrap; flex: none; } + +@media (max-width: 640px) { + .doc-section-name { flex-basis: 100%; } + .doc-section-actions { width: 100%; } + .doc-section-actions > * { flex: 1; } +} diff --git a/frontend/src/pages/DocumentDetailPage.jsx b/frontend/src/pages/DocumentDetailPage.jsx index 2814a6f..fa6b733 100644 --- a/frontend/src/pages/DocumentDetailPage.jsx +++ b/frontend/src/pages/DocumentDetailPage.jsx @@ -3,6 +3,7 @@ import { useParams, useNavigate, Link } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import api from '../api/client' import ConfirmButton from '../components/ConfirmButton' +import './DocumentDetailPage.css' function ExtractionProgress({ jobId, onDone, onClose, label = 'Extracting Questions' }) { const [steps, setSteps] = useState([]) @@ -472,14 +473,17 @@ export default function DocumentDetailPage() { )} {doc.sections.map(section => ( -
-
+
+
{section.name} -
+
Pages {section.start_page}–{section.end_page}
-
+ {/* The row had a fixed name column and a non-wrapping button + group, so a long section title was squeezed to one word a + line while the buttons ran off the card. */} +
{isModerator && ( <>