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.
@@ -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
+ }