@pradip1995/segment-login-template 0.4.1 → 0.4.3
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 +1 -1
- package/src/account-addresses.tsx +14 -13
- package/src/account-guest-orders.tsx +47 -12
- package/src/account-orders.tsx +90 -38
- package/src/account-overview.tsx +96 -74
- package/src/account-payment-methods.tsx +155 -63
- package/src/account-profile.tsx +12 -11
- package/src/account-theme.ts +79 -0
- package/src/forgot-password-form.tsx +44 -26
- package/src/google-auth-section.tsx +8 -10
- package/src/guest-order-modal.tsx +224 -0
- package/src/login-brand-panel.tsx +96 -0
- package/src/login-form.tsx +207 -146
- package/src/payment-methods-actions.ts +214 -0
- package/src/register-form.tsx +77 -57
- package/src/segment.tsx +144 -50
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "
|
|
5
|
+
addPaymentMethodAction,
|
|
6
|
+
deletePaymentMethodAction,
|
|
7
|
+
makeDefaultPaymentMethodAction,
|
|
8
|
+
} from "./payment-methods-actions"
|
|
9
|
+
import { acct } from "./account-theme"
|
|
9
10
|
|
|
10
11
|
type PaymentDetail = {
|
|
11
12
|
id: string
|
|
@@ -14,6 +15,19 @@ type PaymentDetail = {
|
|
|
14
15
|
is_default?: boolean
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
const emptyForm = {
|
|
19
|
+
upi_id: "",
|
|
20
|
+
account_holder_name: "",
|
|
21
|
+
bank_name: "",
|
|
22
|
+
account_number: "",
|
|
23
|
+
ifsc: "",
|
|
24
|
+
branch_name: "",
|
|
25
|
+
card_holder_name: "",
|
|
26
|
+
card_number: "",
|
|
27
|
+
expiry_date: "",
|
|
28
|
+
cvv: "",
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export default function AccountPaymentMethods({
|
|
18
32
|
paymentDetails,
|
|
19
33
|
}: {
|
|
@@ -23,92 +37,118 @@ export default function AccountPaymentMethods({
|
|
|
23
37
|
const [type, setType] = useState<"upi" | "bank" | "card">("upi")
|
|
24
38
|
const [pending, setPending] = useState(false)
|
|
25
39
|
const [error, setError] = useState<string | null>(null)
|
|
26
|
-
const [
|
|
27
|
-
|
|
28
|
-
account_holder: "",
|
|
29
|
-
account_number: "",
|
|
30
|
-
ifsc: "",
|
|
31
|
-
card_last4: "",
|
|
32
|
-
})
|
|
40
|
+
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
|
|
41
|
+
const [form, setForm] = useState(emptyForm)
|
|
33
42
|
|
|
34
43
|
async function handleAdd(e: React.FormEvent) {
|
|
35
44
|
e.preventDefault()
|
|
36
45
|
setPending(true)
|
|
37
46
|
setError(null)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
setFieldErrors({})
|
|
48
|
+
|
|
49
|
+
let detail_json: Record<string, string> = {}
|
|
50
|
+
if (type === "upi") {
|
|
51
|
+
detail_json = { upi_id: form.upi_id.trim() }
|
|
52
|
+
} else if (type === "bank") {
|
|
53
|
+
detail_json = {
|
|
54
|
+
account_holder_name: form.account_holder_name.trim(),
|
|
55
|
+
bank_name: form.bank_name.trim(),
|
|
56
|
+
account_number: form.account_number.trim(),
|
|
57
|
+
ifsc: form.ifsc.trim(),
|
|
58
|
+
}
|
|
59
|
+
if (form.branch_name.trim()) {
|
|
60
|
+
detail_json.branch_name = form.branch_name.trim()
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
} else {
|
|
63
|
+
detail_json = {
|
|
64
|
+
card_holder_name: form.card_holder_name.trim(),
|
|
65
|
+
card_number: form.card_number.trim(),
|
|
66
|
+
expiry_date: form.expiry_date.trim(),
|
|
67
|
+
cvv: form.cvv.trim(),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await addPaymentMethodAction(type, detail_json)
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
setError(result.error)
|
|
74
|
+
setFieldErrors(result.field_errors || {})
|
|
56
75
|
setPending(false)
|
|
76
|
+
return
|
|
57
77
|
}
|
|
78
|
+
window.location.reload()
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
async function handleDefault(id: string) {
|
|
61
82
|
setPending(true)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
setError(null)
|
|
84
|
+
setFieldErrors({})
|
|
85
|
+
const result = await makeDefaultPaymentMethodAction(id)
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
setError(result.error)
|
|
66
88
|
setPending(false)
|
|
89
|
+
return
|
|
67
90
|
}
|
|
91
|
+
window.location.reload()
|
|
68
92
|
}
|
|
69
93
|
|
|
70
94
|
async function handleDelete(id: string) {
|
|
71
95
|
if (!confirm("Remove this payment method?")) return
|
|
72
96
|
setPending(true)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
setError(null)
|
|
98
|
+
setFieldErrors({})
|
|
99
|
+
const result = await deletePaymentMethodAction(id)
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
setError(result.error)
|
|
77
102
|
setPending(false)
|
|
103
|
+
return
|
|
78
104
|
}
|
|
105
|
+
window.location.reload()
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
function labelFor(detail: PaymentDetail) {
|
|
82
109
|
const json = detail.detail_json ?? {}
|
|
83
110
|
if (detail.type === "upi") return `UPI · ${json.upi_id || "—"}`
|
|
84
|
-
if (detail.type === "bank")
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
if (detail.type === "bank") {
|
|
112
|
+
const holder = json.account_holder_name || json.account_holder || "—"
|
|
113
|
+
const last4 = json.account_number?.slice(-4) || "****"
|
|
114
|
+
const bank = json.bank_name ? ` · ${json.bank_name}` : ""
|
|
115
|
+
return `Bank · ${holder}${bank} (····${last4})`
|
|
116
|
+
}
|
|
117
|
+
const holder = json.card_holder_name || "—"
|
|
118
|
+
const last4 =
|
|
119
|
+
json.card_number?.replace(/\s+/g, "").slice(-4) ||
|
|
120
|
+
json.card_last4 ||
|
|
121
|
+
"****"
|
|
122
|
+
return `Card · ${holder} (····${last4})`
|
|
87
123
|
}
|
|
88
124
|
|
|
89
125
|
return (
|
|
90
126
|
<div className="space-y-6">
|
|
91
127
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
|
92
128
|
<div>
|
|
93
|
-
<h2 className=
|
|
94
|
-
<p className=
|
|
129
|
+
<h2 className={acct.title}>Payment methods</h2>
|
|
130
|
+
<p className={`text-sm ${acct.muted}`}>Refund destinations for returns and exchanges.</p>
|
|
95
131
|
</div>
|
|
96
|
-
<button type="button" onClick={() => setShowForm((v) => !v)} className=
|
|
132
|
+
<button type="button" onClick={() => setShowForm((v) => !v)} className={acct.btnPrimary}>
|
|
97
133
|
{showForm ? "Cancel" : "Add method"}
|
|
98
134
|
</button>
|
|
99
135
|
</div>
|
|
100
136
|
|
|
101
137
|
{showForm && (
|
|
102
|
-
<form onSubmit={handleAdd} className=
|
|
138
|
+
<form onSubmit={handleAdd} className={`${acct.card} p-6 space-y-4`}>
|
|
103
139
|
<div className="flex flex-wrap gap-2">
|
|
104
140
|
{(["upi", "bank", "card"] as const).map((t) => (
|
|
105
141
|
<button
|
|
106
142
|
key={t}
|
|
107
143
|
type="button"
|
|
108
|
-
onClick={() =>
|
|
109
|
-
|
|
144
|
+
onClick={() => {
|
|
145
|
+
setType(t)
|
|
146
|
+
setError(null)
|
|
147
|
+
setFieldErrors({})
|
|
148
|
+
}}
|
|
149
|
+
className={`px-3 py-1.5 text-xs font-medium rounded-none border ${
|
|
110
150
|
type === t
|
|
111
|
-
? "border-brand-accent text-brand-accent bg-brand-accent
|
|
151
|
+
? "border-brand-accent text-brand-accent bg-brand-accent-muted"
|
|
112
152
|
: "border-cart-border text-muted"
|
|
113
153
|
}`}
|
|
114
154
|
>
|
|
@@ -121,46 +161,87 @@ export default function AccountPaymentMethods({
|
|
|
121
161
|
<Field
|
|
122
162
|
label="UPI ID"
|
|
123
163
|
value={form.upi_id}
|
|
164
|
+
error={fieldErrors.upi_id}
|
|
124
165
|
onChange={(v) => setForm((f) => ({ ...f, upi_id: v }))}
|
|
125
166
|
/>
|
|
126
167
|
)}
|
|
168
|
+
|
|
127
169
|
{type === "bank" && (
|
|
128
170
|
<>
|
|
129
171
|
<Field
|
|
130
|
-
label="Account holder"
|
|
131
|
-
value={form.
|
|
132
|
-
|
|
172
|
+
label="Account holder name"
|
|
173
|
+
value={form.account_holder_name}
|
|
174
|
+
error={fieldErrors.account_holder_name}
|
|
175
|
+
onChange={(v) => setForm((f) => ({ ...f, account_holder_name: v }))}
|
|
176
|
+
/>
|
|
177
|
+
<Field
|
|
178
|
+
label="Bank name"
|
|
179
|
+
value={form.bank_name}
|
|
180
|
+
error={fieldErrors.bank_name}
|
|
181
|
+
onChange={(v) => setForm((f) => ({ ...f, bank_name: v }))}
|
|
133
182
|
/>
|
|
134
183
|
<Field
|
|
135
184
|
label="Account number"
|
|
136
185
|
value={form.account_number}
|
|
186
|
+
error={fieldErrors.account_number}
|
|
137
187
|
onChange={(v) => setForm((f) => ({ ...f, account_number: v }))}
|
|
138
188
|
/>
|
|
139
189
|
<Field
|
|
140
190
|
label="IFSC"
|
|
141
191
|
value={form.ifsc}
|
|
192
|
+
error={fieldErrors.ifsc}
|
|
142
193
|
onChange={(v) => setForm((f) => ({ ...f, ifsc: v }))}
|
|
143
194
|
/>
|
|
195
|
+
<Field
|
|
196
|
+
label="Branch name (optional)"
|
|
197
|
+
value={form.branch_name}
|
|
198
|
+
required={false}
|
|
199
|
+
error={fieldErrors.branch_name}
|
|
200
|
+
onChange={(v) => setForm((f) => ({ ...f, branch_name: v }))}
|
|
201
|
+
/>
|
|
144
202
|
</>
|
|
145
203
|
)}
|
|
204
|
+
|
|
146
205
|
{type === "card" && (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
206
|
+
<>
|
|
207
|
+
<Field
|
|
208
|
+
label="Card holder name"
|
|
209
|
+
value={form.card_holder_name}
|
|
210
|
+
error={fieldErrors.card_holder_name}
|
|
211
|
+
onChange={(v) => setForm((f) => ({ ...f, card_holder_name: v }))}
|
|
212
|
+
/>
|
|
213
|
+
<Field
|
|
214
|
+
label="Card number"
|
|
215
|
+
value={form.card_number}
|
|
216
|
+
error={fieldErrors.card_number}
|
|
217
|
+
onChange={(v) => setForm((f) => ({ ...f, card_number: v }))}
|
|
218
|
+
/>
|
|
219
|
+
<Field
|
|
220
|
+
label="Expiry (MM/YY)"
|
|
221
|
+
value={form.expiry_date}
|
|
222
|
+
error={fieldErrors.expiry_date}
|
|
223
|
+
placeholder="08/28"
|
|
224
|
+
onChange={(v) => setForm((f) => ({ ...f, expiry_date: v }))}
|
|
225
|
+
/>
|
|
226
|
+
<Field
|
|
227
|
+
label="CVV"
|
|
228
|
+
value={form.cvv}
|
|
229
|
+
error={fieldErrors.cvv}
|
|
230
|
+
onChange={(v) => setForm((f) => ({ ...f, cvv: v }))}
|
|
231
|
+
/>
|
|
232
|
+
</>
|
|
152
233
|
)}
|
|
153
234
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<button type="submit" disabled={pending} className="btn-primary text-sm disabled:opacity-60">
|
|
235
|
+
<button type="submit" disabled={pending} className={acct.btnPrimary}>
|
|
157
236
|
{pending ? "Saving…" : "Save payment method"}
|
|
158
237
|
</button>
|
|
159
238
|
</form>
|
|
160
239
|
)}
|
|
161
240
|
|
|
241
|
+
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
242
|
+
|
|
162
243
|
{paymentDetails.length === 0 ? (
|
|
163
|
-
<div className=
|
|
244
|
+
<div className={`${acct.cardMuted} p-8 text-center text-sm ${acct.muted}`}>
|
|
164
245
|
No refund payment methods saved.
|
|
165
246
|
</div>
|
|
166
247
|
) : (
|
|
@@ -168,7 +249,7 @@ export default function AccountPaymentMethods({
|
|
|
168
249
|
{paymentDetails.map((detail) => (
|
|
169
250
|
<li
|
|
170
251
|
key={detail.id}
|
|
171
|
-
className=
|
|
252
|
+
className={`${acct.card} p-5 flex flex-wrap justify-between gap-4 items-center`}
|
|
172
253
|
>
|
|
173
254
|
<div>
|
|
174
255
|
<p className="font-medium text-heading text-sm">{labelFor(detail)}</p>
|
|
@@ -191,7 +272,7 @@ export default function AccountPaymentMethods({
|
|
|
191
272
|
type="button"
|
|
192
273
|
onClick={() => handleDelete(detail.id)}
|
|
193
274
|
disabled={pending}
|
|
194
|
-
className="text-
|
|
275
|
+
className="text-red-600 hover:underline disabled:opacity-60"
|
|
195
276
|
>
|
|
196
277
|
Remove
|
|
197
278
|
</button>
|
|
@@ -208,22 +289,33 @@ function Field({
|
|
|
208
289
|
label,
|
|
209
290
|
value,
|
|
210
291
|
onChange,
|
|
292
|
+
error,
|
|
293
|
+
required = true,
|
|
294
|
+
placeholder,
|
|
211
295
|
}: {
|
|
212
296
|
label: string
|
|
213
297
|
value: string
|
|
214
298
|
onChange: (value: string) => void
|
|
299
|
+
error?: string
|
|
300
|
+
required?: boolean
|
|
301
|
+
placeholder?: string
|
|
215
302
|
}) {
|
|
216
303
|
return (
|
|
217
304
|
<label className="block">
|
|
218
|
-
<span className="text-xs font-semibold uppercase tracking-
|
|
305
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted mb-1.5 block">
|
|
219
306
|
{label}
|
|
220
307
|
</span>
|
|
221
308
|
<input
|
|
222
309
|
value={value}
|
|
223
310
|
onChange={(e) => onChange(e.target.value)}
|
|
224
|
-
required
|
|
225
|
-
|
|
311
|
+
required={required}
|
|
312
|
+
placeholder={placeholder}
|
|
313
|
+
aria-invalid={Boolean(error)}
|
|
314
|
+
className={`w-full border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent ${
|
|
315
|
+
error ? "border-red-500" : "border-cart-border"
|
|
316
|
+
}`}
|
|
226
317
|
/>
|
|
318
|
+
{error && <span className="mt-1 block text-xs text-red-600">{error}</span>}
|
|
227
319
|
</label>
|
|
228
320
|
)
|
|
229
321
|
}
|
package/src/account-profile.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./auth-server"
|
|
10
10
|
import type { OtpVerificationComponent } from "./otp-component"
|
|
11
11
|
import OtpVerificationModal from "./otp-verification-modal"
|
|
12
|
+
import { acct } from "./account-theme"
|
|
12
13
|
|
|
13
14
|
export default function AccountProfile({
|
|
14
15
|
customer,
|
|
@@ -94,28 +95,28 @@ export default function AccountProfile({
|
|
|
94
95
|
return (
|
|
95
96
|
<div className="space-y-8">
|
|
96
97
|
<div>
|
|
97
|
-
<h2 className=
|
|
98
|
-
<p className=
|
|
98
|
+
<h2 className={acct.title}>Account details</h2>
|
|
99
|
+
<p className={`text-sm ${acct.muted}`}>Update your name and contact information.</p>
|
|
99
100
|
</div>
|
|
100
101
|
|
|
101
|
-
<form action={formAction} className=
|
|
102
|
+
<form action={formAction} className={`${acct.card} p-6 space-y-4`}>
|
|
102
103
|
<Field label="Full name" name="full_name" defaultValue={fullName} />
|
|
103
104
|
<div>
|
|
104
|
-
<label className="block text-xs font-semibold uppercase tracking-
|
|
105
|
+
<label className="block text-xs font-semibold uppercase tracking-wider text-muted mb-1.5">
|
|
105
106
|
Email
|
|
106
107
|
</label>
|
|
107
108
|
<div className="flex flex-wrap items-center gap-3">
|
|
108
109
|
<input
|
|
109
110
|
value={customer.email}
|
|
110
111
|
readOnly
|
|
111
|
-
className="flex-1 min-w-[200px] border border-cart-border rounded px-3 py-2.5 text-sm bg-surface-muted text-muted"
|
|
112
|
+
className="flex-1 min-w-[200px] border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface-muted text-muted"
|
|
112
113
|
/>
|
|
113
114
|
{!emailVerified && (
|
|
114
115
|
<button
|
|
115
116
|
type="button"
|
|
116
117
|
onClick={() => sendVerifyOtp("email_verification")}
|
|
117
118
|
disabled={otpPending}
|
|
118
|
-
className=
|
|
119
|
+
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
119
120
|
>
|
|
120
121
|
Verify email
|
|
121
122
|
</button>
|
|
@@ -133,7 +134,7 @@ export default function AccountProfile({
|
|
|
133
134
|
type="button"
|
|
134
135
|
onClick={() => sendVerifyOtp("phone_verification")}
|
|
135
136
|
disabled={otpPending}
|
|
136
|
-
className=
|
|
137
|
+
className={`${acct.btnOutline} !py-2 !px-3`}
|
|
137
138
|
>
|
|
138
139
|
Verify phone
|
|
139
140
|
</button>
|
|
@@ -145,12 +146,12 @@ export default function AccountProfile({
|
|
|
145
146
|
</div>
|
|
146
147
|
|
|
147
148
|
{(state.error || message) && (
|
|
148
|
-
<p className={`text-sm ${state.ok || message ? "text-green-600" : "text-
|
|
149
|
+
<p className={`text-sm ${state.ok || message ? "text-green-600" : "text-red-600"}`}>
|
|
149
150
|
{message || state.error}
|
|
150
151
|
</p>
|
|
151
152
|
)}
|
|
152
153
|
|
|
153
|
-
<button type="submit" disabled={pending} className=
|
|
154
|
+
<button type="submit" disabled={pending} className={acct.btnPrimary}>
|
|
154
155
|
{pending ? "Saving…" : "Save changes"}
|
|
155
156
|
</button>
|
|
156
157
|
</form>
|
|
@@ -184,14 +185,14 @@ function Field({
|
|
|
184
185
|
}) {
|
|
185
186
|
return (
|
|
186
187
|
<label className="block">
|
|
187
|
-
<span className="text-xs font-semibold uppercase tracking-
|
|
188
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-muted mb-1.5 block">
|
|
188
189
|
{label}
|
|
189
190
|
</span>
|
|
190
191
|
<input
|
|
191
192
|
name={name}
|
|
192
193
|
type={type}
|
|
193
194
|
defaultValue={defaultValue}
|
|
194
|
-
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
195
|
+
className="w-full border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent"
|
|
195
196
|
/>
|
|
196
197
|
</label>
|
|
197
198
|
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Logged-in account visual tokens — maps to segment-tokens CSS variables */
|
|
2
|
+
export const acct = {
|
|
3
|
+
pageBg: "bg-page-bg",
|
|
4
|
+
card: "bg-surface border border-cart-border rounded-none shadow-card",
|
|
5
|
+
cardMuted: "bg-surface-muted border border-cart-border rounded-none",
|
|
6
|
+
title:
|
|
7
|
+
"font-heading text-2xl sm:text-3xl font-extrabold tracking-tight text-heading",
|
|
8
|
+
sectionTitle:
|
|
9
|
+
"text-base sm:text-lg font-bold uppercase tracking-wider text-heading",
|
|
10
|
+
subsection: "text-sm font-semibold uppercase tracking-wider text-muted",
|
|
11
|
+
accent: "text-brand-accent",
|
|
12
|
+
muted: "text-muted",
|
|
13
|
+
heading: "text-heading",
|
|
14
|
+
body: "text-sm text-body",
|
|
15
|
+
border: "border-cart-border",
|
|
16
|
+
btnPrimary:
|
|
17
|
+
"inline-flex items-center justify-center bg-brand-primary hover:bg-brand-accent text-inverse rounded-none px-6 py-3 font-bold text-xs uppercase tracking-widest transition-all disabled:opacity-50",
|
|
18
|
+
btnAccent:
|
|
19
|
+
"inline-flex items-center justify-center bg-brand-accent hover:bg-brand-accent-hover text-inverse rounded-none px-6 py-3 font-bold text-xs uppercase tracking-widest transition-all disabled:opacity-50",
|
|
20
|
+
btnOutline:
|
|
21
|
+
"inline-flex items-center justify-center border border-cart-border text-brand-accent rounded-none px-6 py-3 font-bold text-xs uppercase tracking-widest hover:bg-surface-muted transition-all disabled:opacity-50",
|
|
22
|
+
input:
|
|
23
|
+
"w-full border border-cart-border rounded-none px-3 py-2.5 text-sm bg-surface text-heading focus:outline-none focus:border-brand-accent",
|
|
24
|
+
label:
|
|
25
|
+
"block text-xs font-semibold uppercase tracking-wider text-muted mb-1.5",
|
|
26
|
+
badge:
|
|
27
|
+
"inline-flex items-center px-3 py-1 text-xs font-semibold border rounded-none",
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
/** Auth / login visual tokens — maps to segment-tokens CSS variables (theme-safe) */
|
|
31
|
+
export const auth = {
|
|
32
|
+
viewport:
|
|
33
|
+
"w-full flex flex-col lg:flex-row overflow-hidden min-h-[calc(100svh-6.25rem)] lg:h-[calc(100svh-6.25rem)] lg:max-h-[calc(100svh-6.25rem)] bg-page-bg",
|
|
34
|
+
formColumn: "relative flex flex-1 flex-col min-h-0 w-full lg:w-1/2 bg-page-bg",
|
|
35
|
+
formWash:
|
|
36
|
+
"pointer-events-none absolute inset-y-0 -left-20 w-40 bg-gradient-to-r from-transparent to-page-bg hidden lg:block z-[1]",
|
|
37
|
+
formGlow:
|
|
38
|
+
"pointer-events-none absolute inset-0 bg-gradient-to-l from-brand-primary/[0.03] via-transparent to-transparent hidden lg:block",
|
|
39
|
+
formInner:
|
|
40
|
+
"relative z-[2] flex flex-1 flex-col justify-center min-h-0 overflow-y-auto overscroll-contain px-5 sm:px-10 md:px-12 lg:px-14 xl:px-20 py-5 sm:py-6 lg:py-8",
|
|
41
|
+
formCard:
|
|
42
|
+
"relative w-full max-w-[440px] shrink-0 border border-cart-border/60 bg-surface shadow-[0_10px_40px_-16px_rgba(0,0,0,0.12)] p-7 sm:p-8",
|
|
43
|
+
desktopHeader: "hidden lg:block mb-6 max-w-[440px] shrink-0",
|
|
44
|
+
title:
|
|
45
|
+
"font-heading text-2xl xl:text-[32px] font-bold text-heading uppercase tracking-[0.08em] leading-tight",
|
|
46
|
+
subtitle: "mt-2 text-[11px] font-medium text-muted uppercase tracking-[0.28em]",
|
|
47
|
+
mobileTitle:
|
|
48
|
+
"font-heading text-base font-bold text-heading uppercase tracking-[0.16em] lg:hidden",
|
|
49
|
+
fieldLabel:
|
|
50
|
+
"text-[10px] font-semibold text-heading/80 uppercase tracking-[0.22em]",
|
|
51
|
+
input:
|
|
52
|
+
"w-full px-0 py-3 text-sm text-heading bg-transparent border-0 border-b-2 border-cart-border rounded-none placeholder:text-muted/50 focus:outline-none focus:border-brand-primary focus:ring-0 transition-colors duration-200",
|
|
53
|
+
btnPrimary:
|
|
54
|
+
"group relative w-full py-3.5 bg-brand-primary text-inverse text-[11px] font-bold uppercase tracking-[0.28em] shadow-[0_8px_28px_-6px_rgba(0,0,0,0.25)] hover:bg-brand-accent hover:shadow-[0_12px_32px_-6px_rgba(0,0,0,0.3)] transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed overflow-hidden active:scale-[0.98]",
|
|
55
|
+
btnShine:
|
|
56
|
+
"before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/15 before:to-transparent before:-translate-x-full hover:before:translate-x-full before:transition-transform before:duration-700",
|
|
57
|
+
btnOutline:
|
|
58
|
+
"w-full flex items-center justify-center gap-2.5 py-3 border border-brand-primary/15 bg-surface/80 hover:bg-page-bg hover:border-brand-primary/35 shadow-sm transition-all duration-200 disabled:opacity-50",
|
|
59
|
+
guestCta:
|
|
60
|
+
"w-full py-3 border border-brand-primary/20 bg-gradient-to-r from-surface to-page-bg text-heading text-[10px] font-bold uppercase tracking-[0.18em] hover:border-brand-accent hover:shadow-[0_4px_16px_-4px_rgba(0,0,0,0.15)] transition-all duration-300 flex items-center justify-center gap-2 disabled:opacity-50 active:scale-[0.98]",
|
|
61
|
+
tabActive:
|
|
62
|
+
"pb-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] border-b-2 -mb-px border-brand-accent text-brand-accent transition-colors duration-200",
|
|
63
|
+
tabIdle:
|
|
64
|
+
"pb-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] border-b-2 -mb-px border-transparent text-muted hover:text-heading transition-colors duration-200",
|
|
65
|
+
modeActive:
|
|
66
|
+
"flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md bg-surface text-heading shadow-sm transition-colors duration-200",
|
|
67
|
+
modeIdle:
|
|
68
|
+
"flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md text-muted hover:text-heading transition-colors duration-200",
|
|
69
|
+
chipActive:
|
|
70
|
+
"px-3 py-1.5 text-xs font-medium border border-brand-accent text-brand-accent bg-brand-accent/5 transition-colors duration-200",
|
|
71
|
+
chipIdle:
|
|
72
|
+
"px-3 py-1.5 text-xs font-medium border border-cart-border text-muted hover:text-heading transition-colors duration-200",
|
|
73
|
+
} as const
|
|
74
|
+
|
|
75
|
+
export type BrandPanelConfig = {
|
|
76
|
+
image?: string | null
|
|
77
|
+
eyebrow?: string | null
|
|
78
|
+
headline?: string | null
|
|
79
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
import { checkEmailRegistered, requestPasswordReset } from "./auth-server"
|
|
5
|
+
import { auth } from "./account-theme"
|
|
6
|
+
import { AuthField, Ornament } from "./login-form"
|
|
5
7
|
|
|
6
8
|
export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
|
|
7
9
|
const [email, setEmail] = useState("")
|
|
@@ -42,13 +44,17 @@ export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
|
|
|
42
44
|
|
|
43
45
|
if (success) {
|
|
44
46
|
return (
|
|
45
|
-
<div className="space-y-
|
|
46
|
-
<
|
|
47
|
-
|
|
47
|
+
<div className="space-y-5 text-center py-4">
|
|
48
|
+
<Ornament />
|
|
49
|
+
<div className="w-14 h-14 mx-auto rounded-full bg-brand-accent/10 flex items-center justify-center text-brand-accent">
|
|
50
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" aria-hidden>
|
|
51
|
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
|
|
52
|
+
<polyline points="22,6 12,13 2,6" />
|
|
53
|
+
</svg>
|
|
48
54
|
</div>
|
|
49
|
-
<h2 className=
|
|
55
|
+
<h2 className={`${auth.title} text-xl`}>Check your email</h2>
|
|
50
56
|
<p className="text-sm text-muted">{message}</p>
|
|
51
|
-
<button type="button" onClick={onBack} className=
|
|
57
|
+
<button type="button" onClick={onBack} className={auth.btnOutline}>
|
|
52
58
|
Back to sign in
|
|
53
59
|
</button>
|
|
54
60
|
</div>
|
|
@@ -56,31 +62,43 @@ export default function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
|
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
return (
|
|
59
|
-
<form onSubmit={handleSubmit} className="space-y-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
66
|
+
<div className="text-center lg:text-left">
|
|
67
|
+
<Ornament />
|
|
68
|
+
<h2 className={`${auth.title} text-xl lg:text-2xl`}>Reset password</h2>
|
|
69
|
+
<p className="mt-2 text-sm text-muted">
|
|
70
|
+
Enter your email and we'll send a link to reset your password.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<AuthField
|
|
75
|
+
label="Email"
|
|
76
|
+
name="email"
|
|
77
|
+
type="email"
|
|
78
|
+
value={email}
|
|
79
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
80
|
+
autoComplete="email"
|
|
81
|
+
placeholder="you@example.com"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
{error && (
|
|
85
|
+
<p className="text-sm text-brand-sale" role="alert">
|
|
86
|
+
{error}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<button
|
|
91
|
+
type="submit"
|
|
92
|
+
disabled={pending}
|
|
93
|
+
className={`${auth.btnPrimary} ${auth.btnShine}`}
|
|
94
|
+
>
|
|
95
|
+
<span className="relative z-[1]">{pending ? "Sending…" : "Send reset link"}</span>
|
|
79
96
|
</button>
|
|
97
|
+
|
|
80
98
|
<button
|
|
81
99
|
type="button"
|
|
82
100
|
onClick={onBack}
|
|
83
|
-
className="text-sm text-muted hover:text-brand-accent w-full text-center"
|
|
101
|
+
className="text-sm text-muted hover:text-brand-accent w-full text-center transition-colors"
|
|
84
102
|
>
|
|
85
103
|
Back to sign in
|
|
86
104
|
</button>
|
|
@@ -6,9 +6,7 @@ import {
|
|
|
6
6
|
GOOGLE_LOGIN_COUNTRY_CODE_KEY,
|
|
7
7
|
} from "@pradip1995/commerce-auth/util/google-auth-client"
|
|
8
8
|
import { initiateGoogleAuth } from "./auth-server"
|
|
9
|
-
|
|
10
|
-
const BUTTON_CLASS =
|
|
11
|
-
"btn-outline w-full flex items-center justify-center gap-2.5 py-3 px-4"
|
|
9
|
+
import { auth } from "./account-theme"
|
|
12
10
|
|
|
13
11
|
export default function GoogleAuthSection({ countryCode }: { countryCode: string }) {
|
|
14
12
|
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim()
|
|
@@ -46,9 +44,9 @@ export default function GoogleAuthSection({ countryCode }: { countryCode: string
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
return (
|
|
49
|
-
<div
|
|
47
|
+
<div>
|
|
50
48
|
{error && (
|
|
51
|
-
<p className="mb-3 text-sm text-
|
|
49
|
+
<p className="mb-3 text-sm text-brand-sale" role="alert">
|
|
52
50
|
{error}
|
|
53
51
|
</p>
|
|
54
52
|
)}
|
|
@@ -56,10 +54,10 @@ export default function GoogleAuthSection({ countryCode }: { countryCode: string
|
|
|
56
54
|
type="button"
|
|
57
55
|
onClick={handleClick}
|
|
58
56
|
disabled={loading}
|
|
59
|
-
className={
|
|
60
|
-
|
|
57
|
+
className={auth.btnOutline}
|
|
58
|
+
aria-label="Continue with Google"
|
|
61
59
|
>
|
|
62
|
-
<svg width="
|
|
60
|
+
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
|
|
63
61
|
<path
|
|
64
62
|
fill="#4285F4"
|
|
65
63
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
@@ -77,8 +75,8 @@ export default function GoogleAuthSection({ countryCode }: { countryCode: string
|
|
|
77
75
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
78
76
|
/>
|
|
79
77
|
</svg>
|
|
80
|
-
<span className="text-
|
|
81
|
-
{loading ? "
|
|
78
|
+
<span className="text-xs text-heading font-medium tracking-wide">
|
|
79
|
+
{loading ? "Redirecting…" : "Continue with Google"}
|
|
82
80
|
</span>
|
|
83
81
|
</button>
|
|
84
82
|
</div>
|