@pradip1995/segment-login-template 0.2.5 → 0.4.0
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 +9 -2
- package/src/account-addresses.tsx +216 -0
- package/src/account-guest-orders.tsx +60 -0
- package/src/account-orders.tsx +81 -0
- package/src/account-overview.tsx +111 -0
- package/src/account-payment-methods.tsx +229 -0
- package/src/account-profile.tsx +198 -0
- package/src/account-utils.ts +20 -0
- package/src/auth-server.ts +307 -0
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/forgot-password-form.tsx +89 -0
- package/src/google-auth-section.tsx +86 -0
- package/src/index.ts +5 -0
- package/src/login-form.tsx +411 -0
- package/src/otp-component.ts +14 -0
- package/src/otp-input.tsx +46 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +324 -0
- package/src/reset-password-form.tsx +110 -0
- package/src/reset-password-page.tsx +12 -0
- package/src/segment.tsx +81 -200
package/src/segment.tsx
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { login, requestPasswordReset, signout, signup } from "@pradip1995/commerce-core/client/actions/customer"
|
|
5
|
-
import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
|
|
6
|
-
import { formatPrice } from "@pradip1995/segment-primitives/format-price"
|
|
3
|
+
import { useState } from "react"
|
|
7
4
|
import type { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import LoginForm from "./login-form"
|
|
6
|
+
import RegisterForm from "./register-form"
|
|
7
|
+
import ForgotPasswordForm from "./forgot-password-form"
|
|
8
|
+
import AccountOverview from "./account-overview"
|
|
9
|
+
import AccountProfile from "./account-profile"
|
|
10
|
+
import AccountAddresses from "./account-addresses"
|
|
11
|
+
import AccountOrders from "./account-orders"
|
|
12
|
+
import AccountPaymentMethods from "./account-payment-methods"
|
|
13
|
+
import AccountGuestOrders from "./account-guest-orders"
|
|
14
|
+
import { resolveOtpComponent, type OtpVerificationComponent } from "./otp-component"
|
|
15
|
+
import { accountSectionPath } from "./account-utils"
|
|
8
16
|
|
|
9
17
|
type AccountProps = {
|
|
10
18
|
countryCode?: string
|
|
11
19
|
path?: string
|
|
12
20
|
customer?: HttpTypes.StoreCustomer | null
|
|
13
21
|
orders?: HttpTypes.StoreOrder[]
|
|
22
|
+
paymentDetails?: Record<string, unknown>[]
|
|
23
|
+
guestOrders?: HttpTypes.StoreOrder[]
|
|
24
|
+
title?: string
|
|
25
|
+
subtitle?: string
|
|
26
|
+
otpComponent?: OtpVerificationComponent
|
|
14
27
|
}
|
|
15
28
|
|
|
16
29
|
export default function LoginTemplate({
|
|
@@ -18,33 +31,84 @@ export default function LoginTemplate({
|
|
|
18
31
|
path = "",
|
|
19
32
|
customer,
|
|
20
33
|
orders = [],
|
|
34
|
+
paymentDetails = [],
|
|
35
|
+
guestOrders = [],
|
|
36
|
+
title,
|
|
37
|
+
subtitle,
|
|
38
|
+
otpComponent,
|
|
21
39
|
}: AccountProps) {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
const OtpComponent = resolveOtpComponent(otpComponent)
|
|
41
|
+
const section = accountSectionPath(path)
|
|
42
|
+
|
|
43
|
+
if (section === "guest-orders") {
|
|
44
|
+
return <AccountGuestOrders orders={guestOrders} countryCode={countryCode} />
|
|
45
|
+
}
|
|
25
46
|
|
|
26
47
|
if (customer && customer.id !== "pending_deletion") {
|
|
27
|
-
|
|
48
|
+
switch (section) {
|
|
49
|
+
case "profile":
|
|
50
|
+
return (
|
|
51
|
+
<AccountProfile
|
|
52
|
+
customer={customer}
|
|
53
|
+
countryCode={countryCode}
|
|
54
|
+
OtpComponent={OtpComponent}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
case "addresses":
|
|
58
|
+
return <AccountAddresses customer={customer} countryCode={countryCode} />
|
|
59
|
+
case "orders":
|
|
60
|
+
return <AccountOrders orders={orders} />
|
|
61
|
+
case "payment-methods":
|
|
62
|
+
return (
|
|
63
|
+
<AccountPaymentMethods
|
|
64
|
+
paymentDetails={paymentDetails as Parameters<typeof AccountPaymentMethods>[0]["paymentDetails"]}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
case "overview":
|
|
68
|
+
default:
|
|
69
|
+
return <AccountOverview customer={customer} orders={orders} countryCode={countryCode} />
|
|
70
|
+
}
|
|
28
71
|
}
|
|
29
72
|
|
|
73
|
+
const [view, setView] = useState<"login" | "register" | "forgot">(
|
|
74
|
+
path === "register" ? "register" : path === "forgot-password" ? "forgot" : "login"
|
|
75
|
+
)
|
|
76
|
+
|
|
30
77
|
return (
|
|
31
78
|
<div className="w-full max-w-md mx-auto">
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
40
96
|
|
|
41
97
|
{view === "login" && (
|
|
42
98
|
<LoginForm
|
|
43
99
|
countryCode={countryCode}
|
|
100
|
+
OtpComponent={OtpComponent}
|
|
44
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")}
|
|
45
110
|
/>
|
|
46
111
|
)}
|
|
47
|
-
{view === "register" && <RegisterForm countryCode={countryCode} />}
|
|
48
112
|
{view === "forgot" && <ForgotPasswordForm onBack={() => setView("login")} />}
|
|
49
113
|
</div>
|
|
50
114
|
)
|
|
@@ -73,186 +137,3 @@ function TabButton({
|
|
|
73
137
|
</button>
|
|
74
138
|
)
|
|
75
139
|
}
|
|
76
|
-
|
|
77
|
-
function LoginForm({
|
|
78
|
-
countryCode,
|
|
79
|
-
onForgot,
|
|
80
|
-
}: {
|
|
81
|
-
countryCode: string
|
|
82
|
-
onForgot: () => void
|
|
83
|
-
}) {
|
|
84
|
-
const [state, formAction, pending] = useActionState(login, null)
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<form action={formAction} className="space-y-4">
|
|
88
|
-
<input type="hidden" name="countryCode" value={countryCode} />
|
|
89
|
-
<Field label="Email or phone" name="email_or_phone" type="text" autoComplete="username" />
|
|
90
|
-
<Field label="Password" name="password" type="password" autoComplete="current-password" />
|
|
91
|
-
{state && typeof state === "string" && (
|
|
92
|
-
<p className="text-sm text-brand-sale">{state}</p>
|
|
93
|
-
)}
|
|
94
|
-
<button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
|
|
95
|
-
{pending ? "Signing in…" : "Sign in"}
|
|
96
|
-
</button>
|
|
97
|
-
<button type="button" onClick={onForgot} className="text-sm text-muted hover:text-brand-accent w-full text-center">
|
|
98
|
-
Forgot password?
|
|
99
|
-
</button>
|
|
100
|
-
</form>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function RegisterForm({ countryCode }: { countryCode: string }) {
|
|
105
|
-
const [state, formAction, pending] = useActionState(signup, null)
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<form action={formAction} className="space-y-4">
|
|
109
|
-
<input type="hidden" name="countryCode" value={countryCode} />
|
|
110
|
-
<div className="grid grid-cols-2 gap-3">
|
|
111
|
-
<Field label="First name" name="first_name" type="text" autoComplete="given-name" />
|
|
112
|
-
<Field label="Last name" name="last_name" type="text" autoComplete="family-name" />
|
|
113
|
-
</div>
|
|
114
|
-
<Field label="Email" name="email" type="email" autoComplete="email" />
|
|
115
|
-
<Field label="Phone" name="phone" type="tel" autoComplete="tel" />
|
|
116
|
-
<Field label="Password" name="password" type="password" autoComplete="new-password" />
|
|
117
|
-
{state && typeof state === "string" && (
|
|
118
|
-
<p className="text-sm text-brand-sale">{state}</p>
|
|
119
|
-
)}
|
|
120
|
-
<button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
|
|
121
|
-
{pending ? "Creating account…" : "Create account"}
|
|
122
|
-
</button>
|
|
123
|
-
</form>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function ForgotPasswordForm({ onBack }: { onBack: () => void }) {
|
|
128
|
-
const [email, setEmail] = useState("")
|
|
129
|
-
const [message, setMessage] = useState<string | null>(null)
|
|
130
|
-
const [pending, setPending] = useState(false)
|
|
131
|
-
|
|
132
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
133
|
-
e.preventDefault()
|
|
134
|
-
setPending(true)
|
|
135
|
-
setMessage(null)
|
|
136
|
-
const result = await requestPasswordReset(email)
|
|
137
|
-
setMessage(result.success ? result.message : result.error || "Something went wrong")
|
|
138
|
-
setPending(false)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
143
|
-
<p className="text-sm text-muted">Enter your email and we'll send a reset link.</p>
|
|
144
|
-
<Field
|
|
145
|
-
label="Email"
|
|
146
|
-
name="email"
|
|
147
|
-
type="email"
|
|
148
|
-
value={email}
|
|
149
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
150
|
-
/>
|
|
151
|
-
{message && <p className="text-sm text-body">{message}</p>}
|
|
152
|
-
<button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
|
|
153
|
-
{pending ? "Sending…" : "Send reset link"}
|
|
154
|
-
</button>
|
|
155
|
-
<button type="button" onClick={onBack} className="text-sm text-muted hover:text-brand-accent w-full text-center">
|
|
156
|
-
Back to sign in
|
|
157
|
-
</button>
|
|
158
|
-
</form>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function AccountOverview({
|
|
163
|
-
customer,
|
|
164
|
-
orders,
|
|
165
|
-
countryCode,
|
|
166
|
-
}: {
|
|
167
|
-
customer: HttpTypes.StoreCustomer
|
|
168
|
-
orders: HttpTypes.StoreOrder[]
|
|
169
|
-
countryCode: string
|
|
170
|
-
}) {
|
|
171
|
-
const name = [customer.first_name, customer.last_name].filter(Boolean).join(" ") || customer.email
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className="space-y-8">
|
|
175
|
-
<div className="card-surface rounded-lg p-6">
|
|
176
|
-
<p className="text-xs uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-2">My account</p>
|
|
177
|
-
<h1 className="section-heading text-xl mb-1">{name}</h1>
|
|
178
|
-
<p className="text-sm text-muted">{customer.email}</p>
|
|
179
|
-
{customer.phone && <p className="text-sm text-muted mt-1">{customer.phone}</p>}
|
|
180
|
-
<form action={signout.bind(null, countryCode)} className="mt-6">
|
|
181
|
-
<button type="submit" className="btn-outline text-xs">
|
|
182
|
-
Sign out
|
|
183
|
-
</button>
|
|
184
|
-
</form>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<div>
|
|
188
|
-
<h2 className="text-sm font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading mb-4">
|
|
189
|
-
Recent orders
|
|
190
|
-
</h2>
|
|
191
|
-
{orders.length === 0 ? (
|
|
192
|
-
<div className="card-surface rounded-lg p-8 text-center">
|
|
193
|
-
<p className="text-muted text-sm mb-4">No orders yet.</p>
|
|
194
|
-
<LocalizedLink href="/store" className="btn-primary inline-block">
|
|
195
|
-
Start shopping
|
|
196
|
-
</LocalizedLink>
|
|
197
|
-
</div>
|
|
198
|
-
) : (
|
|
199
|
-
<ul className="space-y-3">
|
|
200
|
-
{orders.map((order) => (
|
|
201
|
-
<li key={order.id}>
|
|
202
|
-
<LocalizedLink
|
|
203
|
-
href={`/orders/${order.id}`}
|
|
204
|
-
className="card-surface rounded-lg p-4 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
|
|
205
|
-
>
|
|
206
|
-
<div>
|
|
207
|
-
<p className="font-medium text-heading text-sm">
|
|
208
|
-
Order #{order.display_id}
|
|
209
|
-
</p>
|
|
210
|
-
<p className="text-xs text-muted mt-1 capitalize">
|
|
211
|
-
{order.status?.replace(/_/g, " ")}
|
|
212
|
-
</p>
|
|
213
|
-
</div>
|
|
214
|
-
<p className="text-sm font-semibold text-brand-accent shrink-0">
|
|
215
|
-
{formatPrice(order.total, order.currency_code)}
|
|
216
|
-
</p>
|
|
217
|
-
</LocalizedLink>
|
|
218
|
-
</li>
|
|
219
|
-
))}
|
|
220
|
-
</ul>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function Field({
|
|
228
|
-
label,
|
|
229
|
-
name,
|
|
230
|
-
type,
|
|
231
|
-
autoComplete,
|
|
232
|
-
value,
|
|
233
|
-
onChange,
|
|
234
|
-
}: {
|
|
235
|
-
label: string
|
|
236
|
-
name: string
|
|
237
|
-
type: string
|
|
238
|
-
autoComplete?: string
|
|
239
|
-
value?: string
|
|
240
|
-
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
|
241
|
-
}) {
|
|
242
|
-
return (
|
|
243
|
-
<label className="block">
|
|
244
|
-
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
245
|
-
{label}
|
|
246
|
-
</span>
|
|
247
|
-
<input
|
|
248
|
-
name={name}
|
|
249
|
-
type={type}
|
|
250
|
-
autoComplete={autoComplete}
|
|
251
|
-
value={value}
|
|
252
|
-
onChange={onChange}
|
|
253
|
-
required
|
|
254
|
-
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
255
|
-
/>
|
|
256
|
-
</label>
|
|
257
|
-
)
|
|
258
|
-
}
|