create-brainerce-store 1.31.2 → 1.33.1
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/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.33.1",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -153,7 +153,7 @@ var ALLOWED_PACKAGE_MANAGERS = [
|
|
|
153
153
|
"bun"
|
|
154
154
|
];
|
|
155
155
|
var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
156
|
-
brainerce: "^1.
|
|
156
|
+
brainerce: "^1.22.0",
|
|
157
157
|
"isomorphic-dompurify": "^3.8.0"
|
|
158
158
|
});
|
|
159
159
|
|
package/package.json
CHANGED
|
@@ -1,211 +1,461 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contact form — fully driven by the merchant's dashboard configuration.
|
|
5
|
+
*
|
|
6
|
+
* The merchant configures the form in the Brainerce dashboard under
|
|
7
|
+
* Customers → Contact Forms → <form>
|
|
8
|
+
*
|
|
9
|
+
* From there they can:
|
|
10
|
+
* • edit the title, description, submit button label, success message
|
|
11
|
+
* • add/remove/reorder/hide any field (TEXT/TEXTAREA/EMAIL/PHONE/NUMBER/SELECT/
|
|
12
|
+
* MULTI_SELECT/CHECKBOX/URL/DATE)
|
|
13
|
+
* • toggle required, set placeholder/helpText/enumValues/validation
|
|
14
|
+
* • provide per-locale translations for every label/placeholder/helpText/successMessage
|
|
15
|
+
*
|
|
16
|
+
* Everything below is generic rendering logic — it adapts automatically to any
|
|
17
|
+
* form shape returned by the API, so **you should not hardcode field keys or
|
|
18
|
+
* labels here**. If you want a different visual layout, change the markup
|
|
19
|
+
* inside `DynamicField` (keep the behavior) and keep using `schema.fields` as
|
|
20
|
+
* the source of truth.
|
|
21
|
+
*
|
|
22
|
+
* API contract the page relies on:
|
|
23
|
+
* 1. `GET /stores/{storeId}/contact-forms/main?locale={locale}` → `ContactFormPublic`
|
|
24
|
+
* (see `brainerce` SDK type). Server pre-resolves translations for the
|
|
25
|
+
* requested locale and strips any field with `isVisible=false`.
|
|
26
|
+
* 2. `POST /stores/{storeId}/inquiries` with `{ formKey, fields, locale }` →
|
|
27
|
+
* `CreateInquiryResponse`. Unknown keys are stripped server-side.
|
|
28
|
+
*
|
|
29
|
+
* Rate limit: 3 submissions / 60s per IP. Honeypot field below blocks naive bots.
|
|
30
|
+
* If the merchant has more than one form (e.g. 'main' + 'newsletter'), you can
|
|
31
|
+
* list them via `client.contactForms.list()` and pick by `key`.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
35
|
+
import type { ContactFormPublic, ContactFormPublicField } from 'brainerce';
|
|
36
|
+
import { getClient } from '@/lib/brainerce';
|
|
37
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
38
|
+
import { useTranslations } from '@/lib/translations';
|
|
39
|
+
|
|
40
|
+
type FieldValue = string | string[] | boolean;
|
|
41
|
+
|
|
42
|
+
function defaultValueFor(field: ContactFormPublicField): FieldValue {
|
|
43
|
+
if (field.type === 'CHECKBOX') return false;
|
|
44
|
+
if (field.type === 'MULTI_SELECT') return [];
|
|
45
|
+
return field.defaultValue ?? '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isEmpty(v: FieldValue): boolean {
|
|
49
|
+
if (typeof v === 'string') return v.trim().length === 0;
|
|
50
|
+
if (Array.isArray(v)) return v.length === 0;
|
|
51
|
+
return v === false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const inputClass =
|
|
55
|
+
'border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2';
|
|
56
|
+
|
|
57
|
+
const textareaClass =
|
|
58
|
+
'border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary w-full rounded border px-3 py-2 text-sm focus:outline-none focus:ring-2';
|
|
59
|
+
|
|
60
|
+
export default function ContactPage() {
|
|
61
|
+
const t = useTranslations('contact');
|
|
62
|
+
const [schema, setSchema] = useState<ContactFormPublic | null>(null);
|
|
63
|
+
const [schemaError, setSchemaError] = useState<string | null>(null);
|
|
64
|
+
const [values, setValues] = useState<Record<string, FieldValue>>({});
|
|
65
|
+
const [honeypot, setHoneypot] = useState('');
|
|
66
|
+
const [loading, setLoading] = useState(false);
|
|
67
|
+
const [sent, setSent] = useState(false);
|
|
68
|
+
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
69
|
+
|
|
70
|
+
const locale = useMemo(() => {
|
|
71
|
+
if (typeof document !== 'undefined') return document.documentElement.lang || undefined;
|
|
72
|
+
return undefined;
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
(async () => {
|
|
78
|
+
try {
|
|
79
|
+
const client = getClient();
|
|
80
|
+
const form = await client.contactForms.get('main', locale);
|
|
81
|
+
if (cancelled) return;
|
|
82
|
+
setSchema(form);
|
|
83
|
+
const initial: Record<string, FieldValue> = {};
|
|
84
|
+
for (const field of form.fields) initial[field.key] = defaultValueFor(field);
|
|
85
|
+
setValues(initial);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (cancelled) return;
|
|
88
|
+
setSchemaError(err instanceof Error ? err.message : t('genericError'));
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true;
|
|
93
|
+
};
|
|
94
|
+
}, [t, locale]);
|
|
95
|
+
|
|
96
|
+
function updateValue(key: string, value: FieldValue) {
|
|
97
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
if (loading || !schema) return;
|
|
103
|
+
|
|
104
|
+
if (honeypot.trim().length > 0) {
|
|
105
|
+
setSent(true);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
setLoading(true);
|
|
111
|
+
setSubmitError(null);
|
|
112
|
+
|
|
113
|
+
const payload: Record<string, unknown> = {};
|
|
114
|
+
for (const field of schema.fields) {
|
|
115
|
+
const raw = values[field.key];
|
|
116
|
+
if (isEmpty(raw)) continue;
|
|
117
|
+
payload[field.key] = typeof raw === 'string' ? raw.trim() : raw;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await getClient().createInquiry({
|
|
121
|
+
formKey: schema.key,
|
|
122
|
+
fields: payload,
|
|
123
|
+
locale,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
setSent(true);
|
|
127
|
+
const reset: Record<string, FieldValue> = {};
|
|
128
|
+
for (const field of schema.fields) reset[field.key] = defaultValueFor(field);
|
|
129
|
+
setValues(reset);
|
|
130
|
+
setHoneypot('');
|
|
131
|
+
} catch (err) {
|
|
132
|
+
setSubmitError(err instanceof Error ? err.message : t('genericError'));
|
|
133
|
+
} finally {
|
|
134
|
+
setLoading(false);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (schemaError) {
|
|
139
|
+
return (
|
|
140
|
+
<div className="mx-auto max-w-2xl px-4 py-12 sm:px-6 lg:px-8">
|
|
141
|
+
<div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
|
|
142
|
+
{schemaError}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!schema) {
|
|
149
|
+
return (
|
|
150
|
+
<div className="mx-auto flex max-w-2xl items-center justify-center px-4 py-24 sm:px-6 lg:px-8">
|
|
151
|
+
<LoadingSpinner />
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div className="mx-auto max-w-2xl px-4 py-12 sm:px-6 lg:px-8">
|
|
158
|
+
<div className="mb-8 text-center">
|
|
159
|
+
<h1 className="text-foreground text-3xl font-bold">{schema.name || t('title')}</h1>
|
|
160
|
+
{schema.description ? (
|
|
161
|
+
<p className="text-muted-foreground mt-2 text-sm">{schema.description}</p>
|
|
162
|
+
) : (
|
|
163
|
+
<p className="text-muted-foreground mt-2 text-sm">{t('subtitle')}</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{submitError && (
|
|
168
|
+
<div className="bg-destructive/10 border-destructive/20 text-destructive mb-6 rounded-lg border px-4 py-3 text-sm">
|
|
169
|
+
{submitError}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{sent ? (
|
|
174
|
+
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-6 text-center text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
|
|
175
|
+
<p className="font-medium">{t('thanksTitle')}</p>
|
|
176
|
+
<p className="mt-1">{schema.successMessage || t('thanksBody')}</p>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={() => setSent(false)}
|
|
180
|
+
className="text-primary mt-4 text-sm font-medium hover:underline"
|
|
181
|
+
>
|
|
182
|
+
{t('sendAnother')}
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
) : (
|
|
186
|
+
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
|
|
187
|
+
<div
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
className="pointer-events-none absolute -start-[10000px] h-0 w-0 overflow-hidden"
|
|
190
|
+
>
|
|
191
|
+
<label htmlFor="contact-honeypot">Leave this field empty</label>
|
|
192
|
+
<input
|
|
193
|
+
id="contact-honeypot"
|
|
194
|
+
type="text"
|
|
195
|
+
tabIndex={-1}
|
|
196
|
+
autoComplete="off"
|
|
197
|
+
value={honeypot}
|
|
198
|
+
onChange={(e) => setHoneypot(e.target.value)}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{schema.fields.map((field) => (
|
|
203
|
+
<DynamicField
|
|
204
|
+
key={field.key}
|
|
205
|
+
field={field}
|
|
206
|
+
value={values[field.key] ?? defaultValueFor(field)}
|
|
207
|
+
onChange={(v) => updateValue(field.key, v)}
|
|
208
|
+
t={t}
|
|
209
|
+
/>
|
|
210
|
+
))}
|
|
211
|
+
|
|
212
|
+
<button
|
|
213
|
+
type="submit"
|
|
214
|
+
disabled={loading}
|
|
215
|
+
className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
216
|
+
>
|
|
217
|
+
{loading ? (
|
|
218
|
+
<>
|
|
219
|
+
<LoadingSpinner
|
|
220
|
+
size="sm"
|
|
221
|
+
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
222
|
+
/>
|
|
223
|
+
{t('sending')}
|
|
224
|
+
</>
|
|
225
|
+
) : (
|
|
226
|
+
schema.submitButton || t('send')
|
|
227
|
+
)}
|
|
228
|
+
</button>
|
|
229
|
+
</form>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function DynamicField({
|
|
236
|
+
field,
|
|
237
|
+
value,
|
|
238
|
+
onChange,
|
|
239
|
+
t,
|
|
240
|
+
}: {
|
|
241
|
+
field: ContactFormPublicField;
|
|
242
|
+
value: FieldValue;
|
|
243
|
+
onChange: (v: FieldValue) => void;
|
|
244
|
+
t: (key: string, values?: Record<string, string>) => string;
|
|
245
|
+
}) {
|
|
246
|
+
const id = `contact-${field.key}`;
|
|
247
|
+
const label = (
|
|
248
|
+
<label htmlFor={id} className="text-foreground mb-1.5 block text-sm font-medium">
|
|
249
|
+
{field.label}
|
|
250
|
+
{!field.isRequired && <span className="text-muted-foreground ms-1">({t('optional')})</span>}
|
|
251
|
+
</label>
|
|
252
|
+
);
|
|
253
|
+
const help = field.helpText ? (
|
|
254
|
+
<p className="text-muted-foreground mt-1 text-xs">{field.helpText}</p>
|
|
255
|
+
) : null;
|
|
256
|
+
|
|
257
|
+
const maxLength = field.validation?.maxLength;
|
|
258
|
+
const minLength = field.validation?.minLength;
|
|
259
|
+
const min = field.validation?.min;
|
|
260
|
+
const max = field.validation?.max;
|
|
261
|
+
const pattern = field.validation?.pattern;
|
|
262
|
+
const strVal = typeof value === 'string' ? value : '';
|
|
263
|
+
|
|
264
|
+
switch (field.type) {
|
|
265
|
+
case 'TEXTAREA':
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
{label}
|
|
269
|
+
<textarea
|
|
270
|
+
id={id}
|
|
271
|
+
required={field.isRequired}
|
|
272
|
+
maxLength={maxLength}
|
|
273
|
+
minLength={minLength}
|
|
274
|
+
rows={6}
|
|
275
|
+
placeholder={field.placeholder}
|
|
276
|
+
value={strVal}
|
|
277
|
+
onChange={(e) => onChange(e.target.value)}
|
|
278
|
+
className={textareaClass}
|
|
279
|
+
/>
|
|
280
|
+
{help}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
case 'SELECT':
|
|
285
|
+
return (
|
|
286
|
+
<div>
|
|
287
|
+
{label}
|
|
288
|
+
<select
|
|
289
|
+
id={id}
|
|
290
|
+
required={field.isRequired}
|
|
291
|
+
value={strVal}
|
|
292
|
+
onChange={(e) => onChange(e.target.value)}
|
|
293
|
+
className={inputClass}
|
|
294
|
+
>
|
|
295
|
+
<option value="">—</option>
|
|
296
|
+
{field.enumValues?.map((opt) => (
|
|
297
|
+
<option key={opt.value} value={opt.value}>
|
|
298
|
+
{opt.label}
|
|
299
|
+
</option>
|
|
300
|
+
))}
|
|
301
|
+
</select>
|
|
302
|
+
{help}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
case 'MULTI_SELECT': {
|
|
307
|
+
const arr = Array.isArray(value) ? value : [];
|
|
308
|
+
return (
|
|
309
|
+
<div>
|
|
310
|
+
{label}
|
|
311
|
+
<div className="space-y-2">
|
|
312
|
+
{field.enumValues?.map((opt) => {
|
|
313
|
+
const checked = arr.includes(opt.value);
|
|
314
|
+
return (
|
|
315
|
+
<label key={opt.value} className="flex items-center gap-2 text-sm">
|
|
316
|
+
<input
|
|
317
|
+
type="checkbox"
|
|
318
|
+
checked={checked}
|
|
319
|
+
onChange={(e) => {
|
|
320
|
+
if (e.target.checked) onChange([...arr, opt.value]);
|
|
321
|
+
else onChange(arr.filter((v) => v !== opt.value));
|
|
322
|
+
}}
|
|
323
|
+
/>
|
|
324
|
+
<span>{opt.label}</span>
|
|
325
|
+
</label>
|
|
326
|
+
);
|
|
327
|
+
})}
|
|
328
|
+
</div>
|
|
329
|
+
{help}
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case 'CHECKBOX':
|
|
335
|
+
return (
|
|
336
|
+
<div>
|
|
337
|
+
<label htmlFor={id} className="flex items-start gap-2 text-sm">
|
|
338
|
+
<input
|
|
339
|
+
id={id}
|
|
340
|
+
type="checkbox"
|
|
341
|
+
required={field.isRequired}
|
|
342
|
+
checked={value === true}
|
|
343
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
344
|
+
className="mt-0.5"
|
|
345
|
+
/>
|
|
346
|
+
<span className="text-foreground">{field.label}</span>
|
|
347
|
+
</label>
|
|
348
|
+
{help}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
case 'NUMBER':
|
|
353
|
+
return (
|
|
354
|
+
<div>
|
|
355
|
+
{label}
|
|
356
|
+
<input
|
|
357
|
+
id={id}
|
|
358
|
+
type="number"
|
|
359
|
+
required={field.isRequired}
|
|
360
|
+
min={min}
|
|
361
|
+
max={max}
|
|
362
|
+
placeholder={field.placeholder}
|
|
363
|
+
value={strVal}
|
|
364
|
+
onChange={(e) => onChange(e.target.value)}
|
|
365
|
+
className={inputClass}
|
|
366
|
+
/>
|
|
367
|
+
{help}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
case 'EMAIL':
|
|
372
|
+
return (
|
|
373
|
+
<div>
|
|
374
|
+
{label}
|
|
375
|
+
<input
|
|
376
|
+
id={id}
|
|
377
|
+
type="email"
|
|
378
|
+
required={field.isRequired}
|
|
379
|
+
autoComplete="email"
|
|
380
|
+
placeholder={field.placeholder}
|
|
381
|
+
value={strVal}
|
|
382
|
+
onChange={(e) => onChange(e.target.value)}
|
|
383
|
+
className={inputClass}
|
|
384
|
+
/>
|
|
385
|
+
{help}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
case 'PHONE':
|
|
390
|
+
return (
|
|
391
|
+
<div>
|
|
392
|
+
{label}
|
|
393
|
+
<input
|
|
394
|
+
id={id}
|
|
395
|
+
type="tel"
|
|
396
|
+
required={field.isRequired}
|
|
397
|
+
autoComplete="tel"
|
|
398
|
+
placeholder={field.placeholder}
|
|
399
|
+
value={strVal}
|
|
400
|
+
onChange={(e) => onChange(e.target.value)}
|
|
401
|
+
className={inputClass}
|
|
402
|
+
/>
|
|
403
|
+
{help}
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
case 'URL':
|
|
408
|
+
return (
|
|
409
|
+
<div>
|
|
410
|
+
{label}
|
|
411
|
+
<input
|
|
412
|
+
id={id}
|
|
413
|
+
type="url"
|
|
414
|
+
required={field.isRequired}
|
|
415
|
+
placeholder={field.placeholder}
|
|
416
|
+
value={strVal}
|
|
417
|
+
onChange={(e) => onChange(e.target.value)}
|
|
418
|
+
className={inputClass}
|
|
419
|
+
/>
|
|
420
|
+
{help}
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
case 'DATE':
|
|
425
|
+
return (
|
|
426
|
+
<div>
|
|
427
|
+
{label}
|
|
428
|
+
<input
|
|
429
|
+
id={id}
|
|
430
|
+
type="date"
|
|
431
|
+
required={field.isRequired}
|
|
432
|
+
value={strVal}
|
|
433
|
+
onChange={(e) => onChange(e.target.value)}
|
|
434
|
+
className={inputClass}
|
|
435
|
+
/>
|
|
436
|
+
{help}
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
case 'TEXT':
|
|
441
|
+
default:
|
|
442
|
+
return (
|
|
443
|
+
<div>
|
|
444
|
+
{label}
|
|
445
|
+
<input
|
|
446
|
+
id={id}
|
|
447
|
+
type="text"
|
|
448
|
+
required={field.isRequired}
|
|
449
|
+
maxLength={maxLength}
|
|
450
|
+
minLength={minLength}
|
|
451
|
+
pattern={pattern}
|
|
452
|
+
placeholder={field.placeholder}
|
|
453
|
+
value={strVal}
|
|
454
|
+
onChange={(e) => onChange(e.target.value)}
|
|
455
|
+
className={inputClass}
|
|
456
|
+
/>
|
|
457
|
+
{help}
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% if (i18nEnabled) { %>
|
|
2
2
|
'use client';
|
|
3
3
|
|
|
4
|
-
import { createContext, useContext } from 'react';
|
|
4
|
+
import { createContext, useCallback, useContext } from 'react';
|
|
5
5
|
|
|
6
6
|
type Messages = Record<string, Record<string, string>>;
|
|
7
7
|
|
|
@@ -11,33 +11,40 @@ export { MessagesContext };
|
|
|
11
11
|
|
|
12
12
|
export function useTranslations(namespace: string) {
|
|
13
13
|
const messages = useContext(MessagesContext);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
return useCallback(
|
|
15
|
+
(key: string, values?: Record<string, string>): string => {
|
|
16
|
+
const ns = (messages[namespace] || {}) as Record<string, string>;
|
|
17
|
+
let result = ns[key] || `${namespace}.${key}`;
|
|
18
|
+
if (values) {
|
|
19
|
+
for (const [k, v] of Object.entries(values)) {
|
|
20
|
+
result = result.replace(`{${k}}`, v);
|
|
21
|
+
}
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
return result;
|
|
24
|
+
},
|
|
25
|
+
[messages, namespace]
|
|
26
|
+
);
|
|
24
27
|
}
|
|
25
28
|
<% } else { %>
|
|
29
|
+
import { useCallback } from 'react';
|
|
26
30
|
import { messages } from '@/i18n';
|
|
27
31
|
|
|
28
32
|
type Messages = typeof messages;
|
|
29
33
|
type Namespace = keyof Messages;
|
|
30
34
|
|
|
31
35
|
export function useTranslations<N extends Namespace>(namespace: N) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
return useCallback(
|
|
37
|
+
(key: string, values?: Record<string, string>): string => {
|
|
38
|
+
const ns = messages[namespace] as Record<string, string>;
|
|
39
|
+
let result = ns[key] || `${String(namespace)}.${key}`;
|
|
40
|
+
if (values) {
|
|
41
|
+
for (const [k, v] of Object.entries(values)) {
|
|
42
|
+
result = result.replace(`{${k}}`, v);
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
[namespace]
|
|
48
|
+
);
|
|
42
49
|
}
|
|
43
50
|
<% } %>
|