@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-login-template",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -4,6 +4,7 @@ import type { HttpTypes } from "@medusajs/types"
4
4
  import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
5
5
  import { formatPrice } from "@pradip1995/segment-primitives/format-price"
6
6
  import { logoutGuest } from "@pradip1995/commerce-core/data/guest"
7
+ import { auth } from "./account-theme"
7
8
 
8
9
  export default function AccountGuestOrders({
9
10
  orders,
@@ -14,30 +15,56 @@ export default function AccountGuestOrders({
14
15
  }) {
15
16
  const safeOrders: HttpTypes.StoreOrder[] = Array.isArray(orders)
16
17
  ? orders
17
- : Array.isArray((orders as any)?.orders)
18
- ? (orders as any).orders
19
- : []
18
+ : Array.isArray((orders as { orders?: HttpTypes.StoreOrder[] })?.orders)
19
+ ? ((orders as { orders: HttpTypes.StoreOrder[] }).orders)
20
+ : []
20
21
 
21
22
  async function handleSignOut() {
22
23
  await logoutGuest()
23
24
  window.location.href = `/${countryCode}/account`
24
25
  }
25
26
 
27
+ async function handleCheckDifferent() {
28
+ await logoutGuest()
29
+ window.location.href = `/${countryCode}/account?view=guest`
30
+ }
31
+
26
32
  return (
27
33
  <div className="space-y-6">
28
34
  <div className="flex flex-wrap items-start justify-between gap-4">
29
35
  <div>
30
- <h2 className="section-heading text-xl mb-1">Guest orders</h2>
31
- <p className="text-sm text-muted">Orders linked to your verified email.</p>
36
+ <div className="flex items-center gap-2 mb-2" aria-hidden>
37
+ <span className="h-px w-6 bg-brand-accent/70" />
38
+ <span className="text-brand-accent text-[7px]">◆</span>
39
+ <span className="h-px w-6 bg-brand-accent/70" />
40
+ </div>
41
+ <h2 className="font-heading text-2xl font-bold text-heading uppercase tracking-[0.08em] mb-1">
42
+ Guest orders
43
+ </h2>
44
+ <p className="text-sm text-muted">
45
+ Track orders linked to your verified email.
46
+ </p>
47
+ </div>
48
+ <div className="flex flex-wrap gap-2">
49
+ <button
50
+ type="button"
51
+ onClick={handleCheckDifferent}
52
+ className="btn-outline text-xs"
53
+ >
54
+ Check different email
55
+ </button>
56
+ <button type="button" onClick={handleSignOut} className="btn-outline text-xs">
57
+ End guest session
58
+ </button>
32
59
  </div>
33
- <button type="button" onClick={handleSignOut} className="btn-outline text-xs">
34
- End guest session
35
- </button>
36
60
  </div>
37
61
 
38
62
  {safeOrders.length === 0 ? (
39
- <div className="card-surface rounded-2xl p-8 text-center text-sm text-muted">
40
- No guest orders found for this session.
63
+ <div className="border border-cart-border bg-surface p-8 text-center">
64
+ <p className="text-sm text-muted mb-4">No guest orders found for this session.</p>
65
+ <LocalizedLink href="/store" className={`${auth.btnPrimary} ${auth.btnShine} inline-flex`}>
66
+ <span className="relative z-[1]">Continue shopping</span>
67
+ </LocalizedLink>
41
68
  </div>
42
69
  ) : (
43
70
  <ul className="space-y-3">
@@ -45,10 +72,12 @@ export default function AccountGuestOrders({
45
72
  <li key={order.id}>
46
73
  <LocalizedLink
47
74
  href={`/orders/${order.id}`}
48
- className="card-surface rounded-2xl p-5 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
75
+ className="border border-cart-border bg-surface p-5 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors duration-200 block"
49
76
  >
50
77
  <div>
51
- <p className="font-semibold text-heading text-sm">Order #{order.display_id}</p>
78
+ <p className="font-semibold text-heading text-sm">
79
+ Order #{order.display_id}
80
+ </p>
52
81
  <p className="text-xs text-muted mt-1 capitalize">
53
82
  {order.status?.replace(/_/g, " ")}
54
83
  </p>
@@ -1,4 +1,4 @@
1
- /** Account / order visual tokens — maps to segment-tokens CSS variables */
1
+ /** Logged-in account visual tokens — maps to segment-tokens CSS variables */
2
2
  export const acct = {
3
3
  pageBg: "bg-page-bg",
4
4
  card: "bg-surface border border-cart-border rounded-none shadow-card",
@@ -26,3 +26,54 @@ export const acct = {
26
26
  badge:
27
27
  "inline-flex items-center px-3 py-1 text-xs font-semibold border rounded-none",
28
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-4 text-center py-4">
46
- <div className="w-14 h-14 mx-auto rounded-full bg-green-100 flex items-center justify-center text-2xl">
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="section-heading text-lg">Check your email</h2>
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="btn-outline w-full">
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-4">
60
- <p className="text-sm text-muted">
61
- Enter your email and we&apos;ll send a link to reset your password.
62
- </p>
63
- <label className="block">
64
- <span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
65
- Email
66
- </span>
67
- <input
68
- name="email"
69
- type="email"
70
- value={email}
71
- onChange={(e) => setEmail(e.target.value)}
72
- required
73
- className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
74
- />
75
- </label>
76
- {error && <p className="text-sm text-brand-sale">{error}</p>}
77
- <button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
78
- {pending ? "Sending…" : "Send reset link"}
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&apos;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 className="mb-6">
47
+ <div>
50
48
  {error && (
51
- <p className="mb-3 text-sm text-red-600" role="alert">
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={BUTTON_CLASS}
60
- style={{ borderRadius: "30px" }}
57
+ className={auth.btnOutline}
58
+ aria-label="Continue with Google"
61
59
  >
62
- <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
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-sm text-gray-700 font-bold">
81
- {loading ? "Connecting..." : "Continue with Google"}
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>
@@ -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
+ }