| // src/context/ModalContext.tsx | |
| import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react"; | |
| import { AnimatePresence, motion } from "framer-motion"; | |
| // ============================================ | |
| // Types | |
| // ============================================ | |
| export interface ModalConfig { | |
| id: string; | |
| title?: string; | |
| content: ReactNode; | |
| size?: "sm" | "md" | "lg" | "xl" | "full"; | |
| onClose?: () => void; | |
| closeOnBackdrop?: boolean; | |
| closeOnEsc?: boolean; | |
| showCloseButton?: boolean; | |
| className?: string; | |
| } | |
| interface ModalContextValue { | |
| modals: ModalConfig[]; | |
| openModal: (config: Omit<ModalConfig, "id">) => string; | |
| closeModal: (id: string) => void; | |
| closeTopModal: () => void; | |
| closeAllModals: () => void; | |
| updateModal: (id: string, updates: Partial<ModalConfig>) => void; | |
| } | |
| // ============================================ | |
| // Constants | |
| // ============================================ | |
| const sizeClasses = { | |
| sm: "max-w-md", | |
| md: "max-w-lg", | |
| lg: "max-w-2xl", | |
| xl: "max-w-4xl", | |
| full: "max-w-[90vw] max-h-[90vh]", | |
| }; | |
| // ============================================ | |
| // Context | |
| // ============================================ | |
| const ModalContext = createContext<ModalContextValue | null>(null); | |
| export function ModalProvider({ children }: { children: ReactNode }) { | |
| const [modals, setModals] = useState<ModalConfig[]>([]); | |
| // ============================================ | |
| // Open Modal | |
| // ============================================ | |
| const openModal = useCallback((config: Omit<ModalConfig, "id">): string => { | |
| const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| const newModal: ModalConfig = { | |
| ...config, | |
| id, | |
| size: config.size || "md", | |
| closeOnBackdrop: config.closeOnBackdrop ?? true, | |
| closeOnEsc: config.closeOnEsc ?? true, | |
| showCloseButton: config.showCloseButton ?? true, | |
| }; | |
| setModals(prev => [...prev, newModal]); | |
| return id; | |
| }, []); | |
| // ============================================ | |
| // Close Modal | |
| // ============================================ | |
| const closeModal = useCallback((id: string) => { | |
| const modal = modals.find(m => m.id === id); | |
| if (modal?.onClose) modal.onClose(); | |
| setModals(prev => prev.filter(m => m.id !== id)); | |
| }, [modals]); | |
| // ============================================ | |
| // Close Top Modal | |
| // ============================================ | |
| const closeTopModal = useCallback(() => { | |
| if (modals.length > 0) { | |
| const top = modals[modals.length - 1]; | |
| closeModal(top.id); | |
| } | |
| }, [modals, closeModal]); | |
| // ============================================ | |
| // Close All Modals | |
| // ============================================ | |
| const closeAllModals = useCallback(() => { | |
| modals.forEach(modal => modal.onClose?.()); | |
| setModals([]); | |
| }, [modals]); | |
| // ============================================ | |
| // Update Modal | |
| // ============================================ | |
| const updateModal = useCallback((id: string, updates: Partial<ModalConfig>) => { | |
| setModals(prev => | |
| prev.map(m => (m.id === id ? { ...m, ...updates } : m)) | |
| ); | |
| }, []); | |
| // ============================================ | |
| // ESC Key Handler | |
| // ============================================ | |
| useEffect(() => { | |
| const handleKeyDown = (e: KeyboardEvent) => { | |
| if (e.key === "Escape" && modals.length > 0) { | |
| const topModal = modals[modals.length - 1]; | |
| if (topModal.closeOnEsc) { | |
| closeTopModal(); | |
| } | |
| } | |
| }; | |
| window.addEventListener("keydown", handleKeyDown); | |
| return () => window.removeEventListener("keydown", handleKeyDown); | |
| }, [modals, closeTopModal]); | |
| // ============================================ | |
| // Value | |
| // ============================================ | |
| const value: ModalContextValue = { | |
| modals, | |
| openModal, | |
| closeModal, | |
| closeTopModal, | |
| closeAllModals, | |
| updateModal, | |
| }; | |
| return ( | |
| <ModalContext.Provider value={value}> | |
| {children} | |
| <AnimatePresence> | |
| {modals.map((modal, index) => ( | |
| <div | |
| key={modal.id} | |
| className="fixed inset-0 z-[100] flex items-center justify-center p-4" | |
| style={{ zIndex: 100 + index }} | |
| > | |
| {/* Backdrop */} | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| onClick={() => modal.closeOnBackdrop && closeModal(modal.id)} | |
| className="absolute inset-0 bg-black/50" | |
| /> | |
| {/* Modal content */} | |
| <motion.div | |
| initial={{ scale: 0.95, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| exit={{ scale: 0.95, opacity: 0 }} | |
| className={`relative bg-white dark:bg-gray-800 rounded-2xl shadow-xl w-full ${sizeClasses[modal.size || "md"]} ${modal.className || ""}`} | |
| > | |
| {modal.showCloseButton && ( | |
| <button | |
| onClick={() => closeModal(modal.id)} | |
| className="absolute top-4 right-4 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition" | |
| aria-label="Close modal" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| )} | |
| {modal.title && ( | |
| <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700"> | |
| <h2 className="text-xl font-semibold">{modal.title}</h2> | |
| </div> | |
| )} | |
| <div className="p-6">{modal.content}</div> | |
| </motion.div> | |
| </div> | |
| ))} | |
| </AnimatePresence> | |
| </ModalContext.Provider> | |
| ); | |
| } | |
| export const useModal = () => { | |
| const ctx = useContext(ModalContext); | |
| if (!ctx) throw new Error("useModal must be used within ModalProvider"); | |
| return ctx; | |
| }; |
Xet Storage Details
- Size:
- 6.17 kB
- Xet hash:
- a68ddc0bf0e758cdc52be35c235d9a91acb4bfceb9520c9f79a4193710d13578
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.