tahamajs/IE / Projects /front /src /context /ModalContext.tsx
tahamajs's picture
download
raw
6.17 kB
// 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.