Spaces:
Build error
Build error
Claude commited on
fix(frontend): Sprint 2b — dead code, accessibility, Viewer robustness, ApiError
Browse files- Delete unused AdminNav.tsx (dead code)
- Fix RetroTextarea accessibility (useId + htmlFor linkage)
- Fix Viewer.tsx: add open-failed handler, prevent handler accumulation
- Add ApiError class with status code for structured error handling
- Replace fragile msg.includes('404') with ApiError.status check in Reader/Editor
https://claude.ai/code/session_012NCh8yLxMXkRmBYQgHCTik
frontend/src/components/AdminNav.tsx
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
import { RetroButton } from './retro'
|
| 2 |
-
|
| 3 |
-
interface AdminNavProps {
|
| 4 |
-
onClick: () => void
|
| 5 |
-
}
|
| 6 |
-
|
| 7 |
-
export default function AdminNav({ onClick }: AdminNavProps) {
|
| 8 |
-
return (
|
| 9 |
-
<RetroButton size="sm" onClick={onClick}>
|
| 10 |
-
Admin
|
| 11 |
-
</RetroButton>
|
| 12 |
-
)
|
| 13 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/Viewer.tsx
CHANGED
|
@@ -45,6 +45,10 @@ const Viewer: FC<Props> = ({ iiifServiceUrl, fallbackImageUrl, onViewerReady })
|
|
| 45 |
const viewer = viewerRef.current
|
| 46 |
if (!viewer || !source) return
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
if (iiifServiceUrl) {
|
| 49 |
// Zoom tuilé natif — OpenSeadragon fetch info.json et configure les tuiles
|
| 50 |
viewer.open(iiifServiceUrl + '/info.json')
|
|
@@ -56,6 +60,10 @@ const Viewer: FC<Props> = ({ iiifServiceUrl, fallbackImageUrl, onViewerReady })
|
|
| 56 |
viewer.addOnceHandler('open', () => {
|
| 57 |
onViewerReadyRef.current?.(viewer)
|
| 58 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}, [source, iiifServiceUrl])
|
| 60 |
|
| 61 |
return (
|
|
|
|
| 45 |
const viewer = viewerRef.current
|
| 46 |
if (!viewer || !source) return
|
| 47 |
|
| 48 |
+
// Remove previous handlers to avoid accumulation on rapid page changes
|
| 49 |
+
viewer.removeAllHandlers('open')
|
| 50 |
+
viewer.removeAllHandlers('open-failed')
|
| 51 |
+
|
| 52 |
if (iiifServiceUrl) {
|
| 53 |
// Zoom tuilé natif — OpenSeadragon fetch info.json et configure les tuiles
|
| 54 |
viewer.open(iiifServiceUrl + '/info.json')
|
|
|
|
| 60 |
viewer.addOnceHandler('open', () => {
|
| 61 |
onViewerReadyRef.current?.(viewer)
|
| 62 |
})
|
| 63 |
+
|
| 64 |
+
viewer.addOnceHandler('open-failed', () => {
|
| 65 |
+
console.warn('[Viewer] Failed to open image source:', source)
|
| 66 |
+
})
|
| 67 |
}, [source, iiifServiceUrl])
|
| 68 |
|
| 69 |
return (
|
frontend/src/components/retro/RetroTextarea.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
|
| 3 |
interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
| 4 |
/** Optional label rendered above the textarea */
|
|
@@ -6,14 +6,16 @@ interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
| 6 |
}
|
| 7 |
|
| 8 |
export default function RetroTextarea({ 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 |
<textarea
|
|
|
|
| 17 |
className={`
|
| 18 |
px-2 py-[3px]
|
| 19 |
text-retro-sm font-retro
|
|
|
|
| 1 |
+
import { useId, type TextareaHTMLAttributes } from 'react'
|
| 2 |
|
| 3 |
interface Props extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
| 4 |
/** Optional label rendered above the textarea */
|
|
|
|
| 6 |
}
|
| 7 |
|
| 8 |
export default function RetroTextarea({ label, className = '', ...rest }: Props) {
|
| 9 |
+
const id = useId()
|
| 10 |
return (
|
| 11 |
<div className="flex flex-col gap-[2px]">
|
| 12 |
{label && (
|
| 13 |
+
<label htmlFor={id} className="text-retro-xs font-retro font-medium text-retro-black">
|
| 14 |
{label}
|
| 15 |
</label>
|
| 16 |
)}
|
| 17 |
<textarea
|
| 18 |
+
id={id}
|
| 19 |
className={`
|
| 20 |
px-2 py-[3px]
|
| 21 |
text-retro-sm font-retro
|
frontend/src/lib/api.ts
CHANGED
|
@@ -188,6 +188,17 @@ export interface CorpusProfile {
|
|
| 188 |
export_config: { mets: boolean; alto: boolean; tei: boolean }
|
| 189 |
}
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
// ── Fetch helpers ─────────────────────────────────────────────────────────────
|
| 192 |
|
| 193 |
/**
|
|
@@ -214,7 +225,7 @@ function extractDetail(payload: unknown, fallback: string): string {
|
|
| 214 |
|
| 215 |
async function get<T>(path: string): Promise<T> {
|
| 216 |
const resp = await fetch(`${BASE_URL}${path}`)
|
| 217 |
-
if (!resp.ok) throw new
|
| 218 |
return resp.json() as Promise<T>
|
| 219 |
}
|
| 220 |
|
|
|
|
| 188 |
export_config: { mets: boolean; alto: boolean; tei: boolean }
|
| 189 |
}
|
| 190 |
|
| 191 |
+
// ── Errors ────────────────────────────────────────────────────────────────────
|
| 192 |
+
|
| 193 |
+
export class ApiError extends Error {
|
| 194 |
+
status: number
|
| 195 |
+
constructor(status: number, message: string) {
|
| 196 |
+
super(message)
|
| 197 |
+
this.name = 'ApiError'
|
| 198 |
+
this.status = status
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
// ── Fetch helpers ─────────────────────────────────────────────────────────────
|
| 203 |
|
| 204 |
/**
|
|
|
|
| 225 |
|
| 226 |
async function get<T>(path: string): Promise<T> {
|
| 227 |
const resp = await fetch(`${BASE_URL}${path}`)
|
| 228 |
+
if (!resp.ok) throw new ApiError(resp.status, `HTTP ${resp.status} — ${path}`)
|
| 229 |
return resp.json() as Promise<T>
|
| 230 |
}
|
| 231 |
|
frontend/src/pages/Editor.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
| 4 |
applyCorrections,
|
| 5 |
getHistory,
|
| 6 |
fetchMasterJson,
|
|
|
|
| 7 |
type PageMaster,
|
| 8 |
type VersionInfo,
|
| 9 |
} from '../lib/api.ts'
|
|
@@ -67,11 +68,11 @@ export default function Editor() {
|
|
| 67 |
const ext = m.extensions as { region_validations?: Record<string, string> } | undefined
|
| 68 |
setRegionValidations(ext?.region_validations ?? {})
|
| 69 |
} catch (e: unknown) {
|
| 70 |
-
|
| 71 |
-
if (msg.includes('404')) {
|
| 72 |
setError('Cette page n\'a pas encore ete analysee par l\'IA. Lancez le pipeline depuis Administration.')
|
| 73 |
} else {
|
| 74 |
-
|
|
|
|
| 75 |
}
|
| 76 |
} finally {
|
| 77 |
setLoading(false)
|
|
|
|
| 4 |
applyCorrections,
|
| 5 |
getHistory,
|
| 6 |
fetchMasterJson,
|
| 7 |
+
ApiError,
|
| 8 |
type PageMaster,
|
| 9 |
type VersionInfo,
|
| 10 |
} from '../lib/api.ts'
|
|
|
|
| 68 |
const ext = m.extensions as { region_validations?: Record<string, string> } | undefined
|
| 69 |
setRegionValidations(ext?.region_validations ?? {})
|
| 70 |
} catch (e: unknown) {
|
| 71 |
+
if (e instanceof ApiError && e.status === 404) {
|
|
|
|
| 72 |
setError('Cette page n\'a pas encore ete analysee par l\'IA. Lancez le pipeline depuis Administration.')
|
| 73 |
} else {
|
| 74 |
+
const msg = e instanceof Error ? e.message : ''
|
| 75 |
+
setError(msg || 'Erreur de chargement')
|
| 76 |
}
|
| 77 |
} finally {
|
| 78 |
setLoading(false)
|
frontend/src/pages/Reader.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
| 5 |
fetchPages,
|
| 6 |
fetchMasterJson,
|
| 7 |
fetchProfile,
|
|
|
|
| 8 |
type Page,
|
| 9 |
type PageMaster,
|
| 10 |
type CorpusProfile,
|
|
@@ -63,10 +64,10 @@ export default function Reader() {
|
|
| 63 |
.then(setMaster)
|
| 64 |
.catch((e: unknown) => {
|
| 65 |
// 404 = page non analysée (normal), autres erreurs = problème réseau
|
| 66 |
-
|
| 67 |
-
if (msg.includes('404')) {
|
| 68 |
setMaster(null)
|
| 69 |
} else {
|
|
|
|
| 70 |
setMasterError(msg || 'Erreur de chargement')
|
| 71 |
}
|
| 72 |
})
|
|
|
|
| 5 |
fetchPages,
|
| 6 |
fetchMasterJson,
|
| 7 |
fetchProfile,
|
| 8 |
+
ApiError,
|
| 9 |
type Page,
|
| 10 |
type PageMaster,
|
| 11 |
type CorpusProfile,
|
|
|
|
| 64 |
.then(setMaster)
|
| 65 |
.catch((e: unknown) => {
|
| 66 |
// 404 = page non analysée (normal), autres erreurs = problème réseau
|
| 67 |
+
if (e instanceof ApiError && e.status === 404) {
|
|
|
|
| 68 |
setMaster(null)
|
| 69 |
} else {
|
| 70 |
+
const msg = e instanceof Error ? e.message : ''
|
| 71 |
setMasterError(msg || 'Erreur de chargement')
|
| 72 |
}
|
| 73 |
})
|