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