@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.
@@ -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
- <div className="relative">
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 rounded-lg">
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
- <Field
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
- <Field
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
- <Field
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
- <Field
204
- label="Password"
205
- type="password"
206
- value={form.password}
207
- onChange={(e) => updateField("password", e.target.value)}
208
- autoComplete="new-password"
209
- />
210
-
211
- {error && <p className="text-sm text-brand-sale">{error}</p>}
212
-
213
- <button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
214
- {pending ? "Creating account…" : "Create account"}
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 type="button" onClick={onLogin} className="text-brand-accent font-semibold hover:underline">
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={`flex-1 py-2 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] rounded-md transition-colors ${
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 Field({
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
- <label className="block">
311
- <span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
312
- {label}
313
- </span>
314
- <input
315
- type={type}
316
- value={value}
317
- onChange={onChange}
318
- autoComplete={autoComplete}
319
- required={required}
320
- className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
321
- />
322
- </label>
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={paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["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 <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
121
+ return (
122
+ <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
123
+ )
70
124
  }
71
125
  }
72
126
 
73
- const [view, setView] = useState<"login" | "register" | "forgot">(
74
- path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
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
- <div className="w-full max-w-md mx-auto">
79
- {(title || subtitle) && view !== "forgot" && (
80
- <div className="mb-8 text-center">
81
- {title && <h1 className="section-heading text-2xl mb-2">{title}</h1>}
82
- {subtitle && <p className="text-sm text-muted">{subtitle}</p>}
83
- </div>
84
- )}
85
-
86
- {view !== "forgot" && (
87
- <div className="flex gap-6 mb-8 border-b border-cart-border">
88
- <TabButton active={view === "login"} onClick={() => setView("login")}>
89
- Sign in
90
- </TabButton>
91
- <TabButton active={view === "register"} onClick={() => setView("register")}>
92
- Register
93
- </TabButton>
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
- {view === "login" && (
98
- <LoginForm
99
- countryCode={countryCode}
100
- OtpComponent={OtpComponent}
101
- onForgot={() => setView("forgot")}
102
- onRegister={() => setView("register")}
103
- />
104
- )}
105
- {view === "register" && (
106
- <RegisterForm
107
- countryCode={countryCode}
108
- OtpComponent={OtpComponent}
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={`pb-3 text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] border-b-2 -mb-px transition-colors ${
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>