@pradip1995/segment-login-template 0.3.0 → 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 +5 -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 +61 -19
- 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 +157 -2
- package/src/commerce-auth-otp-modal.tsx +104 -0
- package/src/google-auth-section.tsx +69 -8
- package/src/index.ts +4 -0
- package/src/login-form.tsx +143 -87
- package/src/otp-component.ts +14 -0
- package/src/otp-verification-modal.tsx +112 -0
- package/src/register-form.tsx +130 -104
- package/src/segment.tsx +63 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pradip1995/segment-login-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -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
|
+
}
|
package/src/account-overview.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
22
|
-
<p className="text-
|
|
23
|
-
<
|
|
24
|
-
<p className="text-sm text-muted">{customer.email}</p>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
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-
|
|
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-
|
|
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
|
+
}
|