@rxdrag/website-lib-core 0.0.50 → 0.0.51
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.
- package/package.json +10 -9
- package/src/astro/README.md +1 -0
- package/src/astro/animation.ts +146 -0
- package/src/astro/background.ts +53 -0
- package/src/astro/grid/consts.ts +80 -0
- package/src/astro/grid/index.ts +2 -0
- package/src/astro/grid/types.ts +35 -0
- package/src/astro/index.ts +7 -0
- package/src/astro/media.ts +109 -0
- package/src/astro/section/index.ts +12 -0
- package/src/entify/Entify.ts +32 -2
- package/src/entify/IEntify.ts +30 -6
- package/src/entify/lib/createUploadCredentials.ts +56 -0
- package/src/entify/lib/index.ts +30 -29
- package/src/entify/lib/newQueryProductOptions.ts +1 -0
- package/src/entify/lib/queryLangs.ts +5 -5
- package/src/entify/lib/queryOneTheme.ts +12 -12
- package/src/index.ts +1 -0
- package/src/react/components/Analytics/eventHandlers.ts +173 -0
- package/src/react/components/Analytics/index.tsx +21 -0
- package/src/react/components/Analytics/singleton.ts +214 -0
- package/src/react/components/Analytics/tracking.ts +221 -0
- package/src/react/components/Analytics/types.ts +60 -0
- package/src/react/components/Analytics/utils.ts +95 -0
- package/src/react/components/BackgroundHlsVideoPlayer.tsx +68 -0
- package/src/react/components/BackgroundVideoPlayer.tsx +32 -0
- package/src/react/components/ContactForm/ContactForm.tsx +286 -0
- package/src/react/components/ContactForm/FileUpload.tsx +430 -0
- package/src/react/components/ContactForm/Input.tsx +6 -10
- package/src/react/components/ContactForm/Input2.tsx +64 -0
- package/src/react/components/ContactForm/Submit.tsx +25 -10
- package/src/react/components/ContactForm/Textarea.tsx +7 -10
- package/src/react/components/ContactForm/Textarea2.tsx +64 -0
- package/src/react/components/ContactForm/factory.tsx +49 -0
- package/src/react/components/ContactForm/funcs.ts +64 -0
- package/src/react/components/ContactForm/index.ts +7 -0
- package/src/react/components/ContactForm/types.ts +67 -0
- package/src/react/components/ContactForm/useVisitorInfo.ts +31 -0
- package/src/react/components/index.ts +3 -0
- package/src/react/components/ContactForm/index.tsx +0 -351
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
import { forwardRef, useState } from "react";
|
|
2
|
-
import { Input } from "./Input";
|
|
3
|
-
import { Submit } from "./Submit";
|
|
4
|
-
import { Textarea } from "./Textarea";
|
|
5
|
-
import clsx from "clsx";
|
|
6
|
-
import { modal } from "../../../controller";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 简单的加密函数,用于生成防机器人的加密字段
|
|
10
|
-
* @param value 需要加密的值
|
|
11
|
-
* @returns 加密后的字符串
|
|
12
|
-
*/
|
|
13
|
-
const encrypt = (value: string, formSalt: string): string => {
|
|
14
|
-
// 获取当前时间戳,精确到分钟级别(防止频繁变化)
|
|
15
|
-
const timestamp = Math.floor(Date.now() / (60 * 1000));
|
|
16
|
-
|
|
17
|
-
// 将时间戳与盐值和输入值组合
|
|
18
|
-
const dataToEncrypt = `${formSalt}:${timestamp}:${value}`;
|
|
19
|
-
|
|
20
|
-
// 使用简单的哈希算法
|
|
21
|
-
let hash = 0;
|
|
22
|
-
for (let i = 0; i < dataToEncrypt.length; i++) {
|
|
23
|
-
const char = dataToEncrypt.charCodeAt(i);
|
|
24
|
-
hash = (hash << 5) - hash + char;
|
|
25
|
-
hash = hash & hash; // 转换为32位整数
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 转换为16进制字符串并添加时间戳信息
|
|
29
|
-
return `${hash.toString(16)}_${timestamp}`;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 验证加密字段是否有效
|
|
34
|
-
* @param encryptedValue 加密后的字符串
|
|
35
|
-
* @param originalValue 原始值(如蜜罐字段的值)
|
|
36
|
-
* @param maxAgeMinutes 最大有效时间(分钟)
|
|
37
|
-
* @returns 是否有效
|
|
38
|
-
*/
|
|
39
|
-
export const verifyEncryption = (
|
|
40
|
-
formSalt: string,
|
|
41
|
-
encryptedValue: string = "",
|
|
42
|
-
originalValue: string = "",
|
|
43
|
-
maxAgeMinutes: number = 30
|
|
44
|
-
): boolean => {
|
|
45
|
-
// 解析加密值
|
|
46
|
-
const parts = encryptedValue.split("_");
|
|
47
|
-
if (parts.length !== 2) {
|
|
48
|
-
return false; // 格式不正确
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const [hashString, timestampString] = parts;
|
|
52
|
-
const timestamp = parseInt(timestampString, 10);
|
|
53
|
-
|
|
54
|
-
// 验证时间戳是否在有效期内
|
|
55
|
-
const currentTimestamp = Math.floor(Date.now() / (60 * 1000));
|
|
56
|
-
if (isNaN(timestamp) || currentTimestamp - timestamp > maxAgeMinutes) {
|
|
57
|
-
return false; // 时间戳无效或已过期
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 重新计算哈希值
|
|
61
|
-
const dataToEncrypt = `${formSalt}:${timestamp}:${originalValue}`;
|
|
62
|
-
let hash = 0;
|
|
63
|
-
for (let i = 0; i < dataToEncrypt.length; i++) {
|
|
64
|
-
const char = dataToEncrypt.charCodeAt(i);
|
|
65
|
-
hash = (hash << 5) - hash + char;
|
|
66
|
-
hash = hash & hash;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 比较哈希值
|
|
70
|
-
return hashString === hash.toString(16);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export interface QuoteRequest {
|
|
74
|
-
name?: string;
|
|
75
|
-
email?: string;
|
|
76
|
-
company?: string;
|
|
77
|
-
message?: string;
|
|
78
|
-
fromCta?: string;
|
|
79
|
-
// 电话或者whatsapp
|
|
80
|
-
mobile?: string;
|
|
81
|
-
// 蜜罐字段,用于检测机器人
|
|
82
|
-
phone: string;
|
|
83
|
-
// 一个加密字段,用于防机器人,点击时附加一个加密的字符串
|
|
84
|
-
encryptedField?: string;
|
|
85
|
-
}
|
|
86
|
-
interface FormErrors {
|
|
87
|
-
name?: string;
|
|
88
|
-
email?: string;
|
|
89
|
-
message?: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export type ContactFormProps = {
|
|
93
|
-
submitAlign?: "left" | "center" | "right";
|
|
94
|
-
actionUrl?: string;
|
|
95
|
-
formSalt: string;
|
|
96
|
-
classNames?: {
|
|
97
|
-
submit?: string;
|
|
98
|
-
container?: string;
|
|
99
|
-
};
|
|
100
|
-
labels?: {
|
|
101
|
-
name?: string;
|
|
102
|
-
email?: string;
|
|
103
|
-
company?: string;
|
|
104
|
-
message?: string;
|
|
105
|
-
submit?: string;
|
|
106
|
-
mobile?: string;
|
|
107
|
-
};
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export const ContactForm = forwardRef<HTMLDivElement, ContactFormProps>(
|
|
111
|
-
(props, ref) => {
|
|
112
|
-
const {
|
|
113
|
-
submitAlign = "right",
|
|
114
|
-
actionUrl = "/api/ask-for-quote",
|
|
115
|
-
formSalt,
|
|
116
|
-
labels = {},
|
|
117
|
-
classNames = {},
|
|
118
|
-
} = props;
|
|
119
|
-
const [formData, setFormData] = useState<QuoteRequest>({
|
|
120
|
-
name: "",
|
|
121
|
-
email: "",
|
|
122
|
-
company: "",
|
|
123
|
-
message: "",
|
|
124
|
-
phone: "", // 初始化蜜罐字段
|
|
125
|
-
});
|
|
126
|
-
const submitClassName = classNames.submit || "btn-primary";
|
|
127
|
-
const containerClassName = classNames.container || "gap-y-6";
|
|
128
|
-
// 错误状态
|
|
129
|
-
const [errors, setErrors] = useState<FormErrors>({});
|
|
130
|
-
const [submitting, setSubmitting] = useState(false);
|
|
131
|
-
const [submitStatus, setSubmitStatus] = useState<{
|
|
132
|
-
success?: boolean;
|
|
133
|
-
message?: string;
|
|
134
|
-
}>({});
|
|
135
|
-
|
|
136
|
-
// 处理输入变化
|
|
137
|
-
const handleChange = (
|
|
138
|
-
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
139
|
-
) => {
|
|
140
|
-
const { name, value } = e.target;
|
|
141
|
-
setFormData((prev) => ({
|
|
142
|
-
...prev,
|
|
143
|
-
[name]: value,
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
setSubmitStatus({}); // 重置提交状态
|
|
147
|
-
|
|
148
|
-
// 清除对应字段的错误
|
|
149
|
-
if (errors[name as keyof FormErrors]) {
|
|
150
|
-
setErrors((prev) => ({
|
|
151
|
-
...prev,
|
|
152
|
-
[name]: undefined,
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
// 验证表单
|
|
157
|
-
const validateForm = (): boolean => {
|
|
158
|
-
const newErrors: FormErrors = {};
|
|
159
|
-
|
|
160
|
-
// if (!formData.name?.trim()) {
|
|
161
|
-
// newErrors.name = "Please enter your name";
|
|
162
|
-
// }
|
|
163
|
-
|
|
164
|
-
if (!formData.email?.trim()) {
|
|
165
|
-
newErrors.email = "Please enter your email";
|
|
166
|
-
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
|
167
|
-
newErrors.email = "Please enter a valid email address";
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!formData.message?.trim()) {
|
|
171
|
-
newErrors.message = "Please enter your message";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// 检查蜜罐字段 - 如果填写了则表示是机器人
|
|
175
|
-
if (formData.phone) {
|
|
176
|
-
// 悄悄失败,不显示错误信息
|
|
177
|
-
console.log("Honeypot triggered - likely spam submission");
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
setErrors(newErrors);
|
|
182
|
-
return Object.keys(newErrors).length === 0;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const handleClick = async (event: React.MouseEvent) => {
|
|
186
|
-
event.preventDefault(); // 阻止表单默认提交行为
|
|
187
|
-
|
|
188
|
-
// 验证表单
|
|
189
|
-
if (!validateForm()) {
|
|
190
|
-
return; // 如果验证失败,不继续提交
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
setSubmitting(true);
|
|
195
|
-
setSubmitStatus({}); // 重置提交状态
|
|
196
|
-
const response = await fetch(actionUrl, {
|
|
197
|
-
method: "POST",
|
|
198
|
-
body: JSON.stringify({
|
|
199
|
-
...formData,
|
|
200
|
-
fromCta: modal.lastCta,
|
|
201
|
-
encryptedField: encrypt(formData.phone, formSalt),
|
|
202
|
-
}),
|
|
203
|
-
headers: {
|
|
204
|
-
"X-Request-URL": window.location.href,
|
|
205
|
-
"Content-Type": "application/json",
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
if (!response.ok) {
|
|
210
|
-
throw new Error(`Server responded with status: ${response.status}`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const result = await response.json();
|
|
214
|
-
|
|
215
|
-
setSubmitStatus({
|
|
216
|
-
success: result.success,
|
|
217
|
-
message: result.message,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
if (result.success) {
|
|
221
|
-
// 重置表单
|
|
222
|
-
setFormData({
|
|
223
|
-
name: "",
|
|
224
|
-
email: "",
|
|
225
|
-
company: "",
|
|
226
|
-
message: "",
|
|
227
|
-
phone: "",
|
|
228
|
-
});
|
|
229
|
-
window.location.href = "/thanks";
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
// 如果出现错误,打印错误信息
|
|
233
|
-
console.error("Form submission error:", error);
|
|
234
|
-
setSubmitStatus({
|
|
235
|
-
success: false,
|
|
236
|
-
message:
|
|
237
|
-
error instanceof Error
|
|
238
|
-
? `Error: ${error.message}`
|
|
239
|
-
: "Failed to submit the form. Please try again later.",
|
|
240
|
-
});
|
|
241
|
-
} finally {
|
|
242
|
-
setSubmitting(false);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<div
|
|
248
|
-
ref={ref}
|
|
249
|
-
className={clsx(
|
|
250
|
-
"py-4 grid max-w-2xl grid-cols-1 gap-x-6 sm:grid-cols-2",
|
|
251
|
-
containerClassName
|
|
252
|
-
)}
|
|
253
|
-
>
|
|
254
|
-
<Input
|
|
255
|
-
className="sm:col-span-1"
|
|
256
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
257
|
-
label={labels?.name || "Your Name"}
|
|
258
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
259
|
-
name="name"
|
|
260
|
-
value={formData.name}
|
|
261
|
-
onChange={handleChange}
|
|
262
|
-
error={errors.name}
|
|
263
|
-
/>
|
|
264
|
-
<Input
|
|
265
|
-
className="sm:col-span-1"
|
|
266
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
267
|
-
label={(labels?.email || "Your Email") + " *"}
|
|
268
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
269
|
-
name="email"
|
|
270
|
-
required={true}
|
|
271
|
-
type="email"
|
|
272
|
-
value={formData.email}
|
|
273
|
-
onChange={handleChange}
|
|
274
|
-
error={errors.email}
|
|
275
|
-
/>
|
|
276
|
-
<Input
|
|
277
|
-
className="sm:col-span-1"
|
|
278
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
279
|
-
label={labels?.mobile || "Your tel"}
|
|
280
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
281
|
-
name="mobile"
|
|
282
|
-
required={true}
|
|
283
|
-
value={formData.mobile}
|
|
284
|
-
onChange={handleChange}
|
|
285
|
-
/>
|
|
286
|
-
<Input
|
|
287
|
-
className="sm:col-span-1"
|
|
288
|
-
inputClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
289
|
-
label={labels?.company || "Company"}
|
|
290
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
291
|
-
name="company"
|
|
292
|
-
value={formData.company}
|
|
293
|
-
onChange={handleChange}
|
|
294
|
-
/>
|
|
295
|
-
<Textarea
|
|
296
|
-
className="col-span-full"
|
|
297
|
-
label={(labels?.message || "Message") + " *"}
|
|
298
|
-
labelClassName="block text-sm font-medium leading-6 text-gray-900"
|
|
299
|
-
name="message"
|
|
300
|
-
required={true}
|
|
301
|
-
textareaClassName="mt-2 block w-full rounded-md outline-none border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-sky-600 sm:text-sm sm:leading-6"
|
|
302
|
-
value={formData.message}
|
|
303
|
-
onChange={handleChange}
|
|
304
|
-
error={errors.message}
|
|
305
|
-
/>
|
|
306
|
-
{/* 蜜罐字段 - 对用户隐藏但对机器人可见 */}
|
|
307
|
-
<div style={{ display: "none" }}>
|
|
308
|
-
<input
|
|
309
|
-
type="text"
|
|
310
|
-
name="phone"
|
|
311
|
-
value={formData.phone}
|
|
312
|
-
onChange={handleChange}
|
|
313
|
-
tabIndex={-1}
|
|
314
|
-
autoComplete="off"
|
|
315
|
-
/>
|
|
316
|
-
</div>
|
|
317
|
-
<div
|
|
318
|
-
className={clsx("col-span-full flex items-center gap-x-6 px-0 py-2", {
|
|
319
|
-
"justify-start": submitAlign === "left",
|
|
320
|
-
"justify-center": submitAlign === "center",
|
|
321
|
-
"justify-end": submitAlign === "right",
|
|
322
|
-
})}
|
|
323
|
-
>
|
|
324
|
-
{submitStatus.message && !submitStatus.success && (
|
|
325
|
-
<div
|
|
326
|
-
className={`text-sm ${
|
|
327
|
-
submitStatus.success ? "text-green-600" : "text-red-600"
|
|
328
|
-
}`}
|
|
329
|
-
>
|
|
330
|
-
{submitStatus.message}
|
|
331
|
-
</div>
|
|
332
|
-
)}
|
|
333
|
-
<Submit
|
|
334
|
-
className={clsx(
|
|
335
|
-
"flex gap-2 items-center relative shadow-sm btn btn-lg nowrap",
|
|
336
|
-
submitClassName
|
|
337
|
-
)}
|
|
338
|
-
title={labels?.submit || "Send Message"}
|
|
339
|
-
spinner={
|
|
340
|
-
<div className="left-8 flex items-center justify-center">
|
|
341
|
-
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white" />
|
|
342
|
-
</div>
|
|
343
|
-
}
|
|
344
|
-
submitting={submitting}
|
|
345
|
-
onClick={handleClick}
|
|
346
|
-
/>
|
|
347
|
-
</div>
|
|
348
|
-
</div>
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
);
|