Repository Public MIT
1import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";2import { ChevronLeft, ChevronRight, Download, X } from "lucide-react";3import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";4import { cn } from "@/lib/utils";56export interface LightboxItem {7 src: string;8 alt?: string;9 filename?: string;10}1112interface LightboxContextValue {13 open: (items: LightboxItem[], index?: number) => void;14}1516const LightboxContext = createContext<LightboxContextValue>({ open: () => undefined });1718export function useLightbox() {19 return useContext(LightboxContext);20}2122export function LightboxProvider({ children }: { children: ReactNode }) {23 const [items, setItems] = useState<LightboxItem[]>([]);24 const [index, setIndex] = useState(0);25 const [isOpen, setIsOpen] = useState(false);2627 const open = useCallback((list: LightboxItem[], i = 0) => {28 if (!list.length) return;29 setItems(list);30 setIndex(Math.min(Math.max(i, 0), list.length - 1));31 setIsOpen(true);32 }, []);3334 const prev = useCallback(() => setIndex((i) => (i - 1 + items.length) % items.length), [items.length]);35 const next = useCallback(() => setIndex((i) => (i + 1) % items.length), [items.length]);3637 useEffect(() => {38 if (!isOpen) return;39 const onKey = (e: KeyboardEvent) => {40 if (e.key === "ArrowLeft") prev();41 if (e.key === "ArrowRight") next();42 };43 window.addEventListener("keydown", onKey);44 return () => window.removeEventListener("keydown", onKey);45 }, [isOpen, prev, next]);4647 const value = useMemo(() => ({ open }), [open]);48 const current = items[index];4950 return (51 <LightboxContext.Provider value={value}>52 {children}53 <Dialog open={isOpen} onOpenChange={setIsOpen}>54 <DialogContent className="max-w-[min(96vw,1400px)] w-full p-0 overflow-hidden rounded-2xl border-border bg-background [&>button]:hidden">55 <DialogTitle className="sr-only">{current?.alt || "Figure"}</DialogTitle>56 {current && (57 <div className="flex flex-col">58 <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">59 <span className="min-w-0 flex-1 truncate text-sm font-medium">{current.alt || "Figure"}</span>60 {items.length > 1 && <span className="text-[12px] tnum text-muted-foreground">{index + 1} / {items.length}</span>}61 <a62 href={current.src}63 download={current.filename || `vquant-figure-${index + 1}.png`}64 className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-border px-2.5 text-[12px] hover:bg-accent"65 >66 <Download className="h-3.5 w-3.5" /> Télécharger67 </a>68 <button type="button" onClick={() => setIsOpen(false)} className="grid h-8 w-8 place-items-center rounded-lg hover:bg-accent" aria-label="Fermer">69 <X className="h-4 w-4" />70 </button>71 </div>72 <div className="relative grid place-items-center bg-white dark:bg-[hsl(228_20%_9%)] p-2 md:p-4 min-h-[40vh] max-h-[82vh]">73 <img src={current.src} alt={current.alt || "Figure"} className="max-h-[78vh] w-auto max-w-full object-contain rounded-lg" />74 {items.length > 1 && (75 <>76 <button type="button" onClick={prev} className={cn("absolute left-3 top-1/2 -translate-y-1/2 grid h-10 w-10 place-items-center rounded-full bg-background/90 border border-border shadow hover:bg-accent")} aria-label="Précédente">77 <ChevronLeft className="h-5 w-5" />78 </button>79 <button type="button" onClick={next} className={cn("absolute right-3 top-1/2 -translate-y-1/2 grid h-10 w-10 place-items-center rounded-full bg-background/90 border border-border shadow hover:bg-accent")} aria-label="Suivante">80 <ChevronRight className="h-5 w-5" />81 </button>82 </>83 )}84 </div>85 </div>86 )}87 </DialogContent>88 </Dialog>89 </LightboxContext.Provider>90 );91}92