Spaces:
Build error
feat(frontend): Sprint Fix 4 — React Router, accessibility, translations
Browse files5 issues fixed:
- #24: Add react-router-dom v7, replace useState-based view routing with
URL-based Routes: /, /admin, /reader/:manuscriptId, /editor/:pageId.
F5 reloads to same page, browser back button works, shareable URLs.
- #18: Fix Editor→Back navigation: navigate(-1) returns to Reader (was
always going to Home due to setState({ name: 'home' }))
- #15: RetroCheckbox now uses a real <input type="checkbox"> (sr-only)
with proper htmlFor/id, keyboard accessible (Tab + Space)
- #16: Accessibility improvements across retro components:
- RetroMenuBar: <div> → <nav aria-label="Menu principal">
- RetroIcon: add aria-label={label} to button
- RetroInput: add htmlFor + auto-generated id
- RetroSelect: add htmlFor + auto-generated id
- #35: TranslationPanel shows FR and EN based on profile active_layers
(was hardcoded to French only)
TypeScript compiles clean. 563 backend tests pass, 0 regressions.
https://claude.ai/code/session_01UB4he7RdRPHLvNjky4X8Sw
- frontend/package.json +2 -1
- frontend/src/App.tsx +7 -40
- frontend/src/components/TranslationPanel.tsx +33 -8
- frontend/src/components/retro/RetroCheckbox.tsx +14 -1
- frontend/src/components/retro/RetroIcon.tsx +1 -0
- frontend/src/components/retro/RetroInput.tsx +6 -2
- frontend/src/components/retro/RetroMenuBar.tsx +3 -2
- frontend/src/components/retro/RetroSelect.tsx +6 -2
- frontend/src/main.tsx +4 -1
- frontend/src/pages/Admin.tsx +4 -5
- frontend/src/pages/Editor.tsx +6 -8
- frontend/src/pages/Home.tsx +8 -12
- frontend/src/pages/Reader.tsx +13 -16
|
@@ -10,7 +10,8 @@
|
|
| 10 |
"dependencies": {
|
| 11 |
"openseadragon": "^4.1.0",
|
| 12 |
"react": "^18.3.1",
|
| 13 |
-
"react-dom": "^18.3.1"
|
|
|
|
| 14 |
},
|
| 15 |
"devDependencies": {
|
| 16 |
"@types/openseadragon": "^3.0.10",
|
|
|
|
| 10 |
"dependencies": {
|
| 11 |
"openseadragon": "^4.1.0",
|
| 12 |
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1",
|
| 14 |
+
"react-router-dom": "^7.14.0"
|
| 15 |
},
|
| 16 |
"devDependencies": {
|
| 17 |
"@types/openseadragon": "^3.0.10",
|
|
@@ -1,49 +1,16 @@
|
|
| 1 |
-
import {
|
| 2 |
import Admin from './pages/Admin.tsx'
|
| 3 |
import Editor from './pages/Editor.tsx'
|
| 4 |
import Home from './pages/Home.tsx'
|
| 5 |
import Reader from './pages/Reader.tsx'
|
| 6 |
|
| 7 |
-
type View =
|
| 8 |
-
| { name: 'home' }
|
| 9 |
-
| { name: 'reader'; manuscriptId: string; profileId: string }
|
| 10 |
-
| { name: 'admin' }
|
| 11 |
-
| { name: 'editor'; pageId: string }
|
| 12 |
-
|
| 13 |
export default function App() {
|
| 14 |
-
const [view, setView] = useState<View>({ name: 'home' })
|
| 15 |
-
|
| 16 |
-
if (view.name === 'reader') {
|
| 17 |
-
return (
|
| 18 |
-
<Reader
|
| 19 |
-
manuscriptId={view.manuscriptId}
|
| 20 |
-
profileId={view.profileId}
|
| 21 |
-
onBack={() => setView({ name: 'home' })}
|
| 22 |
-
onEdit={(pageId) => setView({ name: 'editor', pageId })}
|
| 23 |
-
/>
|
| 24 |
-
)
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
if (view.name === 'admin') {
|
| 28 |
-
return <Admin onHome={() => setView({ name: 'home' })} />
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
if (view.name === 'editor') {
|
| 32 |
-
return (
|
| 33 |
-
<Editor
|
| 34 |
-
pageId={view.pageId}
|
| 35 |
-
onBack={() => setView({ name: 'home' })}
|
| 36 |
-
/>
|
| 37 |
-
)
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
return (
|
| 41 |
-
<
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
/>
|
| 48 |
)
|
| 49 |
}
|
|
|
|
| 1 |
+
import { Routes, Route } from 'react-router-dom'
|
| 2 |
import Admin from './pages/Admin.tsx'
|
| 3 |
import Editor from './pages/Editor.tsx'
|
| 4 |
import Home from './pages/Home.tsx'
|
| 5 |
import Reader from './pages/Reader.tsx'
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
export default function App() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
return (
|
| 9 |
+
<Routes>
|
| 10 |
+
<Route path="/" element={<Home />} />
|
| 11 |
+
<Route path="/admin" element={<Admin />} />
|
| 12 |
+
<Route path="/reader/:manuscriptId" element={<Reader />} />
|
| 13 |
+
<Route path="/editor/:pageId" element={<Editor />} />
|
| 14 |
+
</Routes>
|
|
|
|
| 15 |
)
|
| 16 |
}
|
|
@@ -22,25 +22,50 @@ interface Props {
|
|
| 22 |
translation: Translation | null
|
| 23 |
editorial: EditorialInfo
|
| 24 |
visible: boolean
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
const TranslationPanel: FC<Props> = ({ translation, editorial, visible }) => {
|
| 28 |
if (!visible) return null
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
return (
|
| 31 |
<div className="p-2">
|
| 32 |
<div className="flex items-center justify-between mb-2">
|
| 33 |
-
<span className="text-retro-xs font-bold">Traduction
|
| 34 |
<RetroBadge variant={STATUS_VARIANTS[editorial.status]}>
|
| 35 |
{STATUS_LABELS[editorial.status]}
|
| 36 |
</RetroBadge>
|
| 37 |
</div>
|
| 38 |
-
{
|
| 39 |
-
<
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
)}
|
| 45 |
</div>
|
| 46 |
)
|
|
|
|
| 22 |
translation: Translation | null
|
| 23 |
editorial: EditorialInfo
|
| 24 |
visible: boolean
|
| 25 |
+
/** Active layers from profile — controls which languages are shown */
|
| 26 |
+
activeLayers?: string[]
|
| 27 |
}
|
| 28 |
|
| 29 |
+
const TranslationPanel: FC<Props> = ({ translation, editorial, visible, activeLayers }) => {
|
| 30 |
if (!visible) return null
|
| 31 |
|
| 32 |
+
const showFr = !activeLayers || activeLayers.includes('translation_fr')
|
| 33 |
+
const showEn = !activeLayers || activeLayers.includes('translation_en')
|
| 34 |
+
|
| 35 |
return (
|
| 36 |
<div className="p-2">
|
| 37 |
<div className="flex items-center justify-between mb-2">
|
| 38 |
+
<span className="text-retro-xs font-bold">Traduction</span>
|
| 39 |
<RetroBadge variant={STATUS_VARIANTS[editorial.status]}>
|
| 40 |
{STATUS_LABELS[editorial.status]}
|
| 41 |
</RetroBadge>
|
| 42 |
</div>
|
| 43 |
+
{showFr && (
|
| 44 |
+
<div className="mb-2">
|
| 45 |
+
<div className="text-retro-xs font-bold text-retro-darkgray mb-1">FR</div>
|
| 46 |
+
{translation?.fr ? (
|
| 47 |
+
<p className="text-retro-sm whitespace-pre-wrap font-retro leading-relaxed">
|
| 48 |
+
{translation.fr}
|
| 49 |
+
</p>
|
| 50 |
+
) : (
|
| 51 |
+
<p className="text-retro-sm text-retro-darkgray">Traduction FR non disponible.</p>
|
| 52 |
+
)}
|
| 53 |
+
</div>
|
| 54 |
+
)}
|
| 55 |
+
{showEn && (
|
| 56 |
+
<div>
|
| 57 |
+
<div className="text-retro-xs font-bold text-retro-darkgray mb-1">EN</div>
|
| 58 |
+
{translation?.en ? (
|
| 59 |
+
<p className="text-retro-sm whitespace-pre-wrap font-retro leading-relaxed">
|
| 60 |
+
{translation.en}
|
| 61 |
+
</p>
|
| 62 |
+
) : (
|
| 63 |
+
<p className="text-retro-sm text-retro-darkgray">Traduction EN non disponible.</p>
|
| 64 |
+
)}
|
| 65 |
+
</div>
|
| 66 |
+
)}
|
| 67 |
+
{!showFr && !showEn && (
|
| 68 |
+
<p className="text-retro-sm text-retro-darkgray">Aucune couche de traduction active.</p>
|
| 69 |
)}
|
| 70 |
</div>
|
| 71 |
)
|
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
interface Props {
|
| 2 |
/** Label text next to the checkbox */
|
| 3 |
label: string
|
|
@@ -18,9 +20,11 @@ export default function RetroCheckbox({
|
|
| 18 |
disabled = false,
|
| 19 |
className = '',
|
| 20 |
}: Props) {
|
|
|
|
|
|
|
| 21 |
return (
|
| 22 |
<label
|
| 23 |
-
|
| 24 |
className={`
|
| 25 |
inline-flex items-center gap-[6px]
|
| 26 |
text-retro-sm font-retro
|
|
@@ -29,7 +33,16 @@ export default function RetroCheckbox({
|
|
| 29 |
${className}
|
| 30 |
`}
|
| 31 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
<span
|
|
|
|
| 33 |
className={`
|
| 34 |
inline-flex items-center justify-center
|
| 35 |
w-[13px] h-[13px]
|
|
|
|
| 1 |
+
import { useId } from 'react'
|
| 2 |
+
|
| 3 |
interface Props {
|
| 4 |
/** Label text next to the checkbox */
|
| 5 |
label: string
|
|
|
|
| 20 |
disabled = false,
|
| 21 |
className = '',
|
| 22 |
}: Props) {
|
| 23 |
+
const id = useId()
|
| 24 |
+
|
| 25 |
return (
|
| 26 |
<label
|
| 27 |
+
htmlFor={id}
|
| 28 |
className={`
|
| 29 |
inline-flex items-center gap-[6px]
|
| 30 |
text-retro-sm font-retro
|
|
|
|
| 33 |
${className}
|
| 34 |
`}
|
| 35 |
>
|
| 36 |
+
<input
|
| 37 |
+
id={id}
|
| 38 |
+
type="checkbox"
|
| 39 |
+
checked={checked}
|
| 40 |
+
onChange={(e) => onChange(e.target.checked)}
|
| 41 |
+
disabled={disabled}
|
| 42 |
+
className="sr-only"
|
| 43 |
+
/>
|
| 44 |
<span
|
| 45 |
+
aria-hidden="true"
|
| 46 |
className={`
|
| 47 |
inline-flex items-center justify-center
|
| 48 |
w-[13px] h-[13px]
|
|
@@ -22,6 +22,7 @@ export default function RetroIcon({
|
|
| 22 |
<button
|
| 23 |
type="button"
|
| 24 |
onClick={onClick}
|
|
|
|
| 25 |
className={`
|
| 26 |
flex flex-col items-center gap-1
|
| 27 |
p-2 w-[80px]
|
|
|
|
| 22 |
<button
|
| 23 |
type="button"
|
| 24 |
onClick={onClick}
|
| 25 |
+
aria-label={label}
|
| 26 |
className={`
|
| 27 |
flex flex-col items-center gap-1
|
| 28 |
p-2 w-[80px]
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
|
| 3 |
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
| 4 |
/** Optional label rendered above the input */
|
|
@@ -6,14 +6,18 @@ interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
|
| 6 |
}
|
| 7 |
|
| 8 |
export default function RetroInput({ label, className = '', ...rest }: Props) {
|
|
|
|
|
|
|
|
|
|
| 9 |
return (
|
| 10 |
<div className="flex flex-col gap-[2px]">
|
| 11 |
{label && (
|
| 12 |
-
<label className="text-retro-xs font-retro font-medium text-retro-black">
|
| 13 |
{label}
|
| 14 |
</label>
|
| 15 |
)}
|
| 16 |
<input
|
|
|
|
| 17 |
className={`
|
| 18 |
px-2 py-[3px]
|
| 19 |
text-retro-sm font-retro
|
|
|
|
| 1 |
+
import { useId, type InputHTMLAttributes } from 'react'
|
| 2 |
|
| 3 |
interface Props extends InputHTMLAttributes<HTMLInputElement> {
|
| 4 |
/** Optional label rendered above the input */
|
|
|
|
| 6 |
}
|
| 7 |
|
| 8 |
export default function RetroInput({ label, className = '', ...rest }: Props) {
|
| 9 |
+
const generatedId = useId()
|
| 10 |
+
const inputId = rest.id ?? generatedId
|
| 11 |
+
|
| 12 |
return (
|
| 13 |
<div className="flex flex-col gap-[2px]">
|
| 14 |
{label && (
|
| 15 |
+
<label htmlFor={inputId} className="text-retro-xs font-retro font-medium text-retro-black">
|
| 16 |
{label}
|
| 17 |
</label>
|
| 18 |
)}
|
| 19 |
<input
|
| 20 |
+
id={inputId}
|
| 21 |
className={`
|
| 22 |
px-2 py-[3px]
|
| 23 |
text-retro-sm font-retro
|
|
@@ -17,7 +17,8 @@ interface Props {
|
|
| 17 |
|
| 18 |
export default function RetroMenuBar({ items = [], right, className = '' }: Props) {
|
| 19 |
return (
|
| 20 |
-
<
|
|
|
|
| 21 |
className={`
|
| 22 |
flex items-center
|
| 23 |
bg-retro-gray
|
|
@@ -51,6 +52,6 @@ export default function RetroMenuBar({ items = [], right, className = '' }: Prop
|
|
| 51 |
{right}
|
| 52 |
</div>
|
| 53 |
)}
|
| 54 |
-
</
|
| 55 |
)
|
| 56 |
}
|
|
|
|
| 17 |
|
| 18 |
export default function RetroMenuBar({ items = [], right, className = '' }: Props) {
|
| 19 |
return (
|
| 20 |
+
<nav
|
| 21 |
+
aria-label="Menu principal"
|
| 22 |
className={`
|
| 23 |
flex items-center
|
| 24 |
bg-retro-gray
|
|
|
|
| 52 |
{right}
|
| 53 |
</div>
|
| 54 |
)}
|
| 55 |
+
</nav>
|
| 56 |
)
|
| 57 |
}
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
|
| 3 |
interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
|
| 4 |
/** Optional label rendered above the select */
|
|
@@ -8,14 +8,18 @@ interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
export default function RetroSelect({ label, options, className = '', ...rest }: Props) {
|
|
|
|
|
|
|
|
|
|
| 11 |
return (
|
| 12 |
<div className="flex flex-col gap-[2px]">
|
| 13 |
{label && (
|
| 14 |
-
<label className="text-retro-xs font-retro font-medium text-retro-black">
|
| 15 |
{label}
|
| 16 |
</label>
|
| 17 |
)}
|
| 18 |
<select
|
|
|
|
| 19 |
className={`
|
| 20 |
px-2 py-[3px]
|
| 21 |
text-retro-sm font-retro
|
|
|
|
| 1 |
+
import { useId, type SelectHTMLAttributes } from 'react'
|
| 2 |
|
| 3 |
interface Props extends SelectHTMLAttributes<HTMLSelectElement> {
|
| 4 |
/** Optional label rendered above the select */
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
export default function RetroSelect({ label, options, className = '', ...rest }: Props) {
|
| 11 |
+
const generatedId = useId()
|
| 12 |
+
const selectId = rest.id ?? generatedId
|
| 13 |
+
|
| 14 |
return (
|
| 15 |
<div className="flex flex-col gap-[2px]">
|
| 16 |
{label && (
|
| 17 |
+
<label htmlFor={selectId} className="text-retro-xs font-retro font-medium text-retro-black">
|
| 18 |
{label}
|
| 19 |
</label>
|
| 20 |
)}
|
| 21 |
<select
|
| 22 |
+
id={selectId}
|
| 23 |
className={`
|
| 24 |
px-2 py-[3px]
|
| 25 |
text-retro-sm font-retro
|
|
@@ -1,10 +1,13 @@
|
|
| 1 |
import React from 'react'
|
| 2 |
import ReactDOM from 'react-dom/client'
|
|
|
|
| 3 |
import App from './App.tsx'
|
| 4 |
import './index.css'
|
| 5 |
|
| 6 |
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
<React.StrictMode>
|
| 8 |
-
<
|
|
|
|
|
|
|
| 9 |
</React.StrictMode>,
|
| 10 |
)
|
|
|
|
| 1 |
import React from 'react'
|
| 2 |
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import { BrowserRouter } from 'react-router-dom'
|
| 4 |
import App from './App.tsx'
|
| 5 |
import './index.css'
|
| 6 |
|
| 7 |
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 8 |
<React.StrictMode>
|
| 9 |
+
<BrowserRouter>
|
| 10 |
+
<App />
|
| 11 |
+
</BrowserRouter>
|
| 12 |
</React.StrictMode>,
|
| 13 |
)
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { type FormEvent, useEffect, useRef, useState } from 'react'
|
|
|
|
| 2 |
import {
|
| 3 |
fetchCorpora,
|
| 4 |
fetchManuscripts,
|
|
@@ -36,10 +37,6 @@ import {
|
|
| 36 |
|
| 37 |
type IngestSubTab = 'urls' | 'manifest' | 'files'
|
| 38 |
|
| 39 |
-
interface Props {
|
| 40 |
-
onHome: () => void
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
// ── Feedback helpers ───────────────────────────────────────────────────────
|
| 44 |
|
| 45 |
function ErrorMsg({ message }: { message: string }) {
|
|
@@ -542,7 +539,9 @@ function CorpusDetail({ corpus, onDeleted }: { corpus: Corpus; onDeleted: () =>
|
|
| 542 |
|
| 543 |
// ── Admin (main component) ─────────────────────────────────────────────────
|
| 544 |
|
| 545 |
-
export default function Admin(
|
|
|
|
|
|
|
| 546 |
const [corpora, setCorpora] = useState<Corpus[]>([])
|
| 547 |
const [selectedCorpusId, setSelectedCorpusId] = useState<string | null>(null)
|
| 548 |
const [showCreate, setShowCreate] = useState(false)
|
|
|
|
| 1 |
import { type FormEvent, useEffect, useRef, useState } from 'react'
|
| 2 |
+
import { useNavigate } from 'react-router-dom'
|
| 3 |
import {
|
| 4 |
fetchCorpora,
|
| 5 |
fetchManuscripts,
|
|
|
|
| 37 |
|
| 38 |
type IngestSubTab = 'urls' | 'manifest' | 'files'
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
// ── Feedback helpers ───────────────────────────────────────────────────────
|
| 41 |
|
| 42 |
function ErrorMsg({ message }: { message: string }) {
|
|
|
|
| 539 |
|
| 540 |
// ── Admin (main component) ─────────────────────────────────────────────────
|
| 541 |
|
| 542 |
+
export default function Admin() {
|
| 543 |
+
const navigate = useNavigate()
|
| 544 |
+
const onHome = () => navigate('/')
|
| 545 |
const [corpora, setCorpora] = useState<Corpus[]>([])
|
| 546 |
const [selectedCorpusId, setSelectedCorpusId] = useState<string | null>(null)
|
| 547 |
const [showCreate, setShowCreate] = useState(false)
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
|
| 2 |
import {
|
| 3 |
applyCorrections,
|
| 4 |
getHistory,
|
|
@@ -16,11 +17,6 @@ import {
|
|
| 16 |
RetroBadge,
|
| 17 |
} from '../components/retro'
|
| 18 |
|
| 19 |
-
interface Props {
|
| 20 |
-
pageId: string
|
| 21 |
-
onBack: () => void
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
type Panel = 'transcription' | 'commentary' | 'regions' | 'history'
|
| 25 |
|
| 26 |
const PANEL_LABELS: Record<Panel, string> = {
|
|
@@ -30,7 +26,9 @@ const PANEL_LABELS: Record<Panel, string> = {
|
|
| 30 |
history: 'Historique',
|
| 31 |
}
|
| 32 |
|
| 33 |
-
export default function Editor(
|
|
|
|
|
|
|
| 34 |
const [master, setMaster] = useState<PageMaster | null>(null)
|
| 35 |
const [history, setHistory] = useState<VersionInfo[]>([])
|
| 36 |
const [activePanel, setActivePanel] = useState<Panel>('transcription')
|
|
@@ -144,7 +142,7 @@ export default function Editor({ pageId, onBack }: Props) {
|
|
| 144 |
<RetroWindow title="Erreur" className="w-80">
|
| 145 |
<div className="p-4 text-retro-sm">
|
| 146 |
{error}
|
| 147 |
-
<div className="mt-2"><RetroButton onClick={
|
| 148 |
</div>
|
| 149 |
</RetroWindow>
|
| 150 |
</div>
|
|
@@ -159,7 +157,7 @@ export default function Editor({ pageId, onBack }: Props) {
|
|
| 159 |
{/* ── Menu bar ───────────────────────────────────────────────── */}
|
| 160 |
<RetroMenuBar
|
| 161 |
items={[
|
| 162 |
-
{ label: 'IIIF Studio', onClick:
|
| 163 |
{ label: `Editeur — ${master?.folio_label ?? pageId}` },
|
| 164 |
]}
|
| 165 |
right={
|
|
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from 'react'
|
| 2 |
+
import { useNavigate, useParams } from 'react-router-dom'
|
| 3 |
import {
|
| 4 |
applyCorrections,
|
| 5 |
getHistory,
|
|
|
|
| 17 |
RetroBadge,
|
| 18 |
} from '../components/retro'
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
type Panel = 'transcription' | 'commentary' | 'regions' | 'history'
|
| 21 |
|
| 22 |
const PANEL_LABELS: Record<Panel, string> = {
|
|
|
|
| 26 |
history: 'Historique',
|
| 27 |
}
|
| 28 |
|
| 29 |
+
export default function Editor() {
|
| 30 |
+
const { pageId = '' } = useParams()
|
| 31 |
+
const navigate = useNavigate()
|
| 32 |
const [master, setMaster] = useState<PageMaster | null>(null)
|
| 33 |
const [history, setHistory] = useState<VersionInfo[]>([])
|
| 34 |
const [activePanel, setActivePanel] = useState<Panel>('transcription')
|
|
|
|
| 142 |
<RetroWindow title="Erreur" className="w-80">
|
| 143 |
<div className="p-4 text-retro-sm">
|
| 144 |
{error}
|
| 145 |
+
<div className="mt-2"><RetroButton onClick={() => navigate(-1)}>Retour</RetroButton></div>
|
| 146 |
</div>
|
| 147 |
</RetroWindow>
|
| 148 |
</div>
|
|
|
|
| 157 |
{/* ── Menu bar ───────────────────────────────────────────────── */}
|
| 158 |
<RetroMenuBar
|
| 159 |
items={[
|
| 160 |
+
{ label: 'IIIF Studio', onClick: () => navigate('/') },
|
| 161 |
{ label: `Editeur — ${master?.folio_label ?? pageId}` },
|
| 162 |
]}
|
| 163 |
right={
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { useEffect, useState } from 'react'
|
|
|
|
| 2 |
import SearchBar from '../components/SearchBar.tsx'
|
| 3 |
import { RetroMenuBar, RetroWindow, RetroIcon } from '../components/retro'
|
| 4 |
import {
|
|
@@ -16,13 +17,8 @@ const PROFILE_GLYPHS: Record<string, string> = {
|
|
| 16 |
'modern-handwritten': '\u{270D}',
|
| 17 |
}
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
onOpenPage?: (pageId: string) => void
|
| 22 |
-
onAdmin: () => void
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
| 26 |
const [corpora, setCorpora] = useState<Corpus[]>([])
|
| 27 |
const [loading, setLoading] = useState(true)
|
| 28 |
const [error, setError] = useState<string | null>(null)
|
|
@@ -43,7 +39,7 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 43 |
|
| 44 |
const cached = manuscripts[corpus.id]
|
| 45 |
if (cached) {
|
| 46 |
-
if (cached.length === 1)
|
| 47 |
return
|
| 48 |
}
|
| 49 |
|
|
@@ -52,7 +48,7 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 52 |
try {
|
| 53 |
const ms = await fetchManuscripts(corpus.id)
|
| 54 |
setManuscripts((prev) => ({ ...prev, [corpus.id]: ms }))
|
| 55 |
-
if (ms.length === 1)
|
| 56 |
} catch (e: unknown) {
|
| 57 |
setExpandError(e instanceof Error ? e.message : 'Erreur de chargement')
|
| 58 |
} finally {
|
|
@@ -95,10 +91,10 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 95 |
<RetroMenuBar
|
| 96 |
items={[
|
| 97 |
{ label: 'IIIF Studio' },
|
| 98 |
-
{ label: 'Administration', onClick:
|
| 99 |
]}
|
| 100 |
right={
|
| 101 |
-
<SearchBar onSelectResult={
|
| 102 |
}
|
| 103 |
/>
|
| 104 |
|
|
@@ -176,7 +172,7 @@ export default function Home({ onOpenManuscript, onOpenPage, onAdmin }: Props) {
|
|
| 176 |
<button
|
| 177 |
type="button"
|
| 178 |
key={ms.id}
|
| 179 |
-
onClick={() =>
|
| 180 |
className="
|
| 181 |
w-full text-left px-3 py-[6px]
|
| 182 |
text-retro-sm font-retro
|
|
|
|
| 1 |
import { useEffect, useState } from 'react'
|
| 2 |
+
import { useNavigate } from 'react-router-dom'
|
| 3 |
import SearchBar from '../components/SearchBar.tsx'
|
| 4 |
import { RetroMenuBar, RetroWindow, RetroIcon } from '../components/retro'
|
| 5 |
import {
|
|
|
|
| 17 |
'modern-handwritten': '\u{270D}',
|
| 18 |
}
|
| 19 |
|
| 20 |
+
export default function Home() {
|
| 21 |
+
const navigate = useNavigate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const [corpora, setCorpora] = useState<Corpus[]>([])
|
| 23 |
const [loading, setLoading] = useState(true)
|
| 24 |
const [error, setError] = useState<string | null>(null)
|
|
|
|
| 39 |
|
| 40 |
const cached = manuscripts[corpus.id]
|
| 41 |
if (cached) {
|
| 42 |
+
if (cached.length === 1) navigate(`/reader/${cached[0].id}?profile=${corpus.profile_id}`)
|
| 43 |
return
|
| 44 |
}
|
| 45 |
|
|
|
|
| 48 |
try {
|
| 49 |
const ms = await fetchManuscripts(corpus.id)
|
| 50 |
setManuscripts((prev) => ({ ...prev, [corpus.id]: ms }))
|
| 51 |
+
if (ms.length === 1) navigate(`/reader/${ms[0].id}?profile=${corpus.profile_id}`)
|
| 52 |
} catch (e: unknown) {
|
| 53 |
setExpandError(e instanceof Error ? e.message : 'Erreur de chargement')
|
| 54 |
} finally {
|
|
|
|
| 91 |
<RetroMenuBar
|
| 92 |
items={[
|
| 93 |
{ label: 'IIIF Studio' },
|
| 94 |
+
{ label: 'Administration', onClick: () => navigate('/admin') },
|
| 95 |
]}
|
| 96 |
right={
|
| 97 |
+
<SearchBar onSelectResult={(r) => navigate(`/editor/${r.page_id}`)} />
|
| 98 |
}
|
| 99 |
/>
|
| 100 |
|
|
|
|
| 172 |
<button
|
| 173 |
type="button"
|
| 174 |
key={ms.id}
|
| 175 |
+
onClick={() => navigate(`/reader/${ms.id}?profile=${selectedCorpus.profile_id}`)}
|
| 176 |
className="
|
| 177 |
w-full text-left px-3 py-[6px]
|
| 178 |
text-retro-sm font-retro
|
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import { useCallback, useEffect, useState } from 'react'
|
|
|
|
| 2 |
import type OpenSeadragon from 'openseadragon'
|
| 3 |
import {
|
| 4 |
fetchPages,
|
|
@@ -17,14 +18,11 @@ import TranslationPanel from '../components/TranslationPanel.tsx'
|
|
| 17 |
import CommentaryPanel from '../components/CommentaryPanel.tsx'
|
| 18 |
import { RetroMenuBar, RetroWindow, RetroButton, RetroBadge } from '../components/retro'
|
| 19 |
|
| 20 |
-
|
| 21 |
-
manuscriptId
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Props) {
|
| 28 |
const [pages, setPages] = useState<Page[]>([])
|
| 29 |
const [currentIndex, setCurrentIndex] = useState(0)
|
| 30 |
const [master, setMaster] = useState<PageMaster | null>(null)
|
|
@@ -108,7 +106,7 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
|
|
| 108 |
<div className="p-4 text-retro-sm">
|
| 109 |
Aucune page dans ce manuscrit.
|
| 110 |
<div className="mt-2">
|
| 111 |
-
<RetroButton onClick={
|
| 112 |
</div>
|
| 113 |
</div>
|
| 114 |
</RetroWindow>
|
|
@@ -125,7 +123,7 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
|
|
| 125 |
{/* ── Menu bar ───────────────────────────────────────────────── */}
|
| 126 |
<RetroMenuBar
|
| 127 |
items={[
|
| 128 |
-
{ label: 'IIIF Studio', onClick:
|
| 129 |
{ label: profile?.label ?? profileId },
|
| 130 |
]}
|
| 131 |
right={
|
|
@@ -147,11 +145,9 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
|
|
| 147 |
>
|
| 148 |
Next
|
| 149 |
</RetroButton>
|
| 150 |
-
{
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
</RetroButton>
|
| 154 |
-
)}
|
| 155 |
</div>
|
| 156 |
}
|
| 157 |
/>
|
|
@@ -246,7 +242,8 @@ export default function Reader({ manuscriptId, profileId, onBack, onEdit }: Prop
|
|
| 246 |
<TranslationPanel
|
| 247 |
translation={master.translation}
|
| 248 |
editorial={master.editorial}
|
| 249 |
-
visible={visibleLayers.has('translation_fr')}
|
|
|
|
| 250 |
/>
|
| 251 |
<CommentaryPanel
|
| 252 |
commentary={master.commentary}
|
|
|
|
| 1 |
import { useCallback, useEffect, useState } from 'react'
|
| 2 |
+
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
| 3 |
import type OpenSeadragon from 'openseadragon'
|
| 4 |
import {
|
| 5 |
fetchPages,
|
|
|
|
| 18 |
import CommentaryPanel from '../components/CommentaryPanel.tsx'
|
| 19 |
import { RetroMenuBar, RetroWindow, RetroButton, RetroBadge } from '../components/retro'
|
| 20 |
|
| 21 |
+
export default function Reader() {
|
| 22 |
+
const { manuscriptId = '' } = useParams()
|
| 23 |
+
const [searchParams] = useSearchParams()
|
| 24 |
+
const profileId = searchParams.get('profile') ?? ''
|
| 25 |
+
const navigate = useNavigate()
|
|
|
|
|
|
|
|
|
|
| 26 |
const [pages, setPages] = useState<Page[]>([])
|
| 27 |
const [currentIndex, setCurrentIndex] = useState(0)
|
| 28 |
const [master, setMaster] = useState<PageMaster | null>(null)
|
|
|
|
| 106 |
<div className="p-4 text-retro-sm">
|
| 107 |
Aucune page dans ce manuscrit.
|
| 108 |
<div className="mt-2">
|
| 109 |
+
<RetroButton onClick={() => navigate('/')}>Retour</RetroButton>
|
| 110 |
</div>
|
| 111 |
</div>
|
| 112 |
</RetroWindow>
|
|
|
|
| 123 |
{/* ── Menu bar ───────────────────────────────────────────────── */}
|
| 124 |
<RetroMenuBar
|
| 125 |
items={[
|
| 126 |
+
{ label: 'IIIF Studio', onClick: () => navigate('/') },
|
| 127 |
{ label: profile?.label ?? profileId },
|
| 128 |
]}
|
| 129 |
right={
|
|
|
|
| 145 |
>
|
| 146 |
Next
|
| 147 |
</RetroButton>
|
| 148 |
+
<RetroButton size="sm" onClick={() => navigate(`/editor/${currentPage.id}`)}>
|
| 149 |
+
Editer
|
| 150 |
+
</RetroButton>
|
|
|
|
|
|
|
| 151 |
</div>
|
| 152 |
}
|
| 153 |
/>
|
|
|
|
| 242 |
<TranslationPanel
|
| 243 |
translation={master.translation}
|
| 244 |
editorial={master.editorial}
|
| 245 |
+
visible={visibleLayers.has('translation_fr') || visibleLayers.has('translation_en')}
|
| 246 |
+
activeLayers={profile?.active_layers}
|
| 247 |
/>
|
| 248 |
<CommentaryPanel
|
| 249 |
commentary={master.commentary}
|