From 06433195bbf2a0525f2ebf8e7e6f757d2737d615 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 12 Sep 2026 01:21:20 +0200 Subject: [PATCH] fix: a page you cannot open says so; a section row fits its card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hitting an educators-only page redirected to the dashboard in silence, which leaves you looking at a page you did not ask for with no idea whether the link was broken, you mistyped, or it simply is not yours. It says which, and offers the dashboard and settings — the two places you are actually likely to want from there. Not being signed in stays a redirect, because there is nothing to explain. The section row gave its name 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. The page had a stylesheet that nothing imported, which is why it had no layout rules of its own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/App.jsx | 33 +++++++++++++++++-- frontend/src/App.test.jsx | 39 +++++++++++++++++++++++ frontend/src/pages/DocumentDetailPage.css | 16 ++++++++++ frontend/src/pages/DocumentDetailPage.jsx | 12 ++++--- 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 frontend/src/App.test.jsx create mode 100644 frontend/src/pages/DocumentDetailPage.css 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 && ( <>