PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / components / ui / modal.tsx

modal.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/components/ui/modal.tsx

185 lines 5.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { ReactNode, useEffect } from "react";
2 import { createPortal } from "react-dom";
3 import { X } from "lucide-react";
4
5 type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
6
7 const sizeClasses: Record<ModalSize, string> = {
8 sm: "max-w-md",
9 md: "max-w-xl",
10 lg: "max-w-2xl",
11 xl: "max-w-4xl",
12 full: "max-w-6xl",
13 };
14
15 interface ModalProps {
16 isOpen: boolean;
17 onClose: () => void;
18 title?: ReactNode;
19 description?: ReactNode;
20 children: ReactNode;
21 footer?: ReactNode;
22 size?: ModalSize;
23 maxWidthClassName?: string;
24 panelClassName?: string;
25 bodyClassName?: string;
26 showCloseButton?: boolean;
27 closeOnOverlayClick?: boolean;
28 loading?: boolean;
29 loadingSkeleton?: ReactNode;
30 error?: string | null;
31 errorComponent?: ReactNode;
32 hideHeader?: boolean;
33 hideFooter?: boolean;
34 customZIndex?: number;
35 /** Replaces default `max-h-[70vh] overflow-y-auto custom-scrollbar` on the body wrapper (e.g. full-height previews). */
36 bodyScrollClassName?: string;
37 }
38
39 export const Modal: React.FC<ModalProps> = ({
40 isOpen,
41 onClose,
42 title,
43 description,
44 children,
45 footer,
46 size = "xl",
47 maxWidthClassName,
48 panelClassName = "",
49 bodyClassName,
50 showCloseButton = true,
51 closeOnOverlayClick = true,
52 loading = false,
53 loadingSkeleton,
54 error,
55 errorComponent,
56 hideHeader = false,
57 hideFooter = false,
58 customZIndex,
59 bodyScrollClassName,
60 }) => {
61 useEffect(() => {
62 if (!isOpen) {
63 return;
64 }
65
66 const originalOverflow = document.body.style.overflow;
67 document.body.style.overflow = "hidden";
68
69 return () => {
70 document.body.style.overflow = originalOverflow;
71 };
72 }, [isOpen]);
73
74 if (!isOpen) {
75 return null;
76 }
77
78 const panelWidthClass = maxWidthClassName || sizeClasses[size];
79 const bodyClasses = bodyClassName ?? "px-6 py-5";
80 const bodyScrollClasses =
81 bodyScrollClassName ?? "max-h-[70vh] overflow-y-auto custom-scrollbar";
82 const zIndex = customZIndex || 999999;
83
84 // Default loading skeleton
85 const defaultLoadingSkeleton = (
86 <div className="space-y-4 animate-pulse">
87 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
88 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
89 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-5/6"></div>
90 <div className="space-y-2 pt-4">
91 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded"></div>
92 <div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-4/5"></div>
93 </div>
94 </div>
95 );
96
97 // Default error component
98 const defaultErrorComponent = error ? (
99 <div className="text-center py-8">
100 <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/20 mb-4">
101 <svg
102 className="w-6 h-6 text-red-600 dark:text-red-400"
103 fill="none"
104 viewBox="0 0 24 24"
105 stroke="currentColor"
106 >
107 <path
108 strokeLinecap="round"
109 strokeLinejoin="round"
110 strokeWidth={2}
111 d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
112 />
113 </svg>
114 </div>
115 <p className="text-red-600 dark:text-red-400 font-medium mb-2">Error</p>
116 <p className="text-gray-600 dark:text-gray-400 text-sm">{error}</p>
117 </div>
118 ) : null;
119
120 return createPortal(
121 <div
122 className="fixed inset-0 flex items-center justify-center px-4"
123 style={{ zIndex }}
124 >
125 <div
126 className="absolute inset-0 bg-black/60 backdrop-blur-sm"
127 onClick={() => {
128 if (closeOnOverlayClick && !loading) {
129 onClose();
130 }
131 }}
132 />
133 <div
134 className={`relative w-full ${panelWidthClass} bg-white dark:bg-gray-900 rounded-2xl shadow-2xl border border-gray-100 dark:border-gray-800 overflow-hidden animate-in fade-in zoom-in duration-150 yatra-model-ui ${panelClassName}`}
135 style={{ zIndex: zIndex + 1 }}
136 >
137 {!hideHeader && (title || showCloseButton) && (
138 <div className="flex items-center justify-between px-6 py-4 border-b border-gray-100 dark:border-gray-800">
139 <div>
140 {title && (
141 <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
142 {title}
143 </h2>
144 )}
145 {description && (
146 <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
147 {description}
148 </p>
149 )}
150 </div>
151 {showCloseButton && (
152 <button
153 type="button"
154 onClick={onClose}
155 disabled={loading}
156 className="p-2 rounded-full text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
157 aria-label="Close modal"
158 >
159 <X className="w-5 h-5" />
160 </button>
161 )}
162 </div>
163 )}
164
165 <div className={`${bodyClasses} ${bodyScrollClasses}`}>
166 <div className="w-full">
167 {loading
168 ? loadingSkeleton || defaultLoadingSkeleton
169 : error
170 ? errorComponent || defaultErrorComponent
171 : children}
172 </div>
173 </div>
174
175 {!hideFooter && footer && (
176 <div className="px-6 py-4 border-t border-gray-100 dark:border-gray-800 bg-gray-50/70 dark:bg-gray-900/50">
177 {footer}
178 </div>
179 )}
180 </div>
181 </div>,
182 document.body,
183 );
184 };
185