From e62a742b733393c2958bc9ce1c376315634011d9 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 04:36:44 +0200 Subject: [PATCH] fix: Reading cannot edit, and the menu button reaches the menu everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading is read-only. Edit, Unpublish and Generate cards sat in the reading toolbar, so an educator reading between questions was one mis-tap from the editor — which is how somebody opened edit mode on Bronchiolitis in the middle of a session. All three live at /editorial/articles/:id now, with one named door through from Reading for whoever may edit. An ?edit=1 on a reading address is not ignored: the ask is fine, the address is wrong, so it is taken to the editorial one. And a cross-reference followed from Editorial stays in Editorial. Reading and Editorial are two modes of the same page, and one link out of the second into the first put an educator into the learner's view of the next article with no way back to the queue. The menu button now reaches the menu on every page it has been taken over on. On a phone the burger belongs to whatever is on screen; the player handed back the site menu as a tab inside its drawer, and an article and an answer review did not — so on those two the only menu button on the page could not open the menu. Both have the player's own two tabs now, from one list of links rather than three copies of it (one of which pointed Qbank at /questions, which has never been a route). And the button shows a cross while what it opens is open. It kept its three bars behind an open drawer, an inch from the cross inside that drawer: two controls disagreeing about what was on screen. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/components/ArticleLink.jsx | 9 +++++-- frontend/src/components/ArticleReader.jsx | 31 ++++++++++++++++++++-- frontend/src/components/Navbar.jsx | 16 +++++++++--- frontend/src/components/SiteMenuLinks.jsx | 27 +++++++++++++++++++ frontend/src/context/SessionDrawer.jsx | 15 ++++++++--- frontend/src/pages/ArticlesPage.css | 32 +++++++++++++++++++++++ frontend/src/pages/ArticlesPage.jsx | 26 ++++++++++++++++-- frontend/src/pages/ArticlesPage.test.jsx | 26 +++++++++++++++++- frontend/src/pages/QuizPage.jsx | 28 ++++++++++---------- frontend/src/pages/ResultsPage.jsx | 20 ++++++++++++-- 10 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/SiteMenuLinks.jsx diff --git a/frontend/src/components/ArticleLink.jsx b/frontend/src/components/ArticleLink.jsx index 2a07ded..57b726b 100644 --- a/frontend/src/components/ArticleLink.jsx +++ b/frontend/src/components/ArticleLink.jsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' -import { Link } from 'react-router-dom' +import { Link, useLocation } from 'react-router-dom' import api from '../api/client' import { useSplitView } from '../context/SplitViewContext' import './ArticleLink.css' @@ -60,7 +60,12 @@ export default function ArticleLink({ slug, sectionId = null, children, classNam // A citation can point at one section rather than the whole article; the // reader opens on it. Nothing in prose writes one, so it is normally absent. const where = sectionId ? `?section=${encodeURIComponent(sectionId)}` : '' - const href = (/^\d+$/.test(slug) ? `/articles/${slug}` : `/articles/s/${slug}`) + where + // A cross-reference followed from Editorial stays in Editorial. Reading and + // Editorial are two modes of the same page, and one link out of the second + // into the first put an educator into the learner's view of the next + // article with no way back to the queue. + const root = useLocation().pathname.startsWith('/editorial') ? '/editorial/articles' : '/articles' + const href = (/^\d+$/.test(slug) ? `${root}/${slug}` : `${root}/s/${slug}`) + where useEffect(() => () => clearTimeout(timer.current), []) diff --git a/frontend/src/components/ArticleReader.jsx b/frontend/src/components/ArticleReader.jsx index b38d54e..a1c012e 100644 --- a/frontend/src/components/ArticleReader.jsx +++ b/frontend/src/components/ArticleReader.jsx @@ -7,6 +7,7 @@ import '../pages/ArticlesPage.css' import useMediaQuery from '../hooks/useMediaQuery' import useHeaderOffset from '../hooks/useHeaderOffset' import { useSessionDrawer } from '../context/SessionDrawer' +import SiteMenuLinks from './SiteMenuLinks' import RichText from './RichText' /** * Article prose goes through the same renderer as everything else. @@ -94,6 +95,11 @@ export default function ArticleReader({ // you consult, and a wall of prose hides the one heading you came for. const [openIds, setOpenIds] = useState({}) const [drawerOpen, setDrawerOpen] = useState(false) + //: Which half of the drawer is showing. The same two the session player + //: offers, because it is the same button: the thing you are in, and the way + //: out of it. Reading had only the contents, so the burger on an article was + //: a menu button that could not reach the menu. + const [drawerTab, setDrawerTab] = useState('contents') // On a phone the contents live behind the same button as the site menu, the // way a session's questions do: one control at the top left whose contents // change with where you are, rather than a second menu to hunt for. @@ -105,13 +111,15 @@ export default function ArticleReader({ // was still sitting there in the layout: the contents did not open, and the // site menu did not either. The button was simply dead. const narrow = useMediaQuery('(max-width: 820px)') - const { register: registerDrawer } = useSessionDrawer() + const { register: registerDrawer, setOpen: setDrawerShown } = useSessionDrawer() useEffect(() => { if (!narrow || bare) return undefined // A toggle, not an opener: the button that opened the contents is the // one a thumb goes back to, and pressing it again did nothing at all. return registerDrawer(() => setDrawerOpen(open => !open)) }, [narrow, bare, registerDrawer]) + // So the header's button can show a cross while this is open. + useEffect(() => { setDrawerShown(drawerOpen) }, [drawerOpen, setDrawerShown]) // Collapsing the contents rail hands its width to the prose. const [railOpen, setRailOpen] = useState(() => stored(RAIL_KEY, 'open') !== 'closed') const [size, setSize] = useState(() => (SIZES.includes(stored(SIZE_KEY, 'm')) ? stored(SIZE_KEY, 'm') : 'm')) @@ -381,6 +389,24 @@ export default function ArticleReader({ onClick={() => setDrawerOpen(false)} /> )}