Spaces:
Build error
Build error
Claude
test(frontend): Sprint 5 — Vitest setup, 14 component/API tests, Viewer memo
cf9842a unverified | import { useEffect, useMemo, useRef, type FC } from 'react' | |
| import OpenSeadragon from 'openseadragon' | |
| import { RetroButton } from './retro' | |
| interface Props { | |
| /** URL du IIIF Image Service (zoom tuilé natif) */ | |
| iiifServiceUrl?: string | null | |
| /** URL image statique (fallback si pas de service IIIF) */ | |
| fallbackImageUrl?: string | null | |
| onViewerReady?: (viewer: OpenSeadragon.Viewer) => void | |
| } | |
| const Viewer: FC<Props> = ({ iiifServiceUrl, fallbackImageUrl, onViewerReady }) => { | |
| const containerRef = useRef<HTMLDivElement>(null) | |
| const viewerRef = useRef<OpenSeadragon.Viewer | null>(null) | |
| const onViewerReadyRef = useRef(onViewerReady) | |
| onViewerReadyRef.current = onViewerReady | |
| useEffect(() => { | |
| if (!containerRef.current) return | |
| const viewer = OpenSeadragon({ | |
| element: containerRef.current, | |
| showNavigationControl: false, | |
| gestureSettingsMouse: { clickToZoom: false, scrollToZoom: true, dragToPan: true }, | |
| gestureSettingsTouch: { scrollToZoom: true, dragToPan: true, pinchToZoom: true }, | |
| animationTime: 0.3, | |
| minZoomLevel: 0.1, | |
| maxZoomLevel: 20, | |
| crossOriginPolicy: 'Anonymous', | |
| }) | |
| viewerRef.current = viewer | |
| return () => { | |
| viewer.destroy() | |
| viewerRef.current = null | |
| } | |
| }, []) | |
| // Source à ouvrir : préférer le service IIIF (zoom tuilé), sinon image statique | |
| const source = useMemo( | |
| () => iiifServiceUrl || fallbackImageUrl || '', | |
| [iiifServiceUrl, fallbackImageUrl] | |
| ) | |
| useEffect(() => { | |
| const viewer = viewerRef.current | |
| if (!viewer || !source) return | |
| // Remove previous handlers to avoid accumulation on rapid page changes | |
| viewer.removeAllHandlers('open') | |
| viewer.removeAllHandlers('open-failed') | |
| if (iiifServiceUrl) { | |
| // Zoom tuilé natif — OpenSeadragon fetch info.json et configure les tuiles | |
| viewer.open(iiifServiceUrl + '/info.json') | |
| } else { | |
| // Image statique simple (pas de zoom tuilé) | |
| viewer.open({ type: 'image', url: source }) | |
| } | |
| viewer.addOnceHandler('open', () => { | |
| onViewerReadyRef.current?.(viewer) | |
| }) | |
| viewer.addOnceHandler('open-failed', () => { | |
| console.warn('[Viewer] Failed to open image source:', source) | |
| }) | |
| }, [source, iiifServiceUrl]) | |
| return ( | |
| <div className="relative w-full h-full bg-retro-black"> | |
| <div ref={containerRef} className="w-full h-full" /> | |
| <div className="absolute bottom-2 right-2 flex gap-[2px]"> | |
| <RetroButton | |
| size="sm" | |
| onClick={() => viewerRef.current?.viewport.zoomBy(1.5)} | |
| title="Zoom +" | |
| > | |
| + | |
| </RetroButton> | |
| <RetroButton | |
| size="sm" | |
| onClick={() => viewerRef.current?.viewport.zoomBy(0.67)} | |
| title="Zoom -" | |
| > | |
| - | |
| </RetroButton> | |
| <RetroButton | |
| size="sm" | |
| onClick={() => viewerRef.current?.viewport.goHome()} | |
| title="Reset" | |
| > | |
| o | |
| </RetroButton> | |
| <RetroButton | |
| size="sm" | |
| onClick={() => viewerRef.current?.setFullScreen(true)} | |
| title="Plein ecran" | |
| > | |
| [] | |
| </RetroButton> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default Viewer | |