@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.
@@ -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 { useActionState, useState } from "react"
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
- <div className="flex gap-6 mb-8 border-b border-cart-border">
33
- <TabButton active={view === "login"} onClick={() => setView("login")}>
34
- Sign in
35
- </TabButton>
36
- <TabButton active={view === "register"} onClick={() => setView("register")}>
37
- Register
38
- </TabButton>
39
- </div>
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" && <RegisterForm countryCode={countryCode} />}
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&apos;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
- }