@rxdrag/website-lib-react 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/ReactModalTrigger-9207e763.js +26 -0
  2. package/dist/ReactModalTrigger-9207e763.js.map +1 -0
  3. package/dist/components/ContactForm/ContactForm.d.ts +2 -1
  4. package/dist/components/Icon/index.d.ts +2 -1
  5. package/dist/components/RichTextOutline/parseOutline.d.ts +5 -0
  6. package/dist/components/all.d.ts +0 -21
  7. package/dist/components/index.d.ts +0 -5
  8. package/dist/forms.d.ts +1 -0
  9. package/dist/forms.mjs +1649 -0
  10. package/dist/forms.mjs.map +1 -0
  11. package/dist/index.mjs +9 -3918
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/jsx-runtime-c02cc059.js +325 -0
  14. package/dist/jsx-runtime-c02cc059.js.map +1 -0
  15. package/dist/media.d.ts +1 -0
  16. package/dist/media.mjs +613 -0
  17. package/dist/media.mjs.map +1 -0
  18. package/dist/richtext.d.ts +1 -0
  19. package/dist/richtext.mjs +191 -0
  20. package/dist/richtext.mjs.map +1 -0
  21. package/dist/ui.d.ts +10 -0
  22. package/dist/ui.mjs +687 -0
  23. package/dist/ui.mjs.map +1 -0
  24. package/dist/video.d.ts +2 -0
  25. package/dist/video.mjs +426 -0
  26. package/dist/video.mjs.map +1 -0
  27. package/forms.ts +1 -0
  28. package/index.ts +1 -0
  29. package/media.ts +1 -0
  30. package/package.json +40 -5
  31. package/richtext.ts +1 -0
  32. package/src/components/Analytics/eventHandlers.ts +173 -0
  33. package/src/components/Analytics/index.tsx +21 -0
  34. package/src/components/Analytics/singleton.ts +214 -0
  35. package/src/components/Analytics/tracking.ts +221 -0
  36. package/src/components/Analytics/types.ts +60 -0
  37. package/src/components/Analytics/utils.ts +95 -0
  38. package/src/components/AttachmentIcon/index.tsx +53 -0
  39. package/src/components/BackgroundHlsVideoPlayer.tsx +97 -0
  40. package/src/components/BackgroundVideoPlayer.tsx +32 -0
  41. package/src/components/Bulletin.tsx +30 -0
  42. package/src/components/ContactForm/ContactForm.tsx +296 -0
  43. package/src/components/ContactForm/FileUpload2.tsx +423 -0
  44. package/src/components/ContactForm/Input.tsx +48 -0
  45. package/src/components/ContactForm/Input2.tsx +59 -0
  46. package/src/components/ContactForm/Submit.tsx +48 -0
  47. package/src/components/ContactForm/TelInput.tsx +215 -0
  48. package/src/components/ContactForm/TelInput2.tsx +213 -0
  49. package/src/components/ContactForm/Textarea.tsx +48 -0
  50. package/src/components/ContactForm/Textarea2.tsx +89 -0
  51. package/src/components/ContactForm/countryDialCodes.ts +243 -0
  52. package/src/components/ContactForm/factory.tsx +60 -0
  53. package/src/components/ContactForm/funcs.ts +64 -0
  54. package/src/components/ContactForm/hooks/useInlineLabelPadding.ts +43 -0
  55. package/src/components/ContactForm/hooks/useTelControl.ts +81 -0
  56. package/src/components/ContactForm/index.ts +7 -0
  57. package/src/components/ContactForm/types.ts +68 -0
  58. package/src/components/Icon/index.tsx +20 -0
  59. package/src/components/Medias/MainMedia.tsx +257 -0
  60. package/src/components/Medias/Thumbnail.tsx +62 -0
  61. package/src/components/Medias/VideoPlayer.tsx +114 -0
  62. package/src/components/Medias/index.tsx +271 -0
  63. package/src/components/ProductCard/ProductCard.tsx +24 -0
  64. package/src/components/ProductCard/ProductCta/index.tsx +28 -0
  65. package/src/components/ProductCard/ProductCta/style.css +4 -0
  66. package/src/components/ProductCard/ProductDescription/index.tsx +13 -0
  67. package/src/components/ProductCard/ProductDescription/style.css +6 -0
  68. package/src/components/ProductCard/ProductMedia/index.tsx +35 -0
  69. package/src/components/ProductCard/ProductMedia/style.css +6 -0
  70. package/src/components/ProductCard/ProductTitle/index.tsx +7 -0
  71. package/src/components/ProductCard/ProductTitle/style.css +4 -0
  72. package/src/components/ProductCard/ProductView.tsx +36 -0
  73. package/src/components/ProductCard/index.ts +5 -0
  74. package/src/components/ProductCard/useQueryProduct.ts +32 -0
  75. package/src/components/ReactModalTrigger.tsx +28 -0
  76. package/src/components/ReactVideoPlayer.tsx +332 -0
  77. package/src/components/RichTextOutline/index.tsx +74 -0
  78. package/src/components/RichTextOutline/parseOutline.ts +63 -0
  79. package/src/components/RichTextOutline/useAcitviedHeading.ts +142 -0
  80. package/src/components/RichTextOutline/useAnchorScroll.ts +24 -0
  81. package/src/components/Scroller.tsx +39 -0
  82. package/src/components/SearchInput.tsx +21 -0
  83. package/src/components/Share/index.tsx +86 -0
  84. package/src/components/Share/socials.tsx +80 -0
  85. package/src/components/Share//350/265/204/346/226/231.md +7 -0
  86. package/src/components/ToTop.tsx +72 -0
  87. package/src/components/VideoPlayIcon.tsx +43 -0
  88. package/src/components/all.ts +25 -0
  89. package/src/components/index.ts +12 -0
  90. package/src/forms.ts +1 -0
  91. package/src/index.ts +1 -0
  92. package/src/media.ts +1 -0
  93. package/src/richtext.ts +1 -0
  94. package/src/types/view-model.ts +37 -0
  95. package/src/ui.ts +10 -0
  96. package/src/video.ts +2 -0
  97. package/ui.ts +1 -0
  98. package/video.ts +1 -0
  99. package/dist/style.css +0 -17
