"use client"; import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; import { ExternalLinkIcon, RefreshCwIcon, RotateCcwIcon, RotateCwIcon, XIcon, ZoomInIcon, ZoomOutIcon, } from "lucide-react"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import type { ReactZoomPanPinchContentRef } from "react-zoom-pan-pinch"; import { TransformComponent, TransformWrapper, } from "react-zoom-pan-pinch"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, DialogClose, DialogOverlay, DialogPortal, DialogTitle, } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; import { translateCurrentMessage } from "@/i18n/messages"; import { useI18n } from "@/i18n/provider"; type ImageLightboxItem = { src: string; alt?: string; }; type ImageLightboxContextValue = { open: (src: string, alt?: string) => void; close: () => void; }; const ImageLightboxContext = createContext( null, ); export function useImageLightbox(): ImageLightboxContextValue { const ctx = useContext(ImageLightboxContext); if (!ctx) { throw new Error(translateCurrentMessage("lightbox.providerError")); } return ctx; } /** Returns null when no provider is mounted, which keeps adoption incremental. */ export function useImageLightboxOptional(): ImageLightboxContextValue | null { return useContext(ImageLightboxContext); } export type ImageLightboxProps = { open: boolean; onOpenChange: (open: boolean) => void; src: string | null; alt?: string; }; function canOpenInNewTab(url: string): boolean { if (!url) { return false; } if (url.startsWith("/")) { return true; } try { const parsed = new URL(url); return ( parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "blob:" ); } catch { return false; } } function LightboxImageBody({ src, alt, pinchRef, rotationDeg, }: { src: string; alt?: string; pinchRef: React.RefObject; rotationDeg: number; }) { const t = useI18n(); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const showOpenTab = canOpenInNewTab(src); useEffect(() => { requestAnimationFrame(() => { pinchRef.current?.centerView(1, 0); }); }, [rotationDeg, pinchRef]); return (
{loading && !error ? (
) : null} {error ? (

{t("lightbox.loadFailed")}

{showOpenTab ? ( {t("lightbox.openInNewTab")} ) : null}
) : ( {/* eslint-disable-next-line @next/next/no-img-element -- External URLs and arbitrary image sizes need native preview. */} {alt { setLoading(false); setError(false); requestAnimationFrame(() => { pinchRef.current?.centerView(1, 0); }); }} onError={() => { setLoading(false); setError(true); }} /> )}
); } /** Mounted by src key so rotation resets when switching images. */ function ImageLightboxDialogContent({ src, alt, }: { src: string; alt?: string; }) { const t = useI18n(); const pinchRef = useRef(null); const [rotationDeg, setRotationDeg] = useState(0); const showOpenTab = canOpenInNewTab(src); const titleText = alt?.trim() || t("lightbox.imagePreview"); const rotateLeft = useCallback(() => { setRotationDeg((d) => (d - 90 + 360) % 360); }, []); const rotateRight = useCallback(() => { setRotationDeg((d) => (d + 90) % 360); }, []); return (
{titleText}
{showOpenTab ? ( ) : null} } > {t("lightbox.close")}

{t("lightbox.help")}

); } export function ImageLightboxView({ open, onOpenChange, src, alt, }: ImageLightboxProps) { return ( {src ? ( ) : null} ); } export function ImageLightboxProvider({ children }: { children: ReactNode }) { const [state, setState] = useState(null); const open = useCallback((src: string, alt?: string) => { const trimmed = src?.trim(); if (!trimmed) { return; } setState({ src: trimmed, alt }); }, []); const close = useCallback(() => { setState(null); }, []); const contextValue = useMemo( () => ({ open, close, }), [open, close], ); return ( {children} { if (!next) { setState(null); } }} src={state?.src ?? null} alt={state?.alt} /> ); }