@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/package.json
CHANGED
|
@@ -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
|
|
18
|
-
|
|
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
|
-
<
|
|
31
|
-
|
|
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="
|
|
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="
|
|
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">
|
|
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>
|
package/src/account-theme.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
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-
|
|
46
|
-
<
|
|
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=
|
|
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=
|
|
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-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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'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
|
|
47
|
+
<div>
|
|
50
48
|
{error && (
|
|
51
|
-
<p className="mb-3 text-sm text-
|
|
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={
|
|
60
|
-
|
|
57
|
+
className={auth.btnOutline}
|
|
58
|
+
aria-label="Continue with Google"
|
|
61
59
|
>
|
|
62
|
-
<svg width="
|
|
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-
|
|
81
|
-
{loading ? "
|
|
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'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
|
+
}
|