@@ -0,0 +1,423 @@
1
+ import clsx from "clsx";
2
+ import { forwardRef, useRef, useState } from "react";
3
+ import { useInlineLabelPadding } from "./hooks/useInlineLabelPadding";
4
+
5
+ /**
6
+ * Simple encryption function for generating anti-bot encrypted fields
7
+ */
8
+ const generateEncryption = (formSalt: string, value: string): string => {
9
+ const timestamp = Math.floor(Date.now() / (60 * 1000));
10
+ const dataToEncrypt = `${formSalt}:${timestamp}:${value}`;
11
+
12
+ let hash = 0;
13
+ for (let i = 0; i < dataToEncrypt.length; i++) {
14
+ const char = dataToEncrypt.charCodeAt(i);
15
+ hash = (hash << 5) - hash + char;
16
+ hash = hash & hash;
17
+ }
18
+
19
+ return `${hash.toString(16)}_${timestamp}`;
20
+ };
21
+
22
+ export type UploadedFile = {
23
+ id: string;
24
+ name: string;
25
+ size: number;
26
+ url: string; // OSS public URL
27
+ };
28
+
29
+ export type FileUploadProps = {
30
+ label: string;
31
+ name: string;
32
+ required?: boolean;
33
+ className?: string;
34
+ labelClassName?: string;
35
+ inputClassName?: string;
36
+ accept?: string;
37
+ onChange?: (fileInfo: UploadedFile | null) => void;
38
+ onUploadStateChange?: (isUploading: boolean) => void;
39
+ error?: string;
40
+ placeholder?: string;
41
+ uploadingText?: string;
42
+ selectedText?: string;
43
+ formSalt?: string; // 加密盐
44
+ maxSize?: number; // 最大文件大小(字节),默认 5MB
45
+ };
46
+
47
+ export const FileUpload2 = forwardRef<HTMLInputElement, FileUploadProps>(
48
+ (props, ref) => {
49
+ const {
50
+ label,
51
+ name,
52
+ required,
53
+ className,
54
+ labelClassName,
55
+ inputClassName,
56
+ accept = "*/*",
57
+ onChange,
58
+ onUploadStateChange,
59
+ error,
60
+ placeholder = "Click to select file",
61
+ uploadingText = "Uploading...",
62
+ selectedText = "Selected: {name}",
63
+ formSalt = "default-salt",
64
+ maxSize = 5 * 1024 * 1024,
65
+ ...rest
66
+ } = props;
67
+
68
+ const [uploadedFile, setUploadedFile] = useState<UploadedFile | null>(null);
69
+ const [isUploading, setIsUploading] = useState(false);
70
+ const [uploadError, setUploadError] = useState<string>("");
71
+ const fileInputRef = useRef<HTMLInputElement>(null);
72
+ const { labelRef, paddingLeft } = useInlineLabelPadding(16);
73
+ const abortControllerRef = useRef<AbortController | null>(null);
74
+
75
+ // Upload single file
76
+ const uploadFile = async (
77
+ file: File,
78
+ signal: AbortSignal
79
+ ): Promise<UploadedFile> => {
80
+ // Validate file size
81
+ if (file.size > maxSize) {
82
+ throw new Error(
83
+ `File ${file.name} exceeds maximum size limit of ${
84
+ maxSize / 1024 / 1024
85
+ }MB`
86
+ );
87
+ }
88
+
89
+ // Generate encrypted field with honeypot value
90
+ const honeypot = "";
91
+ const encryptedField = generateEncryption(formSalt, honeypot);
92
+
93
+ // 1. Get upload credentials
94
+ const credentialsResponse = await fetch("/api/get-upload-credentials", {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ },
99
+ body: JSON.stringify({
100
+ fileName: file.name,
101
+ fileSize: file.size,
102
+ fileType: file.type,
103
+ mediaType: "document", // Request document type credentials (usually R2)
104
+ encryptedField,
105
+ honeypot,
106
+ }),
107
+ signal,
108
+ });
109
+
110
+ if (!credentialsResponse.ok) {
111
+ const errorData = await credentialsResponse.json();
112
+ throw new Error(
113
+ errorData.message || "Failed to get upload credentials"
114
+ );
115
+ }
116
+
117
+ const credentialsData = await credentialsResponse.json();
118
+
119
+ if (!credentialsData.success) {
120
+ throw new Error(
121
+ credentialsData.message || "Failed to get upload credentials"
122
+ );
123
+ }
124
+
125
+ // Extract upload information from response
126
+ const { credentials, mediaType } = credentialsData;
127
+ const uploadUrl = credentials?.uploadUrl;
128
+ const platform = credentials?.platform;
129
+
130
+ if (!uploadUrl) {
131
+ throw new Error("Failed to get upload URL");
132
+ }
133
+
134
+ // 2. Upload to cloud storage
135
+
136
+ let uploadResponse: Response;
137
+
138
+ // Use different upload methods based on platform type
139
+ if (platform === "cloudflare_images") {
140
+ // Cloudflare Images uses FormData + POST
141
+ const formData = new FormData();
142
+ formData.append("file", file);
143
+
144
+ uploadResponse = await fetch(uploadUrl, {
145
+ method: "POST",
146
+ body: formData,
147
+ signal,
148
+ });
149
+ } else {
150
+ // Other platforms use PUT (e.g., pre-signed URLs)
151
+ uploadResponse = await fetch(uploadUrl, {
152
+ method: "PUT",
153
+ headers: {
154
+ "Content-Type": file.type,
155
+ },
156
+ body: file,
157
+ signal,
158
+ });
159
+ }
160
+
161
+ if (!uploadResponse.ok) {
162
+ throw new Error(
163
+ `File upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`
164
+ );
165
+ }
166
+
167
+ // Get resource identifier after upload
168
+ let resourceId: string;
169
+ let publicUrl: string;
170
+
171
+ if (platform === "cloudflare_images") {
172
+ // Cloudflare Images returns JSON, extract result.id
173
+ const uploadResult = await uploadResponse.json();
174
+ resourceId = uploadResult?.result?.id || uploadUrl;
175
+ // Construct Cloudflare Images public URL
176
+ // Format: https://imagedelivery.net/{account_hash}/{image_id}/public
177
+ const accountHash = uploadUrl.split("/")[3]; // Extract from upload URL
178
+ publicUrl = `https://imagedelivery.net/${accountHash}/${resourceId}/public`;
179
+ } else if (platform === "cloudflare_r2") {
180
+ // Cloudflare R2: uploadUrl is already the public URL (pre-signed or public bucket URL)
181
+ resourceId = uploadUrl.split("/").pop() || uploadUrl; // Extract filename from URL
182
+ publicUrl = uploadUrl; // Use the upload URL as public URL
183
+ } else {
184
+ // Other platforms use uploadUrl as resource identifier
185
+ resourceId = uploadUrl;
186
+ publicUrl = uploadUrl;
187
+ }
188
+
189
+ // 3. Insert Media object into database
190
+ const mediaPayload = {
191
+ name: file.name,
192
+ size: file.size,
193
+ mimeType: file.type,
194
+ mediaType,
195
+ resourceUrl: resourceId,
196
+ storageType: platform || "cloudflare_images",
197
+ encryptedField,
198
+ honeypot,
199
+ };
200
+
201
+ const createMediaResponse = await fetch("/api/create-media", {
202
+ method: "POST",
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ },
206
+ body: JSON.stringify(mediaPayload),
207
+ signal,
208
+ });
209
+
210
+ if (!createMediaResponse.ok) {
211
+ const errorData = await createMediaResponse.json();
212
+ throw new Error(errorData.message || "Failed to create Media object");
213
+ }
214
+
215
+ const mediaData = await createMediaResponse.json();
216
+
217
+ if (!mediaData.success) {
218
+ throw new Error(mediaData.message || "Failed to create Media object");
219
+ }
220
+
221
+ // Return uploaded file information
222
+ return {
223
+ id: mediaData.mediaId,
224
+ name: file.name,
225
+ size: file.size,
226
+ url: publicUrl, // Use public URL instead of resource ID
227
+ };
228
+ };
229
+
230
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
231
+ const files = e.target.files;
232
+
233
+ if (!files || files.length === 0) return;
234
+
235
+ const file = files[0]; // Only take the first file
236
+
237
+ // Create new AbortController
238
+ const abortController = new AbortController();
239
+ abortControllerRef.current = abortController;
240
+
241
+ setIsUploading(true);
242
+ onUploadStateChange?.(true); // 通知父组件开始上传
243
+ setUploadError("");
244
+
245
+ try {
246
+ // Validate file size
247
+ if (file.size > maxSize) {
248
+ throw new Error(
249
+ `File ${file.name} exceeds ${maxSize / 1024 / 1024}MB limit`
250
+ );
251
+ }
252
+
253
+ // Upload file
254
+ const uploadedFileData = await uploadFile(file, abortController.signal);
255
+ setUploadedFile(uploadedFileData);
256
+ onChange?.(uploadedFileData); // Pass complete file info
257
+ } catch (error) {
258
+ // Don't show error if user cancelled
259
+ if (error instanceof Error && error.name === "AbortError") {
260
+ console.log("User cancelled upload");
261
+ } else {
262
+ console.error("File upload failed:", error);
263
+ setUploadError(
264
+ error instanceof Error ? error.message : "File upload failed"
265
+ );
266
+ }
267
+ } finally {
268
+ setIsUploading(false);
269
+ onUploadStateChange?.(false); // 通知父组件上传结束
270
+ abortControllerRef.current = null;
271
+ // Clear input to allow re-selecting the same file
272
+ if (fileInputRef.current) {
273
+ fileInputRef.current.value = "";
274
+ }
275
+ }
276
+ };
277
+
278
+ const handleClick = () => {
279
+ // Don't allow selection if uploading or file already exists
280
+ if (isUploading || uploadedFile) return;
281
+ fileInputRef.current?.click();
282
+ };
283
+
284
+ const handleRemoveFile = (e: React.MouseEvent) => {
285
+ e.stopPropagation();
286
+ setUploadedFile(null);
287
+ onChange?.(null); // Pass null when file is removed
288
+ setUploadError("");
289
+ };
290
+
291
+ const handleCancelUpload = (e: React.MouseEvent) => {
292
+ e.stopPropagation();
293
+ if (abortControllerRef.current) {
294
+ abortControllerRef.current.abort();
295
+ }
296
+ };
297
+
298
+ const getDisplayText = () => {
299
+ if (isUploading) return uploadingText;
300
+ if (uploadedFile) {
301
+ return selectedText.replace("{name}", uploadedFile.name);
302
+ }
303
+ return placeholder;
304
+ };
305
+
306
+ return (
307
+ <div
308
+ ref={ref}
309
+ className={clsx("relative w-full", className)}
310
+ {...rest}
311
+ >
312
+ <label
313
+ ref={labelRef}
314
+ htmlFor={name}
315
+ className={clsx(
316
+ "absolute left-3 top-1/2 -translate-y-1/2 mt-1 opacity-70 text-sm pointer-events-none whitespace-nowrap z-10",
317
+ labelClassName
318
+ )}
319
+ >
320
+ {label}
321
+ </label>
322
+
323
+ {/* Hidden file input */}
324
+ <input
325
+ ref={fileInputRef}
326
+ type="file"
327
+ id={name}
328
+ name={name}
329
+ required={required}
330
+ accept={accept}
331
+ onChange={handleFileChange}
332
+ disabled={!!uploadedFile}
333
+ className="hidden"
334
+ />
335
+
336
+ {/* Simulated input box upload area */}
337
+ <div
338
+ onClick={handleClick}
339
+ className={clsx(
340
+ "w-full pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-300 bg-transparent transition-colors duration-200 flex items-center justify-start min-h-[2.5rem]",
341
+ isUploading || uploadedFile
342
+ ? "cursor-not-allowed opacity-75"
343
+ : "cursor-pointer hover:bg-white",
344
+ inputClassName
345
+ )}
346
+ style={{ paddingLeft }}
347
+ >
348
+ <span
349
+ className={clsx(
350
+ "pl-6 text-sm font-medium flex-1 mr-4",
351
+ uploadedFile ? "text-gray-900" : "text-gray-600"
352
+ )}
353
+ >
354
+ {getDisplayText()}
355
+ </span>
356
+ {isUploading ? (
357
+ <button
358
+ type="button"
359
+ onClick={handleCancelUpload}
360
+ className="ml-2 p-1 hover:bg-gray-100 rounded-full transition-colors flex-shrink-0 flex items-center gap-1"
361
+ title="Cancel upload"
362
+ >
363
+ <div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-primary-500" />
364
+ <svg
365
+ className="w-3 h-3 text-gray-500"
366
+ fill="none"
367
+ stroke="currentColor"
368
+ viewBox="0 0 24 24"
369
+ >
370
+ <path
371
+ strokeLinecap="round"
372
+ strokeLinejoin="round"
373
+ strokeWidth={2}
374
+ d="M6 18L18 6M6 6l12 12"
375
+ />
376
+ </svg>
377
+ </button>
378
+ ) : uploadedFile ? (
379
+ <button
380
+ type="button"
381
+ onClick={handleRemoveFile}
382
+ className="ml-2 p-1 hover:bg-red-100 rounded-full transition-colors flex-shrink-0"
383
+ title="Delete file"
384
+ >
385
+ <svg
386
+ className="w-4 h-4 text-red-500"
387
+ fill="none"
388
+ stroke="currentColor"
389
+ viewBox="0 0 24 24"
390
+ >
391
+ <path
392
+ strokeLinecap="round"
393
+ strokeLinejoin="round"
394
+ strokeWidth={2}
395
+ d="M6 18L18 6M6 6l12 12"
396
+ />
397
+ </svg>
398
+ </button>
399
+ ) : (
400
+ <svg
401
+ className="w-5 h-5 text-gray-400 flex-shrink-0"
402
+ fill="none"
403
+ stroke="currentColor"
404
+ viewBox="0 0 24 24"
405
+ >
406
+ <path
407
+ strokeLinecap="round"
408
+ strokeLinejoin="round"
409
+ strokeWidth={2}
410
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
411
+ />
412
+ </svg>
413
+ )}
414
+ </div>
415
+
416
+ {/* Display error message */}
417
+ {(error || uploadError) && (
418
+ <p className="text-red-500 mt-1 text-sm">{error || uploadError}</p>
419
+ )}
420
+ </div>
421
+ );
422
+ }
423
+ );
@@ -0,0 +1,48 @@
1
+ import { forwardRef } from "react";
2
+ import { FieldConfig } from "./types";
3
+
4
+ export type InputProps = Omit<FieldConfig, "feildStyle"> & {
5
+ requiredClassName?: string;
6
+ value?: string;
7
+ onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
8
+ error?: string;
9
+ };
10
+
11
+ export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
12
+ const {
13
+ label,
14
+ name,
15
+ required,
16
+ requiredClassName,
17
+ labelClassName,
18
+ inputClassName,
19
+ type,
20
+ autoFocus,
21
+ value,
22
+ onChange,
23
+ error,
24
+ ...rest
25
+ } = props;
26
+
27
+ return (
28
+ <div {...rest}>
29
+ <label htmlFor={name} className={labelClassName}>
30
+ {label}
31
+ {required ? <span className={requiredClassName}>*</span> : ""}
32
+ </label>
33
+ <input
34
+ ref={ref}
35
+ type={type || "text"}
36
+ id={name}
37
+ name={name}
38
+ required={required}
39
+ className={inputClassName}
40
+ autoComplete={name}
41
+ autoFocus={autoFocus}
42
+ value={value}
43
+ onChange={onChange}
44
+ />
45
+ {error && <p className="text-red-500 mt-1">{error}</p>}
46
+ </div>
47
+ );
48
+ });
@@ -0,0 +1,59 @@
1
+ import clsx from "clsx";
2
+ import { forwardRef } from "react";
3
+ import { InputProps } from "./Input";
4
+ import { useInlineLabelPadding } from "./hooks/useInlineLabelPadding";
5
+
6
+ export const Input2 = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
7
+ const {
8
+ label,
9
+ name,
10
+ required,
11
+ requiredClassName,
12
+ className,
13
+ labelClassName,
14
+ inputClassName,
15
+ type = "text",
16
+ autoFocus,
17
+ value,
18
+ placeholder,
19
+ onChange,
20
+ error,
21
+ ...rest
22
+ } = props;
23
+
24
+ const { labelRef, paddingLeft } = useInlineLabelPadding(16);
25
+
26
+ return (
27
+ <div className={clsx("relative w-full", className)} {...rest}>
28
+ <label
29
+ ref={labelRef}
30
+ htmlFor={name}
31
+ className={clsx(
32
+ "absolute left-3 top-1/2 -translate-y-1/2 mt-1 opacity-70 text-sm pointer-events-none whitespace-nowrap",
33
+ labelClassName
34
+ )}
35
+ >
36
+ {label}
37
+ {required ? <span className={requiredClassName}>*</span> : ""}
38
+ </label>
39
+ <input
40
+ ref={ref}
41
+ type={type}
42
+ id={name}
43
+ name={name}
44
+ required={required}
45
+ placeholder={placeholder}
46
+ className={clsx(
47
+ "w-full pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-300 bg-transparent focus:bg-white transition-colors duration-200",
48
+ inputClassName
49
+ )}
50
+ style={{ paddingLeft }}
51
+ autoComplete={name}
52
+ autoFocus={autoFocus}
53
+ value={value}
54
+ onChange={onChange}
55
+ />
56
+ {error && <p className="text-red-500 mt-1 text-sm">{error}</p>}
57
+ </div>
58
+ );
59
+ });
@@ -0,0 +1,48 @@
1
+ export type SubmitProps = {
2
+ needClasses?: string;
3
+ title: string;
4
+ className?: string;
5
+ rawHtml?: string;
6
+ spinner?: React.ReactNode;
7
+ submitting?: boolean;
8
+ disabled?: boolean;
9
+ onClick?: (e: React.MouseEvent) => void;
10
+ };
11
+
12
+ export const Submit: React.FC<SubmitProps> = (props) => {
13
+ const {
14
+ title = "Send Message",
15
+ className,
16
+ spinner,
17
+ submitting,
18
+ disabled,
19
+ onClick,
20
+ rawHtml,
21
+ needClasses,
22
+ ...rest
23
+ } = props;
24
+ return (
25
+ <>
26
+ <button
27
+ type="button"
28
+ className={className}
29
+ disabled={disabled ?? submitting}
30
+ onClick={onClick}
31
+ {...rest}
32
+ >
33
+ {submitting && spinner}
34
+ {rawHtml ? (
35
+ <div
36
+ style={{ display: "contents" }}
37
+ dangerouslySetInnerHTML={{ __html: rawHtml }}
38
+ />
39
+ ) : (
40
+ title
41
+ )}
42
+ </button>
43
+ <span className="fixed -z-10 top-0 left-0 w-0 h-0 overflow-hidden opacity-0 pointer-events-none">
44
+ <span className={needClasses}></span>
45
+ </span>
46
+ </>
47
+ );
48
+ };