create-brainerce-store 1.30.0 → 1.31.0
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.31.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/messages/en.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"account": "Account",
|
|
44
44
|
"login": "Login",
|
|
45
45
|
"logout": "Logout",
|
|
46
|
+
"contact": "Contact",
|
|
46
47
|
"searchPlaceholder": "Search products...",
|
|
47
48
|
"categories": "Categories",
|
|
48
49
|
"search": "Search",
|
|
@@ -413,5 +414,21 @@
|
|
|
413
414
|
"coupon": {
|
|
414
415
|
"placeholder": "Coupon code",
|
|
415
416
|
"invalidCode": "Invalid coupon code"
|
|
417
|
+
},
|
|
418
|
+
"contact": {
|
|
419
|
+
"title": "Contact Us",
|
|
420
|
+
"subtitle": "Have a question or feedback? Send us a message and we'll get back to you.",
|
|
421
|
+
"name": "Your name",
|
|
422
|
+
"email": "Email",
|
|
423
|
+
"phone": "Phone",
|
|
424
|
+
"subject": "Subject",
|
|
425
|
+
"message": "Message",
|
|
426
|
+
"optional": "optional",
|
|
427
|
+
"send": "Send message",
|
|
428
|
+
"sending": "Sending...",
|
|
429
|
+
"sendAnother": "Send another message",
|
|
430
|
+
"thanksTitle": "Thank you!",
|
|
431
|
+
"thanksBody": "Your message has been received. We'll reply by email as soon as possible.",
|
|
432
|
+
"genericError": "Something went wrong. Please try again."
|
|
416
433
|
}
|
|
417
434
|
}
|
package/messages/he.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"account": "חשבון",
|
|
44
44
|
"login": "התחברות",
|
|
45
45
|
"logout": "התנתקות",
|
|
46
|
+
"contact": "צור קשר",
|
|
46
47
|
"searchPlaceholder": "חיפוש מוצרים...",
|
|
47
48
|
"categories": "קטגוריות",
|
|
48
49
|
"search": "חיפוש",
|
|
@@ -413,5 +414,21 @@
|
|
|
413
414
|
"coupon": {
|
|
414
415
|
"placeholder": "קוד קופון",
|
|
415
416
|
"invalidCode": "קוד קופון לא תקין"
|
|
417
|
+
},
|
|
418
|
+
"contact": {
|
|
419
|
+
"title": "צור קשר",
|
|
420
|
+
"subtitle": "יש לך שאלה או משוב? שלח לנו הודעה ונחזור אליך בהקדם.",
|
|
421
|
+
"name": "שם מלא",
|
|
422
|
+
"email": "אימייל",
|
|
423
|
+
"phone": "טלפון",
|
|
424
|
+
"subject": "נושא",
|
|
425
|
+
"message": "הודעה",
|
|
426
|
+
"optional": "לא חובה",
|
|
427
|
+
"send": "שליחה",
|
|
428
|
+
"sending": "שולח...",
|
|
429
|
+
"sendAnother": "שליחת הודעה נוספת",
|
|
430
|
+
"thanksTitle": "תודה!",
|
|
431
|
+
"thanksBody": "ההודעה התקבלה. נחזור אליך באימייל בהקדם האפשרי.",
|
|
432
|
+
"genericError": "משהו השתבש. אנא נסה שוב."
|
|
416
433
|
}
|
|
417
434
|
}
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -1,41 +1,47 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Link } from '@/lib/navigation';
|
|
4
|
-
import { useTranslations } from '@/lib/translations';
|
|
5
|
-
import { useStoreInfo, useAuth } from '@/providers/store-provider';
|
|
6
|
-
|
|
7
|
-
export function Footer() {
|
|
8
|
-
const t = useTranslations('common');
|
|
9
|
-
const tn = useTranslations('nav');
|
|
10
|
-
const { storeInfo } = useStoreInfo();
|
|
11
|
-
const { isLoggedIn } = useAuth();
|
|
12
|
-
const year = new Date().getFullYear();
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<footer className="border-border bg-background border-t">
|
|
16
|
-
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
17
|
-
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
|
18
|
-
<p className="text-muted-foreground text-sm">
|
|
19
|
-
{year} {storeInfo?.name || t('store')}. {t('allRightsReserved')}
|
|
20
|
-
</p>
|
|
21
|
-
<nav className="flex items-center gap-4">
|
|
22
|
-
<Link
|
|
23
|
-
href="/products"
|
|
24
|
-
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
25
|
-
>
|
|
26
|
-
{tn('products')}
|
|
27
|
-
</Link>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Link } from '@/lib/navigation';
|
|
4
|
+
import { useTranslations } from '@/lib/translations';
|
|
5
|
+
import { useStoreInfo, useAuth } from '@/providers/store-provider';
|
|
6
|
+
|
|
7
|
+
export function Footer() {
|
|
8
|
+
const t = useTranslations('common');
|
|
9
|
+
const tn = useTranslations('nav');
|
|
10
|
+
const { storeInfo } = useStoreInfo();
|
|
11
|
+
const { isLoggedIn } = useAuth();
|
|
12
|
+
const year = new Date().getFullYear();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<footer className="border-border bg-background border-t">
|
|
16
|
+
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
17
|
+
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
|
18
|
+
<p className="text-muted-foreground text-sm">
|
|
19
|
+
{year} {storeInfo?.name || t('store')}. {t('allRightsReserved')}
|
|
20
|
+
</p>
|
|
21
|
+
<nav className="flex items-center gap-4">
|
|
22
|
+
<Link
|
|
23
|
+
href="/products"
|
|
24
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
25
|
+
>
|
|
26
|
+
{tn('products')}
|
|
27
|
+
</Link>
|
|
28
|
+
<Link
|
|
29
|
+
href="/contact"
|
|
30
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
31
|
+
>
|
|
32
|
+
{tn('contact')}
|
|
33
|
+
</Link>
|
|
34
|
+
{isLoggedIn && (
|
|
35
|
+
<Link
|
|
36
|
+
href="/account"
|
|
37
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
38
|
+
>
|
|
39
|
+
{tn('account')}
|
|
40
|
+
</Link>
|
|
41
|
+
)}
|
|
42
|
+
</nav>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</footer>
|
|
46
|
+
);
|
|
47
|
+
}
|