@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react"
|
|
4
|
+
import { sendOTP, verifyOTP } from "./auth-server"
|
|
5
|
+
import OtpVerificationModal from "./otp-verification-modal"
|
|
6
|
+
import type { OtpVerificationComponent } from "./otp-component"
|
|
7
|
+
import { auth } from "./account-theme"
|
|
8
|
+
|
|
9
|
+
type GuestOrderModalProps = {
|
|
10
|
+
open: boolean
|
|
11
|
+
onClose: () => void
|
|
12
|
+
countryCode: string
|
|
13
|
+
initialEmail?: string
|
|
14
|
+
autoStart?: boolean
|
|
15
|
+
OtpComponent?: OtpVerificationComponent
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function GuestOrderModal({
|
|
19
|
+
open,
|
|
20
|
+
onClose,
|
|
21
|
+
countryCode,
|
|
22
|
+
initialEmail = "",
|
|
23
|
+
autoStart = false,
|
|
24
|
+
OtpComponent = OtpVerificationModal,
|
|
25
|
+
}: GuestOrderModalProps) {
|
|
26
|
+
const [email, setEmail] = useState(initialEmail)
|
|
27
|
+
const [otp, setOtp] = useState("")
|
|
28
|
+
const [otpToken, setOtpToken] = useState<string | null>(null)
|
|
29
|
+
const [step, setStep] = useState<"email" | "otp">("email")
|
|
30
|
+
const [pending, setPending] = useState(false)
|
|
31
|
+
const [error, setError] = useState<string | null>(null)
|
|
32
|
+
const [success, setSuccess] = useState(false)
|
|
33
|
+
const autoStartedRef = useRef(false)
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (initialEmail) setEmail(initialEmail)
|
|
37
|
+
}, [initialEmail])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (open && autoStart && initialEmail && !autoStartedRef.current) {
|
|
41
|
+
autoStartedRef.current = true
|
|
42
|
+
void handleSendOtp(initialEmail)
|
|
43
|
+
}
|
|
44
|
+
if (!open) {
|
|
45
|
+
autoStartedRef.current = false
|
|
46
|
+
}
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
}, [open, autoStart, initialEmail])
|
|
49
|
+
|
|
50
|
+
async function handleSendOtp(emailToSend = email) {
|
|
51
|
+
if (!emailToSend.trim()) return
|
|
52
|
+
setPending(true)
|
|
53
|
+
setError(null)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const res = await sendOTP(emailToSend.trim())
|
|
57
|
+
if (!res.success && (res as { error?: string }).error) {
|
|
58
|
+
throw new Error((res as { error?: string }).error)
|
|
59
|
+
}
|
|
60
|
+
setOtpToken((res as { token?: string }).token || "guest")
|
|
61
|
+
setOtp("")
|
|
62
|
+
setStep("otp")
|
|
63
|
+
} catch (err) {
|
|
64
|
+
setError(err instanceof Error ? err.message : "Failed to send code")
|
|
65
|
+
setStep("email")
|
|
66
|
+
} finally {
|
|
67
|
+
setPending(false)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function handleVerifyOtp() {
|
|
72
|
+
if (!otp || otp.length < 6) return
|
|
73
|
+
setPending(true)
|
|
74
|
+
setError(null)
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const res = await verifyOTP(email.trim(), otpToken || "", otp)
|
|
78
|
+
if (!res.success) {
|
|
79
|
+
throw new Error((res as { error?: string }).error || "Invalid code")
|
|
80
|
+
}
|
|
81
|
+
// verifyOTP sets httpOnly `_medusa_guest_jwt` via the server action
|
|
82
|
+
setSuccess(true)
|
|
83
|
+
window.setTimeout(() => {
|
|
84
|
+
window.location.href = `/${countryCode}/account/guest-orders`
|
|
85
|
+
}, 900)
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setError(err instanceof Error ? err.message : "Verification failed")
|
|
88
|
+
setPending(false)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function handleResend() {
|
|
93
|
+
await handleSendOtp(email)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleClose() {
|
|
97
|
+
setEmail(initialEmail || "")
|
|
98
|
+
setOtp("")
|
|
99
|
+
setOtpToken(null)
|
|
100
|
+
setStep("email")
|
|
101
|
+
setError(null)
|
|
102
|
+
setSuccess(false)
|
|
103
|
+
setPending(false)
|
|
104
|
+
autoStartedRef.current = false
|
|
105
|
+
onClose()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!open) return null
|
|
109
|
+
|
|
110
|
+
if (step === "otp" || success) {
|
|
111
|
+
return (
|
|
112
|
+
<OtpComponent
|
|
113
|
+
open
|
|
114
|
+
preventClose={!success}
|
|
115
|
+
title={success ? "Verified" : "Verify your email"}
|
|
116
|
+
description={
|
|
117
|
+
success
|
|
118
|
+
? "Opening your guest orders…"
|
|
119
|
+
: `Enter the 6-digit code sent to ${email || "your email"}.`
|
|
120
|
+
}
|
|
121
|
+
otp={otp}
|
|
122
|
+
onOtpChange={setOtp}
|
|
123
|
+
error={error}
|
|
124
|
+
pending={pending}
|
|
125
|
+
success={success}
|
|
126
|
+
successTitle="Verification successful"
|
|
127
|
+
successMessage="Redirecting to your guest orders…"
|
|
128
|
+
verifyLabel="Verify & view orders"
|
|
129
|
+
onVerify={handleVerifyOtp}
|
|
130
|
+
onResend={handleResend}
|
|
131
|
+
onClose={handleClose}
|
|
132
|
+
extraContent={
|
|
133
|
+
!success ? (
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={() => {
|
|
137
|
+
setStep("email")
|
|
138
|
+
setOtp("")
|
|
139
|
+
setError(null)
|
|
140
|
+
}}
|
|
141
|
+
className="text-xs text-muted hover:text-brand-accent mb-4 w-full text-center"
|
|
142
|
+
>
|
|
143
|
+
Change email
|
|
144
|
+
</button>
|
|
145
|
+
) : undefined
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="absolute inset-0 bg-black/40"
|
|
156
|
+
aria-label="Close guest order dialog"
|
|
157
|
+
onClick={handleClose}
|
|
158
|
+
/>
|
|
159
|
+
<div className="relative w-full max-w-md border border-cart-border/60 bg-surface shadow-[0_10px_40px_-16px_rgba(0,0,0,0.2)] p-7 sm:p-8">
|
|
160
|
+
<form
|
|
161
|
+
onSubmit={(e) => {
|
|
162
|
+
e.preventDefault()
|
|
163
|
+
void handleSendOtp()
|
|
164
|
+
}}
|
|
165
|
+
className="space-y-5"
|
|
166
|
+
>
|
|
167
|
+
<div className="text-center">
|
|
168
|
+
<Ornament />
|
|
169
|
+
<h2 className={`${auth.title} text-xl xl:text-2xl`}>Track your order</h2>
|
|
170
|
+
<p className="mt-2 text-sm text-muted leading-relaxed">
|
|
171
|
+
Enter the email used at checkout. We'll send a one-time code to view guest
|
|
172
|
+
orders.
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<label className="block">
|
|
177
|
+
<span className={`${auth.fieldLabel} mb-1.5 block`}>Email</span>
|
|
178
|
+
<input
|
|
179
|
+
type="email"
|
|
180
|
+
value={email}
|
|
181
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
182
|
+
required
|
|
183
|
+
autoComplete="email"
|
|
184
|
+
placeholder="you@example.com"
|
|
185
|
+
className={auth.input}
|
|
186
|
+
/>
|
|
187
|
+
</label>
|
|
188
|
+
|
|
189
|
+
{error && (
|
|
190
|
+
<p className="text-sm text-brand-sale" role="alert">
|
|
191
|
+
{error}
|
|
192
|
+
</p>
|
|
193
|
+
)}
|
|
194
|
+
|
|
195
|
+
<button
|
|
196
|
+
type="submit"
|
|
197
|
+
disabled={pending || !email.trim()}
|
|
198
|
+
className={`${auth.btnPrimary} ${auth.btnShine}`}
|
|
199
|
+
>
|
|
200
|
+
<span className="relative z-[1]">{pending ? "Sending…" : "Send verification code"}</span>
|
|
201
|
+
</button>
|
|
202
|
+
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onClick={handleClose}
|
|
206
|
+
className="text-sm text-muted hover:text-brand-accent w-full text-center transition-colors"
|
|
207
|
+
>
|
|
208
|
+
Cancel
|
|
209
|
+
</button>
|
|
210
|
+
</form>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function Ornament() {
|
|
217
|
+
return (
|
|
218
|
+
<div className="flex items-center justify-center gap-2 mb-4" aria-hidden>
|
|
219
|
+
<span className="h-px w-8 bg-gradient-to-r from-transparent to-brand-accent" />
|
|
220
|
+
<span className="text-brand-accent text-[8px] leading-none">◆</span>
|
|
221
|
+
<span className="h-px w-8 bg-gradient-to-l from-transparent to-brand-accent" />
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import type { BrandPanelConfig } from "./account-theme"
|
|
5
|
+
|
|
6
|
+
type LoginBrandPanelProps = {
|
|
7
|
+
panel?: BrandPanelConfig | null
|
|
8
|
+
variant?: "desktop" | "mobile"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PLACEHOLDER =
|
|
12
|
+
"data:image/svg+xml," +
|
|
13
|
+
encodeURIComponent(
|
|
14
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1200" viewBox="0 0 800 1200">
|
|
15
|
+
<defs>
|
|
16
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
17
|
+
<stop offset="0%" stop-color="#1a1a1a"/>
|
|
18
|
+
<stop offset="100%" stop-color="#3d3d3d"/>
|
|
19
|
+
</linearGradient>
|
|
20
|
+
</defs>
|
|
21
|
+
<rect width="800" height="1200" fill="url(#g)"/>
|
|
22
|
+
<circle cx="400" cy="520" r="80" fill="none" stroke="#c9a46c" stroke-width="1.5" opacity="0.5"/>
|
|
23
|
+
<circle cx="400" cy="520" r="40" fill="#c9a46c" opacity="0.25"/>
|
|
24
|
+
</svg>`
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export default function LoginBrandPanel({
|
|
28
|
+
panel,
|
|
29
|
+
variant = "desktop",
|
|
30
|
+
}: LoginBrandPanelProps) {
|
|
31
|
+
const image = panel?.image || PLACEHOLDER
|
|
32
|
+
|
|
33
|
+
if (variant === "mobile") {
|
|
34
|
+
return (
|
|
35
|
+
<div className="relative w-full h-[26vh] min-h-[160px] max-h-[200px] shrink-0 overflow-hidden bg-page-bg lg:hidden">
|
|
36
|
+
<Image
|
|
37
|
+
src={image}
|
|
38
|
+
alt=""
|
|
39
|
+
fill
|
|
40
|
+
className="object-cover object-center"
|
|
41
|
+
sizes="100vw"
|
|
42
|
+
priority
|
|
43
|
+
unoptimized={image.startsWith("data:")}
|
|
44
|
+
/>
|
|
45
|
+
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-page-bg/20 to-page-bg" />
|
|
46
|
+
{(panel?.eyebrow || panel?.headline) && (
|
|
47
|
+
<div className="absolute inset-x-0 bottom-0 p-5 z-[1]">
|
|
48
|
+
{panel.eyebrow && (
|
|
49
|
+
<p className="text-[10px] font-semibold uppercase tracking-[0.28em] text-inverse/80 mb-1">
|
|
50
|
+
{panel.eyebrow}
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
{panel.headline && (
|
|
54
|
+
<p className="font-heading text-lg text-inverse font-bold tracking-wide">
|
|
55
|
+
{panel.headline}
|
|
56
|
+
</p>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="relative hidden lg:block lg:w-1/2 h-full shrink-0 overflow-hidden bg-page-bg">
|
|
66
|
+
<Image
|
|
67
|
+
src={image}
|
|
68
|
+
alt=""
|
|
69
|
+
fill
|
|
70
|
+
className="object-cover object-center"
|
|
71
|
+
sizes="50vw"
|
|
72
|
+
priority
|
|
73
|
+
unoptimized={image.startsWith("data:")}
|
|
74
|
+
/>
|
|
75
|
+
<div
|
|
76
|
+
className="absolute inset-y-0 right-0 w-[50%] min-w-[200px] bg-gradient-to-l from-page-bg via-page-bg/80 to-transparent"
|
|
77
|
+
aria-hidden
|
|
78
|
+
/>
|
|
79
|
+
<div className="absolute inset-0 bg-gradient-to-r from-black/[0.04] via-transparent to-transparent" />
|
|
80
|
+
{(panel?.eyebrow || panel?.headline) && (
|
|
81
|
+
<div className="absolute inset-x-0 bottom-0 p-10 xl:p-14 z-[1] max-w-md">
|
|
82
|
+
{panel.eyebrow && (
|
|
83
|
+
<p className="text-[10px] font-semibold uppercase tracking-[0.28em] text-inverse/70 mb-2">
|
|
84
|
+
{panel.eyebrow}
|
|
85
|
+
</p>
|
|
86
|
+
)}
|
|
87
|
+
{panel.headline && (
|
|
88
|
+
<p className="font-heading text-2xl xl:text-3xl text-inverse font-bold tracking-wide leading-snug">
|
|
89
|
+
{panel.headline}
|
|
90
|
+
</p>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|