1import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from "react";
2import {createPortal} from "react-dom";
3import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
4import {faClose} from "@fortawesome/free-solid-svg-icons";
5
6const AlertContext = createContext(null);
7
8export const useAlert = () => {
9 const context = useContext(AlertContext);
10 if (!context) throw new Error("useAlert must be used within AlertProvider");
11 return context;
12};
13
14export const AlertProvider = ({children}) => {
15 const [alerts, setAlerts] = useState([]);
16 const alertIdRef = useRef(0);
17 const resolversRef = useRef(new Map());
18
19 const showAlert = useCallback((config) => {
20 return new Promise((resolve) => {
21 const id = ++alertIdRef.current;
22 resolversRef.current.set(id, resolve);
23 setAlerts(prev => [...prev, {...config, id}]);
24 });
25 }, []);
26
27 const closeAlert = useCallback((id, result = null) => {
28 const resolver = resolversRef.current.get(id);
29 if (resolver) {
30 resolver(result);
31 resolversRef.current.delete(id);
32 }
33 setAlerts(prev => prev.filter(a => a.id !== id));
34 }, []);
35
36 const openAlert = useCallback((title, description, options = {}) =>
37 showAlert({
38 type: "alert",
39 title,
40 description,
41 buttonText: options.buttonText || "OK", ...options
42 }), [showAlert]);
43
44 const openInput = useCallback((title, options = {}) =>
45 showAlert({type: "input", title, ...options}), [showAlert]);
46
47 const openSelect = useCallback((title, selectOptions, options = {}) =>
48 showAlert({
49 type: "select",
50 title,
51 options: selectOptions,
52 value: options.value || Object.keys(selectOptions)[0], ...options
53 }), [showAlert]);
54
55 const openConfirm = useCallback((title, description, options = {}) =>
56 showAlert({
57 type: "confirm",
58 title,
59 description,
60 buttonText: options.buttonText || "OK", ...options
61 }), [showAlert]);
62
63 const contextValue = useMemo(() => ({
64 openAlert, openInput, openSelect, openConfirm
65 }), [openAlert, openInput, openSelect, openConfirm]);
66
67 return (
68 <AlertContext.Provider value={contextValue}>
69 {children}
70 {alerts.map(alert => (
71 <AlertRenderer key={alert.id} alert={alert} onClose={(result) => closeAlert(alert.id, result)}/>
72 ))}
73 </AlertContext.Provider>
74 );
75};
76
77const AlertRenderer = ({alert, onClose}) => {
78 const areaRef = useRef();
79 const dialogRef = useRef();
80 const [inputValue, setInputValue] = useState(alert.value || "");
81 const [inputError, setInputError] = useState(false);
82 const closeResultRef = useRef(null);
83 const isClosingRef = useRef(false);
84
85 const close = useCallback((result = null) => {
86 if (alert.disableClose && result === null) return;
87 if (isClosingRef.current) return;
88 isClosingRef.current = true;
89 closeResultRef.current = result;
90 areaRef.current?.classList.add("dialog-area-hidden");
91 dialogRef.current?.classList.add("dialog-hidden");
92 }, [alert.disableClose]);
93
94 const handleAnimationEnd = (e) => {
95 if (e.animationName === "fadeOut") onClose(closeResultRef.current);
96 };
97
98 const handleBackdropClick = (e) => {
99 if (e.target === areaRef.current) close();
100 };
101
102 const handleKeyDown = useCallback((e) => {
103 if (e.key === "Escape" && !alert.disableClose) {
104 e.preventDefault();
105 close();
106 }
107 if (e.key === "Enter") {
108 e.preventDefault();
109 handleSubmit();
110 }
111 }, [alert, inputValue]);
112
113 useEffect(() => {
114 document.addEventListener("keydown", handleKeyDown);
115 return () => document.removeEventListener("keydown", handleKeyDown);
116 }, [handleKeyDown]);
117
118 const handleSubmit = () => {
119 if (alert.type === "input" && alert.required && !inputValue) {
120 setInputError(true);
121 return;
122 }
123 const result = alert.type === "input" || alert.type === "select" ? inputValue : true;
124 alert.onSuccess?.(result);
125 close(result);
126 };
127
128 return createPortal(
129 <div className="dialog-area" ref={areaRef} onClick={handleBackdropClick}>
130 <div className="dialog" ref={dialogRef} onAnimationEnd={handleAnimationEnd}>
131 <div className="dialog-header">
132 <h4 className="dialog-text">{alert.title}</h4>
133 {!alert.disableClose &&
134 <FontAwesomeIcon icon={faClose} className="dialog-text dialog-icon" onClick={() => close()}/>}
135 </div>
136 <div className="dialog-main">
137 {alert.description && <p className="dialog-description">{alert.description}</p>}
138 {alert.type === "input" && (
139 <input className={`dialog-input${inputError ? " input-error" : ""}`}
140 type={alert.inputType || "text"}
141 placeholder={alert.placeholder} value={inputValue} autoFocus
142 onChange={(e) => {
143 setInputValue(e.target.value);
144 setInputError(false);
145 }}/>
146 )}
147 {alert.type === "select" && (
148 <select className="dialog-input" value={inputValue}
149 onChange={(e) => setInputValue(e.target.value)}>
150 {Object.entries(alert.options || {}).map(([key, label]) => (
151 <option key={key} value={key}>{label}</option>
152 ))}
153 </select>
154 )}
155 </div>
156 <div className="dialog-buttons">
157 {alert.clearButton && (
158 <button className="dialog-btn dialog-secondary" onClick={() => {
159 alert.onClear?.();
160 close();
161 }}>
162 {alert.clearButton}
163 </button>
164 )}
165 {alert.type === "confirm" && (
166 <button className="dialog-btn dialog-secondary" onClick={() => close(false)}>
167 {alert.cancelText || "Cancel"}
168 </button>
169 )}
170 <button className={`dialog-btn${alert.danger ? " dialog-danger" : ""}`} onClick={handleSubmit}>
171 {alert.buttonText || "OK"}
172 </button>
173 </div>
174 </div>
175 </div>,
176 document.body
177 );
178};
179