PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
yatra / resources / js / components / ui / modal.tsx

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

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