create-brainerce-store 1.30.0 → 1.31.2
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 +2 -2
- package/messages/en.json +434 -417
- package/messages/he.json +434 -417
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +242 -242
- package/templates/nextjs/base/src/app/contact/page.tsx +211 -0
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +566 -566
- package/templates/nextjs/base/src/components/account/order-customizations.tsx +125 -125
- package/templates/nextjs/base/src/components/account/order-history.tsx +367 -367
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +58 -58
- package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +79 -79
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +66 -66
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +730 -730
- package/templates/nextjs/base/src/components/layout/footer.tsx +6 -0
- package/templates/nextjs/base/src/components/products/customization-fields.tsx +478 -478
- package/templates/nextjs/base/src/lib/safe-redirect.ts +60 -60
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { getClient } from '@/lib/brainerce';
|
|
5
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
6
|
+
import { useTranslations } from '@/lib/translations';
|
|
7
|
+
|
|
8
|
+
export default function ContactPage() {
|
|
9
|
+
const t = useTranslations('contact');
|
|
10
|
+
const [form, setForm] = useState({
|
|
11
|
+
name: '',
|
|
12
|
+
email: '',
|
|
13
|
+
phone: '',
|
|
14
|
+
subject: '',
|
|
15
|
+
message: '',
|
|
16
|
+
honeypot: '',
|
|
17
|
+
});
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [sent, setSent] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
if (loading) return;
|
|
25
|
+
|
|
26
|
+
// Honeypot — silently drop anything that auto-filled this hidden field.
|
|
27
|
+
if (form.honeypot.trim().length > 0) {
|
|
28
|
+
setSent(true);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
setLoading(true);
|
|
34
|
+
setError(null);
|
|
35
|
+
const client = getClient();
|
|
36
|
+
await client.createInquiry({
|
|
37
|
+
name: form.name.trim(),
|
|
38
|
+
email: form.email.trim(),
|
|
39
|
+
subject: form.subject.trim(),
|
|
40
|
+
message: form.message.trim(),
|
|
41
|
+
phone: form.phone.trim() || undefined,
|
|
42
|
+
});
|
|
43
|
+
setSent(true);
|
|
44
|
+
setForm({ name: '', email: '', phone: '', subject: '', message: '', honeypot: '' });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : t('genericError');
|
|
47
|
+
setError(message);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const inputClass =
|
|
54
|
+
'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';
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="mx-auto max-w-2xl px-4 py-12 sm:px-6 lg:px-8">
|
|
58
|
+
<div className="mb-8 text-center">
|
|
59
|
+
<h1 className="text-foreground text-3xl font-bold">{t('title')}</h1>
|
|
60
|
+
<p className="text-muted-foreground mt-2 text-sm">{t('subtitle')}</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{error && (
|
|
64
|
+
<div className="bg-destructive/10 border-destructive/20 text-destructive mb-6 rounded-lg border px-4 py-3 text-sm">
|
|
65
|
+
{error}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{sent ? (
|
|
70
|
+
<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">
|
|
71
|
+
<p className="font-medium">{t('thanksTitle')}</p>
|
|
72
|
+
<p className="mt-1">{t('thanksBody')}</p>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onClick={() => setSent(false)}
|
|
76
|
+
className="text-primary mt-4 text-sm font-medium hover:underline"
|
|
77
|
+
>
|
|
78
|
+
{t('sendAnother')}
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
|
|
83
|
+
{/* Honeypot — kept off-screen + aria-hidden; real users never touch it. */}
|
|
84
|
+
<div
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
className="pointer-events-none absolute -start-[10000px] h-0 w-0 overflow-hidden"
|
|
87
|
+
>
|
|
88
|
+
<label htmlFor="contact-honeypot">Leave this field empty</label>
|
|
89
|
+
<input
|
|
90
|
+
id="contact-honeypot"
|
|
91
|
+
type="text"
|
|
92
|
+
tabIndex={-1}
|
|
93
|
+
autoComplete="off"
|
|
94
|
+
value={form.honeypot}
|
|
95
|
+
onChange={(e) => setForm({ ...form, honeypot: e.target.value })}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
100
|
+
<div>
|
|
101
|
+
<label
|
|
102
|
+
htmlFor="contact-name"
|
|
103
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
104
|
+
>
|
|
105
|
+
{t('name')}
|
|
106
|
+
</label>
|
|
107
|
+
<input
|
|
108
|
+
id="contact-name"
|
|
109
|
+
type="text"
|
|
110
|
+
required
|
|
111
|
+
maxLength={120}
|
|
112
|
+
autoComplete="name"
|
|
113
|
+
value={form.name}
|
|
114
|
+
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
|
115
|
+
className={inputClass}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
<div>
|
|
119
|
+
<label
|
|
120
|
+
htmlFor="contact-email"
|
|
121
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
122
|
+
>
|
|
123
|
+
{t('email')}
|
|
124
|
+
</label>
|
|
125
|
+
<input
|
|
126
|
+
id="contact-email"
|
|
127
|
+
type="email"
|
|
128
|
+
required
|
|
129
|
+
autoComplete="email"
|
|
130
|
+
value={form.email}
|
|
131
|
+
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
132
|
+
className={inputClass}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div>
|
|
138
|
+
<label
|
|
139
|
+
htmlFor="contact-phone"
|
|
140
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
141
|
+
>
|
|
142
|
+
{t('phone')} <span className="text-muted-foreground">({t('optional')})</span>
|
|
143
|
+
</label>
|
|
144
|
+
<input
|
|
145
|
+
id="contact-phone"
|
|
146
|
+
type="tel"
|
|
147
|
+
autoComplete="tel"
|
|
148
|
+
value={form.phone}
|
|
149
|
+
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
|
150
|
+
className={inputClass}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div>
|
|
155
|
+
<label
|
|
156
|
+
htmlFor="contact-subject"
|
|
157
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
158
|
+
>
|
|
159
|
+
{t('subject')}
|
|
160
|
+
</label>
|
|
161
|
+
<input
|
|
162
|
+
id="contact-subject"
|
|
163
|
+
type="text"
|
|
164
|
+
required
|
|
165
|
+
maxLength={200}
|
|
166
|
+
value={form.subject}
|
|
167
|
+
onChange={(e) => setForm({ ...form, subject: e.target.value })}
|
|
168
|
+
className={inputClass}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div>
|
|
173
|
+
<label
|
|
174
|
+
htmlFor="contact-message"
|
|
175
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
176
|
+
>
|
|
177
|
+
{t('message')}
|
|
178
|
+
</label>
|
|
179
|
+
<textarea
|
|
180
|
+
id="contact-message"
|
|
181
|
+
required
|
|
182
|
+
maxLength={10000}
|
|
183
|
+
rows={6}
|
|
184
|
+
value={form.message}
|
|
185
|
+
onChange={(e) => setForm({ ...form, message: e.target.value })}
|
|
186
|
+
className="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"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<button
|
|
191
|
+
type="submit"
|
|
192
|
+
disabled={loading}
|
|
193
|
+
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"
|
|
194
|
+
>
|
|
195
|
+
{loading ? (
|
|
196
|
+
<>
|
|
197
|
+
<LoadingSpinner
|
|
198
|
+
size="sm"
|
|
199
|
+
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
200
|
+
/>
|
|
201
|
+
{t('sending')}
|
|
202
|
+
</>
|
|
203
|
+
) : (
|
|
204
|
+
t('send')
|
|
205
|
+
)}
|
|
206
|
+
</button>
|
|
207
|
+
</form>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|