@pradip1995/segment-login-template 0.3.0 → 0.4.1

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.3.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,7 +12,10 @@
12
12
  "exports": {
13
13
  ".": "./src/index.ts",
14
14
  "./manifest": "./src/manifest.ts",
15
- "./reset-password": "./src/reset-password-page.tsx"
15
+ "./reset-password": "./src/reset-password-page.tsx",
16
+ "./otp-verification-modal": "./src/otp-verification-modal.tsx",
17
+ "./commerce-auth-otp-modal": "./src/commerce-auth-otp-modal.tsx",
18
+ "./otp-component": "./src/otp-component.ts"
16
19
  },
17
20
  "peerDependencies": {
18
21
  "@pradip1995/commerce-auth": "^4.0.0",
@@ -25,8 +28,8 @@
25
28
  "dependencies": {
26
29
  "@pradip1995/commerce-auth": "^4.0.0",
27
30
  "@pradip1995/commerce-core": "^4.0.0",
28
- "@pradip1995/segment-primitives": "^0.3.0",
29
- "@pradip1995/segment-tokens": "^0.3.2"
31
+ "@pradip1995/segment-primitives": "^0.4.0",
32
+ "@pradip1995/segment-tokens": "^0.3.7"
30
33
  },
31
34
  "devDependencies": {
32
35
  "@pradip1995/plugin-sdk": "^0.2.0",
@@ -0,0 +1,216 @@
1
+ "use client"
2
+
3
+ import { useActionState, useState } from "react"
4
+ import type { HttpTypes } from "@medusajs/types"
5
+ import {
6
+ addCustomerAddress,
7
+ deleteCustomerAddress,
8
+ updateCustomerAddress,
9
+ } from "@pradip1995/commerce-core/data/customer"
10
+
11
+ export default function AccountAddresses({
12
+ customer,
13
+ countryCode,
14
+ }: {
15
+ customer: HttpTypes.StoreCustomer
16
+ countryCode: string
17
+ }) {
18
+ const addresses = customer.addresses ?? []
19
+ const [editingId, setEditingId] = useState<string | null>(null)
20
+ const [showAdd, setShowAdd] = useState(false)
21
+
22
+ return (
23
+ <div className="space-y-8">
24
+ <div className="flex flex-wrap items-end justify-between gap-4">
25
+ <div>
26
+ <h2 className="section-heading text-xl mb-1">Saved addresses</h2>
27
+ <p className="text-sm text-muted">Manage shipping and billing addresses.</p>
28
+ </div>
29
+ <button type="button" onClick={() => setShowAdd(true)} className="btn-primary text-sm">
30
+ Add address
31
+ </button>
32
+ </div>
33
+
34
+ {addresses.length === 0 && !showAdd && (
35
+ <div className="card-surface rounded-2xl p-8 text-center text-sm text-muted">
36
+ No saved addresses yet.
37
+ </div>
38
+ )}
39
+
40
+ <ul className="space-y-4">
41
+ {addresses.map((address) => (
42
+ <li key={address.id} className="card-surface rounded-2xl p-5">
43
+ {editingId === address.id ? (
44
+ <AddressForm
45
+ countryCode={countryCode}
46
+ address={address}
47
+ addressId={address.id}
48
+ onDone={() => setEditingId(null)}
49
+ onCancel={() => setEditingId(null)}
50
+ />
51
+ ) : (
52
+ <div className="flex flex-wrap justify-between gap-4">
53
+ <div className="text-sm text-body space-y-1">
54
+ <p className="font-semibold text-heading">
55
+ {[address.first_name, address.last_name].filter(Boolean).join(" ")}
56
+ </p>
57
+ <p>{address.address_1}</p>
58
+ {address.address_2 && <p>{address.address_2}</p>}
59
+ <p>
60
+ {[address.city, address.province, address.postal_code].filter(Boolean).join(", ")}
61
+ </p>
62
+ <p className="uppercase text-xs text-muted">{address.country_code}</p>
63
+ {address.phone && <p>{address.phone}</p>}
64
+ {(address.is_default_shipping || address.is_default_billing) && (
65
+ <p className="text-xs text-brand-accent font-medium pt-1">
66
+ {address.is_default_shipping && address.is_default_billing
67
+ ? "Default shipping & billing"
68
+ : address.is_default_shipping
69
+ ? "Default shipping"
70
+ : "Default billing"}
71
+ </p>
72
+ )}
73
+ </div>
74
+ <div className="flex gap-2 shrink-0">
75
+ <button
76
+ type="button"
77
+ onClick={() => setEditingId(address.id!)}
78
+ className="btn-outline text-xs"
79
+ >
80
+ Edit
81
+ </button>
82
+ <DeleteAddressButton addressId={address.id!} />
83
+ </div>
84
+ </div>
85
+ )}
86
+ </li>
87
+ ))}
88
+ </ul>
89
+
90
+ {showAdd && (
91
+ <div className="card-surface rounded-2xl p-5">
92
+ <h3 className="text-sm font-semibold text-heading mb-4">New address</h3>
93
+ <AddressForm
94
+ countryCode={countryCode}
95
+ onDone={() => setShowAdd(false)}
96
+ onCancel={() => setShowAdd(false)}
97
+ />
98
+ </div>
99
+ )}
100
+ </div>
101
+ )
102
+ }
103
+
104
+ function AddressForm({
105
+ countryCode,
106
+ address,
107
+ addressId,
108
+ onDone,
109
+ onCancel,
110
+ }: {
111
+ countryCode: string
112
+ address?: HttpTypes.StoreCustomerAddress
113
+ addressId?: string
114
+ onDone: () => void
115
+ onCancel: () => void
116
+ }) {
117
+ const action = addressId ? updateCustomerAddress : addCustomerAddress
118
+ const [state, formAction, pending] = useActionState(
119
+ async (prev: Record<string, unknown>, formData: FormData) => {
120
+ const result = await action(
121
+ addressId ? { ...prev, addressId } : { ...prev, isDefaultShipping: false, isDefaultBilling: false },
122
+ formData
123
+ )
124
+ if (result?.success) {
125
+ onDone()
126
+ window.location.reload()
127
+ }
128
+ return result
129
+ },
130
+ { success: false, error: null as string | null }
131
+ )
132
+
133
+ return (
134
+ <form action={formAction} className="grid grid-cols-1 sm:grid-cols-2 gap-4">
135
+ <Field label="First name" name="first_name" defaultValue={address?.first_name || ""} />
136
+ <Field label="Last name" name="last_name" defaultValue={address?.last_name || ""} />
137
+ <Field label="Address line 1" name="address_1" className="sm:col-span-2" defaultValue={address?.address_1 || ""} />
138
+ <Field label="Address line 2" name="address_2" className="sm:col-span-2" defaultValue={address?.address_2 || ""} />
139
+ <Field label="City" name="city" defaultValue={address?.city || ""} />
140
+ <Field label="State / Province" name="province" defaultValue={address?.province || ""} />
141
+ <Field label="Postal code" name="postal_code" defaultValue={address?.postal_code || ""} />
142
+ <Field
143
+ label="Country code"
144
+ name="country_code"
145
+ defaultValue={address?.country_code || countryCode}
146
+ />
147
+ <Field label="Phone" name="phone" type="tel" defaultValue={address?.phone || ""} />
148
+ {state?.error && (
149
+ <p className="sm:col-span-2 text-sm text-brand-sale">{String(state.error)}</p>
150
+ )}
151
+ <div className="sm:col-span-2 flex gap-3">
152
+ <button type="submit" disabled={pending} className="btn-primary text-sm disabled:opacity-60">
153
+ {pending ? "Saving…" : "Save address"}
154
+ </button>
155
+ <button type="button" onClick={onCancel} className="btn-outline text-sm">
156
+ Cancel
157
+ </button>
158
+ </div>
159
+ </form>
160
+ )
161
+ }
162
+
163
+ function DeleteAddressButton({ addressId }: { addressId: string }) {
164
+ const [pending, setPending] = useState(false)
165
+
166
+ async function handleDelete() {
167
+ if (!confirm("Delete this address?")) return
168
+ setPending(true)
169
+ try {
170
+ await deleteCustomerAddress(addressId)
171
+ window.location.reload()
172
+ } finally {
173
+ setPending(false)
174
+ }
175
+ }
176
+
177
+ return (
178
+ <button
179
+ type="button"
180
+ onClick={handleDelete}
181
+ disabled={pending}
182
+ className="text-xs text-brand-sale hover:underline disabled:opacity-60"
183
+ >
184
+ {pending ? "Deleting…" : "Delete"}
185
+ </button>
186
+ )
187
+ }
188
+
189
+ function Field({
190
+ label,
191
+ name,
192
+ type = "text",
193
+ defaultValue,
194
+ className = "",
195
+ }: {
196
+ label: string
197
+ name: string
198
+ type?: string
199
+ defaultValue?: string
200
+ className?: string
201
+ }) {
202
+ return (
203
+ <label className={`block ${className}`}>
204
+ <span className="text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-1.5 block">
205
+ {label}
206
+ </span>
207
+ <input
208
+ name={name}
209
+ type={type}
210
+ defaultValue={defaultValue}
211
+ required={name !== "address_2"}
212
+ className="w-full border border-cart-border rounded px-3 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
213
+ />
214
+ </label>
215
+ )
216
+ }
@@ -0,0 +1,60 @@
1
+ "use client"
2
+
3
+ import type { HttpTypes } from "@medusajs/types"
4
+ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
5
+ import { formatPrice } from "@pradip1995/segment-primitives/format-price"
6
+ import { logoutGuest } from "@pradip1995/commerce-core/data/guest"
7
+
8
+ export default function AccountGuestOrders({
9
+ orders,
10
+ countryCode,
11
+ }: {
12
+ orders: HttpTypes.StoreOrder[]
13
+ countryCode: string
14
+ }) {
15
+ async function handleSignOut() {
16
+ await logoutGuest()
17
+ window.location.href = `/${countryCode}/account`
18
+ }
19
+
20
+ return (
21
+ <div className="space-y-6">
22
+ <div className="flex flex-wrap items-start justify-between gap-4">
23
+ <div>
24
+ <h2 className="section-heading text-xl mb-1">Guest orders</h2>
25
+ <p className="text-sm text-muted">Orders linked to your verified email.</p>
26
+ </div>
27
+ <button type="button" onClick={handleSignOut} className="btn-outline text-xs">
28
+ End guest session
29
+ </button>
30
+ </div>
31
+
32
+ {orders.length === 0 ? (
33
+ <div className="card-surface rounded-2xl p-8 text-center text-sm text-muted">
34
+ No guest orders found for this session.
35
+ </div>
36
+ ) : (
37
+ <ul className="space-y-3">
38
+ {orders.map((order) => (
39
+ <li key={order.id}>
40
+ <LocalizedLink
41
+ href={`/orders/${order.id}`}
42
+ className="card-surface rounded-2xl p-5 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
43
+ >
44
+ <div>
45
+ <p className="font-semibold text-heading text-sm">Order #{order.display_id}</p>
46
+ <p className="text-xs text-muted mt-1 capitalize">
47
+ {order.status?.replace(/_/g, " ")}
48
+ </p>
49
+ </div>
50
+ <p className="text-sm font-semibold text-brand-accent shrink-0">
51
+ {formatPrice(order.total, order.currency_code)}
52
+ </p>
53
+ </LocalizedLink>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ )}
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,81 @@
1
+ "use client"
2
+
3
+ import { useMemo, useState } from "react"
4
+ import type { HttpTypes } from "@medusajs/types"
5
+ import LocalizedLink from "@pradip1995/segment-primitives/localized-link"
6
+ import { formatPrice } from "@pradip1995/segment-primitives/format-price"
7
+
8
+ export default function AccountOrders({
9
+ orders,
10
+ }: {
11
+ orders: HttpTypes.StoreOrder[]
12
+ }) {
13
+ const [query, setQuery] = useState("")
14
+
15
+ const filtered = useMemo(() => {
16
+ const q = query.trim().toLowerCase()
17
+ if (!q) return orders
18
+ return orders.filter((order) => {
19
+ const id = String(order.display_id ?? order.id)
20
+ const status = String(order.status ?? "").replace(/_/g, " ")
21
+ return id.includes(q) || status.toLowerCase().includes(q)
22
+ })
23
+ }, [orders, query])
24
+
25
+ return (
26
+ <div className="space-y-6">
27
+ <div>
28
+ <h2 className="section-heading text-xl mb-1">Order history</h2>
29
+ <p className="text-sm text-muted">View and track your past orders.</p>
30
+ </div>
31
+
32
+ <input
33
+ type="search"
34
+ value={query}
35
+ onChange={(e) => setQuery(e.target.value)}
36
+ placeholder="Search by order number or status"
37
+ className="w-full border border-cart-border rounded-lg px-4 py-2.5 text-sm bg-surface focus:outline-none focus:border-brand-accent"
38
+ />
39
+
40
+ {filtered.length === 0 ? (
41
+ <div className="card-surface rounded-2xl p-8 text-center">
42
+ <p className="text-muted text-sm mb-4">
43
+ {orders.length === 0 ? "No orders yet." : "No orders match your search."}
44
+ </p>
45
+ {orders.length === 0 && (
46
+ <LocalizedLink href="/store" className="btn-primary inline-block text-sm">
47
+ Start shopping
48
+ </LocalizedLink>
49
+ )}
50
+ </div>
51
+ ) : (
52
+ <ul className="space-y-3">
53
+ {filtered.map((order) => (
54
+ <li key={order.id}>
55
+ <LocalizedLink
56
+ href={`/orders/${order.id}`}
57
+ className="card-surface rounded-2xl p-5 flex flex-wrap items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
58
+ >
59
+ <div>
60
+ <p className="font-semibold text-heading">Order #{order.display_id}</p>
61
+ <p className="text-xs text-muted mt-1 capitalize">
62
+ {order.status?.replace(/_/g, " ")}
63
+ {order.created_at && (
64
+ <> · {new Date(order.created_at).toLocaleDateString()}</>
65
+ )}
66
+ </p>
67
+ <p className="text-xs text-muted mt-1">
68
+ {order.items?.length ?? 0} item{(order.items?.length ?? 0) === 1 ? "" : "s"}
69
+ </p>
70
+ </div>
71
+ <p className="text-sm font-semibold text-brand-accent shrink-0">
72
+ {formatPrice(order.total, order.currency_code)}
73
+ </p>
74
+ </LocalizedLink>
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ )}
79
+ </div>
80
+ )
81
+ }
@@ -1,9 +1,9 @@
1
1
  "use client"
2
2
 
3
- import { signout } from "@pradip1995/commerce-core/client/actions/customer"
3
+ 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
- import type { HttpTypes } from "@medusajs/types"
6
+ import { getProfileCompletion } from "./account-utils"
7
7
 
8
8
  export default function AccountOverview({
9
9
  customer,
@@ -15,39 +15,58 @@ export default function AccountOverview({
15
15
  countryCode: string
16
16
  }) {
17
17
  const name = [customer.first_name, customer.last_name].filter(Boolean).join(" ") || customer.email
18
+ const completion = getProfileCompletion(customer)
19
+ const addressCount = customer.addresses?.length ?? 0
18
20
 
19
21
  return (
20
22
  <div className="space-y-8">
21
- <div className="card-surface rounded-lg p-6">
22
- <p className="text-xs uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-2">My account</p>
23
- <h1 className="section-heading text-xl mb-1">{name}</h1>
24
- <p className="text-sm text-muted">{customer.email}</p>
25
- {customer.phone && <p className="text-sm text-muted mt-1">{customer.phone}</p>}
26
- <form action={signout.bind(null, countryCode)} className="mt-6">
27
- <button type="submit" className="btn-outline text-xs">
28
- Sign out
29
- </button>
30
- </form>
23
+ <div>
24
+ <p className="text-sm text-muted mb-1">Welcome back</p>
25
+ <h2 className="section-heading text-xl">Hello, {customer.first_name || name}</h2>
26
+ <p className="text-sm text-muted mt-1">Signed in as {customer.email}</p>
27
+ </div>
28
+
29
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
30
+ <StatCard
31
+ title="Profile"
32
+ value={`${completion}%`}
33
+ subtitle="Completed"
34
+ href="/account/profile"
35
+ />
36
+ <StatCard
37
+ title="Addresses"
38
+ value={String(addressCount)}
39
+ subtitle="Saved"
40
+ href="/account/addresses"
41
+ />
31
42
  </div>
32
43
 
33
44
  <div>
34
- <h2 className="text-sm font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading mb-4">
35
- Recent orders
36
- </h2>
45
+ <div className="flex items-center justify-between gap-4 mb-4">
46
+ <h3 className="text-sm font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-heading">
47
+ Recent orders
48
+ </h3>
49
+ {orders.length > 0 && (
50
+ <LocalizedLink href="/account/orders" className="text-xs text-brand-accent hover:underline">
51
+ View all
52
+ </LocalizedLink>
53
+ )}
54
+ </div>
55
+
37
56
  {orders.length === 0 ? (
38
- <div className="card-surface rounded-lg p-8 text-center">
57
+ <div className="card-surface rounded-2xl p-8 text-center">
39
58
  <p className="text-muted text-sm mb-4">No orders yet.</p>
40
- <LocalizedLink href="/store" className="btn-primary inline-block">
59
+ <LocalizedLink href="/store" className="btn-primary inline-block text-sm">
41
60
  Start shopping
42
61
  </LocalizedLink>
43
62
  </div>
44
63
  ) : (
45
64
  <ul className="space-y-3">
46
- {orders.map((order) => (
65
+ {orders.slice(0, 5).map((order) => (
47
66
  <li key={order.id}>
48
67
  <LocalizedLink
49
68
  href={`/orders/${order.id}`}
50
- className="card-surface rounded-lg p-4 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
69
+ className="card-surface rounded-2xl p-4 flex items-center justify-between gap-4 hover:border-brand-accent transition-colors block"
51
70
  >
52
71
  <div>
53
72
  <p className="font-medium text-heading text-sm">Order #{order.display_id}</p>
@@ -67,3 +86,26 @@ export default function AccountOverview({
67
86
  </div>
68
87
  )
69
88
  }
89
+
90
+ function StatCard({
91
+ title,
92
+ value,
93
+ subtitle,
94
+ href,
95
+ }: {
96
+ title: string
97
+ value: string
98
+ subtitle: string
99
+ href: string
100
+ }) {
101
+ return (
102
+ <LocalizedLink
103
+ href={href}
104
+ className="card-surface rounded-2xl p-5 hover:border-brand-accent transition-colors block"
105
+ >
106
+ <p className="text-xs uppercase tracking-[var(--letter-spacing-nav)] text-muted mb-2">{title}</p>
107
+ <p className="text-3xl font-semibold text-heading leading-none">{value}</p>
108
+ <p className="text-xs text-muted mt-2 uppercase">{subtitle}</p>
109
+ </LocalizedLink>
110
+ )
111
+ }