@pradip1995/segment-login-template 0.4.2 → 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-guest-orders.tsx +41 -12
- package/src/account-theme.ts +52 -1
- 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/register-form.tsx +77 -57
- package/src/segment.tsx +142 -48
package/src/register-form.tsx
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
import GoogleAuthSection from "./google-auth-section"
|
|
10
10
|
import OtpVerificationModal from "./otp-verification-modal"
|
|
11
11
|
import type { OtpVerificationComponent } from "./otp-component"
|
|
12
|
+
import { auth } from "./account-theme"
|
|
13
|
+
import { AuthDivider, AuthField, Ornament } from "./login-form"
|
|
12
14
|
|
|
13
15
|
type VerificationMethod = "email_verification" | "phone_verification"
|
|
14
16
|
type RegistrationStep = "form" | "otp" | "success"
|
|
@@ -37,6 +39,7 @@ export default function RegisterForm({
|
|
|
37
39
|
const [error, setError] = useState<string | null>(null)
|
|
38
40
|
const [pending, setPending] = useState(false)
|
|
39
41
|
const [showOtpModal, setShowOtpModal] = useState(false)
|
|
42
|
+
const [showPassword, setShowPassword] = useState(false)
|
|
40
43
|
|
|
41
44
|
function updateField(key: keyof typeof form, value: string) {
|
|
42
45
|
setForm((prev) => ({ ...prev, [key]: value }))
|
|
@@ -143,21 +146,22 @@ export default function RegisterForm({
|
|
|
143
146
|
verificationMethod === "email_verification" ? form.email : form.phone
|
|
144
147
|
|
|
145
148
|
return (
|
|
146
|
-
<div className="space-y-6">
|
|
149
|
+
<div className="space-y-6" data-testid="register-page">
|
|
150
|
+
<div className="text-center lg:hidden">
|
|
151
|
+
<Ornament />
|
|
152
|
+
<h2 className={auth.mobileTitle}>Create account</h2>
|
|
153
|
+
<p className="mt-2 text-[10px] text-muted uppercase tracking-[0.24em]">
|
|
154
|
+
Join our collection
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
147
158
|
<GoogleAuthSection countryCode={countryCode} />
|
|
148
159
|
|
|
149
|
-
<
|
|
150
|
-
<div className="absolute inset-0 flex items-center">
|
|
151
|
-
<div className="w-full border-t border-cart-border" />
|
|
152
|
-
</div>
|
|
153
|
-
<div className="relative flex justify-center text-xs uppercase tracking-[var(--letter-spacing-nav)]">
|
|
154
|
-
<span className="bg-page-bg px-3 text-muted">Or register with email</span>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
160
|
+
<AuthDivider label="Or register with email" />
|
|
157
161
|
|
|
158
162
|
{step === "form" && (
|
|
159
163
|
<>
|
|
160
|
-
<div className="flex gap-2 p-1 bg-surface-muted
|
|
164
|
+
<div className="flex gap-2 p-1 bg-surface-muted/80">
|
|
161
165
|
<VerificationToggle
|
|
162
166
|
active={verificationMethod === "email_verification"}
|
|
163
167
|
onClick={() => setVerificationMethod("email_verification")}
|
|
@@ -179,39 +183,67 @@ export default function RegisterForm({
|
|
|
179
183
|
</p>
|
|
180
184
|
|
|
181
185
|
<form onSubmit={handleRegister} className="space-y-4">
|
|
182
|
-
<
|
|
186
|
+
<AuthField
|
|
183
187
|
label="Full name"
|
|
184
188
|
value={form.full_name}
|
|
185
189
|
onChange={(e) => updateField("full_name", e.target.value)}
|
|
186
190
|
autoComplete="name"
|
|
191
|
+
placeholder="Your full name"
|
|
187
192
|
/>
|
|
188
|
-
<
|
|
193
|
+
<AuthField
|
|
189
194
|
label="Email"
|
|
190
195
|
type="email"
|
|
191
196
|
value={form.email}
|
|
192
197
|
onChange={(e) => updateField("email", e.target.value)}
|
|
193
198
|
autoComplete="email"
|
|
199
|
+
placeholder="you@example.com"
|
|
194
200
|
/>
|
|
195
|
-
<
|
|
201
|
+
<AuthField
|
|
196
202
|
label="Phone"
|
|
197
203
|
type="tel"
|
|
198
204
|
value={form.phone}
|
|
199
205
|
onChange={(e) => updateField("phone", e.target.value)}
|
|
200
206
|
autoComplete="tel"
|
|
201
207
|
required={verificationMethod === "phone_verification"}
|
|
208
|
+
placeholder="Phone number"
|
|
202
209
|
/>
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
<div>
|
|
211
|
+
<span className={`${auth.fieldLabel} mb-1.5 block`}>Password</span>
|
|
212
|
+
<div className="relative">
|
|
213
|
+
<input
|
|
214
|
+
type={showPassword ? "text" : "password"}
|
|
215
|
+
value={form.password}
|
|
216
|
+
onChange={(e) => updateField("password", e.target.value)}
|
|
217
|
+
autoComplete="new-password"
|
|
218
|
+
required
|
|
219
|
+
placeholder="Create a password"
|
|
220
|
+
className={`${auth.input} pr-10`}
|
|
221
|
+
/>
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={() => setShowPassword((v) => !v)}
|
|
225
|
+
className="absolute inset-y-0 right-0 flex items-center text-muted hover:text-heading transition-colors"
|
|
226
|
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
227
|
+
>
|
|
228
|
+
{showPassword ? <EyeOffIcon /> : <EyeIcon />}
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{error && (
|
|
234
|
+
<p className="text-sm text-brand-sale" role="alert">
|
|
235
|
+
{error}
|
|
236
|
+
</p>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
<button
|
|
240
|
+
type="submit"
|
|
241
|
+
disabled={pending}
|
|
242
|
+
className={`${auth.btnPrimary} ${auth.btnShine}`}
|
|
243
|
+
>
|
|
244
|
+
<span className="relative z-[1]">
|
|
245
|
+
{pending ? "Creating account…" : "Create account"}
|
|
246
|
+
</span>
|
|
215
247
|
</button>
|
|
216
248
|
</form>
|
|
217
249
|
</>
|
|
@@ -225,7 +257,11 @@ export default function RegisterForm({
|
|
|
225
257
|
|
|
226
258
|
<p className="text-sm text-center text-muted">
|
|
227
259
|
Already have an account?{" "}
|
|
228
|
-
<button
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={onLogin}
|
|
263
|
+
className="text-heading font-semibold hover:text-brand-accent transition-colors underline underline-offset-2"
|
|
264
|
+
>
|
|
229
265
|
Sign in
|
|
230
266
|
</button>
|
|
231
267
|
</p>
|
|
@@ -282,43 +318,27 @@ function VerificationToggle({
|
|
|
282
318
|
<button
|
|
283
319
|
type="button"
|
|
284
320
|
onClick={onClick}
|
|
285
|
-
className={
|
|
286
|
-
active ? "bg-page-bg text-heading shadow-sm" : "text-muted hover:text-heading"
|
|
287
|
-
}`}
|
|
321
|
+
className={active ? auth.modeActive : auth.modeIdle}
|
|
288
322
|
>
|
|
289
323
|
{children}
|
|
290
324
|
</button>
|
|
291
325
|
)
|
|
292
326
|
}
|
|
293
327
|
|
|
294
|
-
function
|
|
295
|
-
label,
|
|
296
|
-
type = "text",
|
|
297
|
-
value,
|
|
298
|
-
onChange,
|
|
299
|
-
autoComplete,
|
|
300
|
-
required = true,
|
|
301
|
-
}: {
|
|
302
|
-
label: string
|
|
303
|
-
type?: string
|
|
304
|
-
value: string
|
|
305
|
-
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
306
|
-
autoComplete?: string
|
|
307
|
-
required?: boolean
|
|
308
|
-
}) {
|
|
328
|
+
function EyeIcon() {
|
|
309
329
|
return (
|
|
310
|
-
<
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
/>
|
|
322
|
-
</
|
|
330
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden>
|
|
331
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
332
|
+
<circle cx="12" cy="12" r="3" />
|
|
333
|
+
</svg>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function EyeOffIcon() {
|
|
338
|
+
return (
|
|
339
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden>
|
|
340
|
+
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
|
|
341
|
+
<line x1="1" y1="1" x2="23" y2="23" />
|
|
342
|
+
</svg>
|
|
323
343
|
)
|
|
324
344
|
}
|
package/src/segment.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { useSearchParams } from "next/navigation"
|
|
4
5
|
import type { HttpTypes } from "@medusajs/types"
|
|
5
6
|
import LoginForm from "./login-form"
|
|
6
7
|
import RegisterForm from "./register-form"
|
|
@@ -11,8 +12,12 @@ import AccountAddresses from "./account-addresses"
|
|
|
11
12
|
import AccountOrders from "./account-orders"
|
|
12
13
|
import AccountPaymentMethods from "./account-payment-methods"
|
|
13
14
|
import AccountGuestOrders from "./account-guest-orders"
|
|
15
|
+
import LoginBrandPanel from "./login-brand-panel"
|
|
16
|
+
import GuestOrderModal from "./guest-order-modal"
|
|
14
17
|
import { resolveOtpComponent, type OtpVerificationComponent } from "./otp-component"
|
|
15
18
|
import { accountSectionPath } from "./account-utils"
|
|
19
|
+
import { auth, type BrandPanelConfig } from "./account-theme"
|
|
20
|
+
import { listGuestOrders } from "@pradip1995/commerce-core/data/guest"
|
|
16
21
|
|
|
17
22
|
type AccountProps = {
|
|
18
23
|
countryCode?: string
|
|
@@ -23,6 +28,10 @@ type AccountProps = {
|
|
|
23
28
|
guestOrders?: HttpTypes.StoreOrder[] | { orders?: HttpTypes.StoreOrder[] } | null
|
|
24
29
|
title?: string
|
|
25
30
|
subtitle?: string
|
|
31
|
+
registerTitle?: string
|
|
32
|
+
registerSubtitle?: string
|
|
33
|
+
brandPanel?: BrandPanelConfig | null
|
|
34
|
+
guestTrackLabel?: string
|
|
26
35
|
otpComponent?: OtpVerificationComponent
|
|
27
36
|
}
|
|
28
37
|
|
|
@@ -33,12 +42,53 @@ export default function LoginTemplate({
|
|
|
33
42
|
orders = [],
|
|
34
43
|
paymentDetails = [],
|
|
35
44
|
guestOrders = [],
|
|
36
|
-
title,
|
|
37
|
-
subtitle,
|
|
45
|
+
title = "Sign in",
|
|
46
|
+
subtitle = "Access your exclusive collection",
|
|
47
|
+
registerTitle = "Create account",
|
|
48
|
+
registerSubtitle = "Join our exclusive collection",
|
|
49
|
+
brandPanel,
|
|
50
|
+
guestTrackLabel = "Track guest order",
|
|
38
51
|
otpComponent,
|
|
39
52
|
}: AccountProps) {
|
|
40
53
|
const OtpComponent = resolveOtpComponent(otpComponent)
|
|
41
54
|
const section = accountSectionPath(path)
|
|
55
|
+
const searchParams = useSearchParams()
|
|
56
|
+
|
|
57
|
+
const [view, setView] = useState<"login" | "register" | "forgot">(
|
|
58
|
+
path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
|
|
59
|
+
)
|
|
60
|
+
const [showGuestModal, setShowGuestModal] = useState(false)
|
|
61
|
+
const [checkingGuest, setCheckingGuest] = useState(false)
|
|
62
|
+
const [guestEmail, setGuestEmail] = useState("")
|
|
63
|
+
const [guestAutoStart, setGuestAutoStart] = useState(false)
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const guestView = searchParams.get("view")
|
|
67
|
+
const email = searchParams.get("email")
|
|
68
|
+
if (guestView === "guest") {
|
|
69
|
+
setGuestEmail(email && email !== "undefined" ? email : "")
|
|
70
|
+
setGuestAutoStart(Boolean(email && email !== "undefined"))
|
|
71
|
+
setShowGuestModal(true)
|
|
72
|
+
}
|
|
73
|
+
}, [searchParams])
|
|
74
|
+
|
|
75
|
+
async function handleTrackGuestOrder() {
|
|
76
|
+
// Guest JWT is httpOnly (_medusa_guest_jwt); probe via server action (empty token → cookie).
|
|
77
|
+
setCheckingGuest(true)
|
|
78
|
+
try {
|
|
79
|
+
await listGuestOrders("")
|
|
80
|
+
window.location.href = `/${countryCode}/account/guest-orders`
|
|
81
|
+
return
|
|
82
|
+
} catch {
|
|
83
|
+
// No valid guest session — open OTP modal
|
|
84
|
+
} finally {
|
|
85
|
+
setCheckingGuest(false)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setGuestEmail(searchParams.get("email") || "")
|
|
89
|
+
setGuestAutoStart(false)
|
|
90
|
+
setShowGuestModal(true)
|
|
91
|
+
}
|
|
42
92
|
|
|
43
93
|
if (section === "guest-orders") {
|
|
44
94
|
return <AccountGuestOrders orders={guestOrders} countryCode={countryCode} />
|
|
@@ -61,56 +111,104 @@ export default function LoginTemplate({
|
|
|
61
111
|
case "payment-methods":
|
|
62
112
|
return (
|
|
63
113
|
<AccountPaymentMethods
|
|
64
|
-
paymentDetails={
|
|
114
|
+
paymentDetails={
|
|
115
|
+
paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["paymentDetails"]
|
|
116
|
+
}
|
|
65
117
|
/>
|
|
66
118
|
)
|
|
67
119
|
case "overview":
|
|
68
120
|
default:
|
|
69
|
-
return
|
|
121
|
+
return (
|
|
122
|
+
<AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
|
|
123
|
+
)
|
|
70
124
|
}
|
|
71
125
|
}
|
|
72
126
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
const isSignIn = view === "login"
|
|
128
|
+
const isRegister = view === "register"
|
|
129
|
+
const heading = isSignIn ? title : isRegister ? registerTitle : "Reset password"
|
|
130
|
+
const subheading = isSignIn ? subtitle : isRegister ? registerSubtitle : undefined
|
|
76
131
|
|
|
77
132
|
return (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
133
|
+
<>
|
|
134
|
+
<div data-login-viewport className={auth.viewport}>
|
|
135
|
+
<LoginBrandPanel panel={brandPanel} variant="mobile" />
|
|
136
|
+
<LoginBrandPanel panel={brandPanel} variant="desktop" />
|
|
137
|
+
|
|
138
|
+
<div className={auth.formColumn}>
|
|
139
|
+
<div className={auth.formWash} aria-hidden />
|
|
140
|
+
<div className={auth.formGlow} aria-hidden />
|
|
141
|
+
|
|
142
|
+
<div className={auth.formInner}>
|
|
143
|
+
{(isSignIn || isRegister) && (
|
|
144
|
+
<header className={auth.desktopHeader}>
|
|
145
|
+
<div className="flex items-center gap-3 mb-3">
|
|
146
|
+
<span className="h-px flex-1 max-w-[48px] bg-gradient-to-r from-brand-accent to-brand-primary" />
|
|
147
|
+
<span className="text-brand-accent text-[8px] leading-none">◆</span>
|
|
148
|
+
<span className="h-px flex-1 max-w-[48px] bg-gradient-to-l from-brand-accent to-brand-primary" />
|
|
149
|
+
</div>
|
|
150
|
+
<h1 className={auth.title}>{heading}</h1>
|
|
151
|
+
{subheading && <p className={auth.subtitle}>{subheading}</p>}
|
|
152
|
+
</header>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<div className={auth.formCard}>
|
|
156
|
+
{view !== "forgot" && (
|
|
157
|
+
<div className="flex gap-6 mb-8 border-b border-cart-border">
|
|
158
|
+
<TabButton active={view === "login"} onClick={() => setView("login")}>
|
|
159
|
+
Sign in
|
|
160
|
+
</TabButton>
|
|
161
|
+
<TabButton active={view === "register"} onClick={() => setView("register")}>
|
|
162
|
+
Register
|
|
163
|
+
</TabButton>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{view === "login" && (
|
|
168
|
+
<LoginForm
|
|
169
|
+
countryCode={countryCode}
|
|
170
|
+
OtpComponent={OtpComponent}
|
|
171
|
+
onForgot={() => setView("forgot")}
|
|
172
|
+
onRegister={() => setView("register")}
|
|
173
|
+
onTrackGuest={() => {
|
|
174
|
+
if (checkingGuest) return
|
|
175
|
+
void handleTrackGuestOrder()
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
{view === "register" && (
|
|
180
|
+
<RegisterForm
|
|
181
|
+
countryCode={countryCode}
|
|
182
|
+
OtpComponent={OtpComponent}
|
|
183
|
+
onLogin={() => setView("login")}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
{view === "forgot" && (
|
|
187
|
+
<ForgotPasswordForm onBack={() => setView("login")} />
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{checkingGuest && (
|
|
192
|
+
<p className="mt-4 text-xs text-muted uppercase tracking-[0.2em]">
|
|
193
|
+
{guestTrackLabel}…
|
|
194
|
+
</p>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
94
197
|
</div>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
onLogin={() => setView("login")}
|
|
110
|
-
/>
|
|
111
|
-
)}
|
|
112
|
-
{view === "forgot" && <ForgotPasswordForm onBack={() => setView("login")} />}
|
|
113
|
-
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<GuestOrderModal
|
|
201
|
+
open={showGuestModal}
|
|
202
|
+
onClose={() => {
|
|
203
|
+
setShowGuestModal(false)
|
|
204
|
+
setGuestAutoStart(false)
|
|
205
|
+
}}
|
|
206
|
+
countryCode={countryCode}
|
|
207
|
+
initialEmail={guestEmail}
|
|
208
|
+
autoStart={guestAutoStart}
|
|
209
|
+
OtpComponent={OtpComponent}
|
|
210
|
+
/>
|
|
211
|
+
</>
|
|
114
212
|
)
|
|
115
213
|
}
|
|
116
214
|
|
|
@@ -127,11 +225,7 @@ function TabButton({
|
|
|
127
225
|
<button
|
|
128
226
|
type="button"
|
|
129
227
|
onClick={onClick}
|
|
130
|
-
className={
|
|
131
|
-
active
|
|
132
|
-
? "border-brand-accent text-brand-accent"
|
|
133
|
-
: "border-transparent text-muted hover:text-heading"
|
|
134
|
-
}`}
|
|
228
|
+
className={active ? auth.tabActive : auth.tabIdle}
|
|
135
229
|
>
|
|
136
230
|
{children}
|
|
137
231
|
</button>
|