Claude commited on
Commit
17c4592
·
unverified ·
1 Parent(s): 5d9b495

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 type { TextareaHTMLAttributes } from 'react'
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 Error(`HTTP ${resp.status} — ${path}`)
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
- const msg = (e as Error).message ?? ''
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
- setError(msg)
 
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
- const msg = e instanceof Error ? e.message : ''
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
  })