@pradip1995/segment-login-template 0.2.5 → 0.3.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 +6 -2
- package/src/account-overview.tsx +69 -0
- package/src/auth-server.ts +152 -0
- package/src/forgot-password-form.tsx +89 -0
- package/src/google-auth-section.tsx +25 -0
- package/src/index.ts +1 -0
- package/src/login-form.tsx +355 -0
- package/src/otp-input.tsx +46 -0
- package/src/register-form.tsx +298 -0
- package/src/reset-password-form.tsx +110 -0
- package/src/reset-password-page.tsx +12 -0
- package/src/segment.tsx +19 -196
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { useSearchParams } from "next/navigation"
|
|
5
|
+
import { completePasswordResetAction } from "./auth-server"
|
|
6
|
+
|
|
7
|
+
export default function ResetPasswordForm({ countryCode }: { countryCode: string }) {
|
|
8
|
+
const searchParams = useSearchParams()
|
|
9
|
+
const token = searchParams.get("token") || ""
|
|
10
|
+
const email = searchParams.get("email") || ""
|
|
11
|
+
|
|
12
|
+
const [password, setPassword] = useState("")
|
|
13
|
+
const [confirmPassword, setConfirmPassword] = useState("")
|
|
14
|
+
const [error, setError] = useState<string | null>(null)
|
|
15
|
+
const [pending, setPending] = useState(false)
|
|
16
|
+
|
|
17
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
setError(null)
|
|
20
|
+
|
|
21
|
+
if (!token || !email) {
|
|
22
|
+
setError("This reset link is invalid or expired. Request a new one from the sign-in page.")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (password.length < 8) {
|
|
27
|
+
setError("Password must be at least 8 characters.")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (password !== confirmPassword) {
|
|
32
|
+
setError("Passwords do not match.")
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setPending(true)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = await completePasswordResetAction({
|
|
40
|
+
email,
|
|
41
|
+
password,
|
|
42
|
+
token,
|
|
43
|
+
countryCode,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (result && !result.success) {
|
|
47
|
+
setError(result.error || "Failed to reset password")
|
|
48
|
+
setPending(false)
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (isNextRedirect(err)) throw err
|
|
52
|
+
setError(err instanceof Error ? err.message : "Failed to reset password")
|
|
53
|
+
setPending(false)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="w-full max-w-md mx-auto space-y-6">
|
|
59
|
+
<div className="text-center space-y-2">
|
|
60
|
+
<h1 className="section-heading text-xl">Set a new password</h1>
|
|
61
|
+
<p className="text-sm text-muted">Choose a strong password for {email || "your account"}.</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
65
|
+
<label className="block">
|
|
66
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
67
|
+
New password
|
|
68
|
+
</span>
|
|
69
|
+
<input
|
|
70
|
+
type="password"
|
|
71
|
+
value={password}
|
|
72
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
73
|
+
required
|
|
74
|
+
autoComplete="new-password"
|
|
75
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
76
|
+
/>
|
|
77
|
+
</label>
|
|
78
|
+
|
|
79
|
+
<label className="block">
|
|
80
|
+
<span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
|
|
81
|
+
Confirm password
|
|
82
|
+
</span>
|
|
83
|
+
<input
|
|
84
|
+
type="password"
|
|
85
|
+
value={confirmPassword}
|
|
86
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
87
|
+
required
|
|
88
|
+
autoComplete="new-password"
|
|
89
|
+
className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
|
|
90
|
+
/>
|
|
91
|
+
</label>
|
|
92
|
+
|
|
93
|
+
{error && <p className="text-sm text-brand-sale">{error}</p>}
|
|
94
|
+
|
|
95
|
+
<button type="submit" disabled={pending} className="btn-primary w-full disabled:opacity-60">
|
|
96
|
+
{pending ? "Updating…" : "Update password"}
|
|
97
|
+
</button>
|
|
98
|
+
</form>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isNextRedirect(err: unknown) {
|
|
104
|
+
return (
|
|
105
|
+
typeof err === "object" &&
|
|
106
|
+
err !== null &&
|
|
107
|
+
"digest" in err &&
|
|
108
|
+
String((err as { digest?: string }).digest || "").includes("NEXT_REDIRECT")
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Suspense } from "react"
|
|
4
|
+
import ResetPasswordForm from "./reset-password-form"
|
|
5
|
+
|
|
6
|
+
export default function ResetPasswordPage({ countryCode = "in" }: { countryCode?: string }) {
|
|
7
|
+
return (
|
|
8
|
+
<Suspense fallback={<p className="text-sm text-muted text-center py-8">Loading…</p>}>
|
|
9
|
+
<ResetPasswordForm countryCode={countryCode} />
|
|
10
|
+
</Suspense>
|
|
11
|
+
)
|
|
12
|
+
}
|
package/src/segment.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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"
|
|
8
9
|
|
|
9
10
|
type AccountProps = {
|
|
10
11
|
countryCode?: string
|
|
@@ -29,22 +30,27 @@ export default function LoginTemplate({
|
|
|
29
30
|
|
|
30
31
|
return (
|
|
31
32
|
<div className="w-full max-w-md mx-auto">
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
{view !== "forgot" && (
|
|
34
|
+
<div className="flex gap-6 mb-8 border-b border-cart-border">
|
|
35
|
+
<TabButton active={view === "login"} onClick={() => setView("login")}>
|
|
36
|
+
Sign in
|
|
37
|
+
</TabButton>
|
|
38
|
+
<TabButton active={view === "register"} onClick={() => setView("register")}>
|
|
39
|
+
Register
|
|
40
|
+
</TabButton>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
40
43
|
|
|
41
44
|
{view === "login" && (
|
|
42
45
|
<LoginForm
|
|
43
46
|
countryCode={countryCode}
|
|
44
47
|
onForgot={() => setView("forgot")}
|
|
48
|
+
onRegister={() => setView("register")}
|
|
45
49
|
/>
|
|
46
50
|
)}
|
|
47
|
-
{view === "register" &&
|
|
51
|
+
{view === "register" && (
|
|
52
|
+
<RegisterForm countryCode={countryCode} onLogin={() => setView("login")} />
|
|
53
|
+
)}
|
|
48
54
|
{view === "forgot" && <ForgotPasswordForm onBack={() => setView("login")} />}
|
|
49
55
|
</div>
|
|
50
56
|
)
|
|
@@ -73,186 +79,3 @@ function TabButton({
|
|
|
73
79
|
</button>
|
|
74
80
|
)
|
|
75
81
|
}
|
|
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
|
-
}
|