@ecomiq/storefront 2.0.11 → 2.0.14

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.
Files changed (34) hide show
  1. package/bin/ecomiq.mjs +3 -1
  2. package/package.json +4 -2
  3. package/src/auth/create-storefront-auth.ts +65 -6
  4. package/src/auth/ecomiq-oauth-password-constants.ts +1 -3
  5. package/src/auth/ecomiq-oauth-password-plugin.ts +12 -17
  6. package/src/auth/ecomiq-storefront-sign-in.ts +0 -3
  7. package/src/auth/secure-cookies.ts +23 -0
  8. package/src/auth/storefront-auth-base-url.ts +117 -0
  9. package/src/codegen.mjs +27 -1
  10. package/src/components/account/customer-addresses-panel.tsx +678 -0
  11. package/src/components/account/customer-order-detail-panel.tsx +285 -0
  12. package/src/components/account/customer-orders-panel.tsx +192 -0
  13. package/src/components/account/customer-profile-panel.tsx +379 -0
  14. package/src/components/checkout/checkout-contact-section.tsx +17 -11
  15. package/src/components/checkout/checkout-session-view.tsx +83 -2
  16. package/src/components/checkout/checkout-shipping-address-section.tsx +107 -2
  17. package/src/components/checkout/checkout-steps.ts +15 -0
  18. package/src/components/checkout/map-customer-to-checkout.ts +188 -0
  19. package/src/components/checkout/use-checkout-customer-prefill.ts +409 -0
  20. package/src/components/checkout/use-checkout-session.ts +5 -4
  21. package/src/config.d.ts +4 -0
  22. package/src/config.mjs +14 -0
  23. package/src/hooks/index.ts +9 -0
  24. package/src/hooks/recently-viewed.ts +125 -0
  25. package/src/index.ts +63 -0
  26. package/src/pages/account/account-page-skeleton.tsx +69 -0
  27. package/src/pages/account/account-page-view.tsx +270 -0
  28. package/src/pages/account/account-shell.tsx +101 -0
  29. package/src/pages/account/parse-account-route.ts +36 -0
  30. package/src/pages/account.tsx +16 -0
  31. package/src/pages/checkout.$checkoutId.tsx +1 -1
  32. package/src/providers/auth.tsx +11 -2
  33. package/src/proxy-api.ts +20 -0
  34. package/src/vite-inline.mjs +54 -1
@@ -0,0 +1,285 @@
1
+ import { ArrowLeft, CreditCard, Loader2, MapPin, Package } from 'lucide-react'
2
+ import {
3
+ customerOrderStatusTone,
4
+ formatCustomerOrderStatus,
5
+ formatOrderDate,
6
+ formatOrderDateTime,
7
+ type CustomerOrderDetail,
8
+ } from '@ecomiq/core'
9
+ import { Link } from '../../link'
10
+ import { buttonVariants, cn } from '../../ui'
11
+ import { formatMoney } from '../checkout/format'
12
+
13
+ function statusBadgeClass(
14
+ tone: ReturnType<typeof customerOrderStatusTone>,
15
+ ): string {
16
+ switch (tone) {
17
+ case 'success':
18
+ return 'bg-emerald-50 text-emerald-800 ring-emerald-200'
19
+ case 'warning':
20
+ return 'bg-amber-50 text-amber-900 ring-amber-200'
21
+ case 'danger':
22
+ return 'bg-red-50 text-red-800 ring-red-200'
23
+ case 'info':
24
+ return 'bg-sky-50 text-sky-900 ring-sky-200'
25
+ default:
26
+ return 'bg-muted text-foreground ring-border'
27
+ }
28
+ }
29
+
30
+ export type CustomerOrderDetailPanelProps = {
31
+ order?: CustomerOrderDetail | null
32
+ isLoading: boolean
33
+ errorMessage?: string | null
34
+ backHref: string
35
+ /** Genera URL de producto si hay handle (p. ej. `/products/{handle}`). */
36
+ productHref?: (handle: string) => string
37
+ language?: string
38
+ }
39
+
40
+ export function CustomerOrderDetailPanel({
41
+ order,
42
+ isLoading,
43
+ errorMessage,
44
+ backHref,
45
+ productHref,
46
+ language = 'es-PE',
47
+ }: CustomerOrderDetailPanelProps) {
48
+ if (isLoading) {
49
+ return (
50
+ <div className="flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-10 text-sm text-muted-foreground">
51
+ <Loader2 className="size-4 shrink-0 animate-spin" />
52
+ Cargando detalle del pedido…
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (errorMessage) {
58
+ return (
59
+ <div className="space-y-4">
60
+ <Link
61
+ to={backHref}
62
+ className={cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'gap-2')}
63
+ >
64
+ <ArrowLeft className="size-4" />
65
+ Volver a mis pedidos
66
+ </Link>
67
+ <p className="rounded-md border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
68
+ {errorMessage}
69
+ </p>
70
+ </div>
71
+ )
72
+ }
73
+
74
+ if (!order) return null
75
+
76
+ const tone = customerOrderStatusTone(order.status)
77
+ const timeline = [...order.statusTimeline].sort(
78
+ (a, b) =>
79
+ new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime(),
80
+ )
81
+ const money = (value: number) =>
82
+ formatMoney(value, language, order.currencyCode)
83
+
84
+ return (
85
+ <div className="space-y-6">
86
+ <Link
87
+ to={backHref}
88
+ className={cn(
89
+ buttonVariants({ variant: 'ghost', size: 'sm' }),
90
+ '-ml-2 gap-2 text-muted-foreground',
91
+ )}
92
+ >
93
+ <ArrowLeft className="size-4" />
94
+ Volver a mis pedidos
95
+ </Link>
96
+
97
+ <div className="rounded-lg border border-border bg-background p-5 sm:p-6">
98
+ <div className="flex flex-wrap items-start justify-between gap-4">
99
+ <div>
100
+ <p className="text-sm text-muted-foreground">
101
+ Pedido del {formatOrderDate(order.orderDate)}
102
+ </p>
103
+ <h2 className="mt-1 text-xl font-semibold text-foreground sm:text-2xl">
104
+ Pedido n.º {order.orderNumber}
105
+ </h2>
106
+ </div>
107
+ <span
108
+ className={cn(
109
+ 'inline-flex rounded-full px-3 py-1 text-sm font-medium ring-1 ring-inset',
110
+ statusBadgeClass(tone),
111
+ )}
112
+ >
113
+ {formatCustomerOrderStatus(order.status)}
114
+ </span>
115
+ </div>
116
+
117
+ <div className="mt-6 grid gap-6 lg:grid-cols-3">
118
+ <div className="space-y-6 lg:col-span-2">
119
+ <section>
120
+ <h3 className="text-base font-semibold text-foreground">Productos</h3>
121
+ <ul className="mt-3 divide-y divide-border rounded-lg border border-border">
122
+ {order.items.map((item) => {
123
+ const href =
124
+ item.productHandle && productHref
125
+ ? productHref(item.productHandle)
126
+ : null
127
+ return (
128
+ <li key={item.id} className="flex gap-4 p-4">
129
+ <div className="size-20 shrink-0 overflow-hidden rounded-md border border-border bg-muted/40">
130
+ {item.thumbnail ? (
131
+ <img
132
+ src={item.thumbnail}
133
+ alt=""
134
+ className="size-full object-cover"
135
+ />
136
+ ) : (
137
+ <div className="flex size-full items-center justify-center text-muted-foreground">
138
+ <Package className="size-8" />
139
+ </div>
140
+ )}
141
+ </div>
142
+ <div className="min-w-0 flex-1">
143
+ {href ? (
144
+ <Link
145
+ to={href}
146
+ className="font-medium text-primary hover:underline"
147
+ >
148
+ {item.title}
149
+ </Link>
150
+ ) : (
151
+ <p className="font-medium text-foreground">{item.title}</p>
152
+ )}
153
+ {item.variantTitle ? (
154
+ <p className="text-sm text-muted-foreground">
155
+ {item.variantTitle}
156
+ </p>
157
+ ) : null}
158
+ <p className="mt-1 text-sm text-muted-foreground">
159
+ Cantidad: {item.quantity}
160
+ </p>
161
+ <p className="mt-1 text-sm font-medium text-foreground">
162
+ {money(item.total)}
163
+ </p>
164
+ </div>
165
+ </li>
166
+ )
167
+ })}
168
+ </ul>
169
+ </section>
170
+
171
+ {timeline.length > 0 ? (
172
+ <section>
173
+ <h3 className="text-base font-semibold text-foreground">
174
+ Seguimiento
175
+ </h3>
176
+ <ol className="mt-3 space-y-0 border-l-2 border-primary/30 pl-4">
177
+ {timeline.map((entry, index) => (
178
+ <li
179
+ key={`${entry.key}-${entry.occurredAt}`}
180
+ className="relative pb-6 last:pb-0"
181
+ >
182
+ <span
183
+ className={cn(
184
+ 'absolute -left-[21px] top-1 size-2.5 rounded-full ring-2 ring-background',
185
+ index === 0 ? 'bg-primary' : 'bg-muted-foreground/40',
186
+ )}
187
+ />
188
+ <p className="text-sm font-medium text-foreground">
189
+ {entry.label}
190
+ </p>
191
+ <p className="text-xs text-muted-foreground">
192
+ {formatOrderDateTime(entry.occurredAt)}
193
+ </p>
194
+ {entry.description ? (
195
+ <p className="mt-1 text-sm text-muted-foreground">
196
+ {entry.description}
197
+ </p>
198
+ ) : null}
199
+ </li>
200
+ ))}
201
+ </ol>
202
+ </section>
203
+ ) : null}
204
+ </div>
205
+
206
+ <div className="space-y-4">
207
+ <section className="rounded-lg border border-border bg-muted/30 p-4">
208
+ <h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
209
+ <MapPin className="size-4" />
210
+ Dirección de envío
211
+ </h3>
212
+ <p className="mt-2 text-sm leading-relaxed text-muted-foreground">
213
+ {order.shippingAddressSummary ??
214
+ [
215
+ order.shippingAddress?.address1,
216
+ order.shippingAddress?.address2,
217
+ order.shippingAddress?.district,
218
+ order.shippingAddress?.province,
219
+ order.shippingAddress?.postalCode,
220
+ ]
221
+ .filter(Boolean)
222
+ .join(', ')}
223
+ </p>
224
+ {order.shippingAddress?.phone ? (
225
+ <p className="mt-2 text-sm text-muted-foreground">
226
+ {order.shippingAddress.phone}
227
+ </p>
228
+ ) : null}
229
+ </section>
230
+
231
+ <section className="rounded-lg border border-border bg-muted/30 p-4">
232
+ <h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
233
+ <CreditCard className="size-4" />
234
+ Resumen de pago
235
+ </h3>
236
+ <dl className="mt-3 space-y-2 text-sm">
237
+ <div className="flex justify-between text-muted-foreground">
238
+ <dt>Subtotal</dt>
239
+ <dd>{money(order.subTotal)}</dd>
240
+ </div>
241
+ {order.discount > 0 ? (
242
+ <div className="flex justify-between text-muted-foreground">
243
+ <dt>Descuento</dt>
244
+ <dd className="text-emerald-700">-{money(order.discount)}</dd>
245
+ </div>
246
+ ) : null}
247
+ <div className="flex justify-between text-muted-foreground">
248
+ <dt>Envío</dt>
249
+ <dd>{money(order.shipping)}</dd>
250
+ </div>
251
+ {order.tax > 0 ? (
252
+ <div className="flex justify-between text-muted-foreground">
253
+ <dt>Impuestos</dt>
254
+ <dd>{money(order.tax)}</dd>
255
+ </div>
256
+ ) : null}
257
+ <div className="flex justify-between border-t border-border pt-2 font-semibold text-foreground">
258
+ <dt>Total</dt>
259
+ <dd>{money(order.total)}</dd>
260
+ </div>
261
+ </dl>
262
+ {order.payments.length > 0 ? (
263
+ <ul className="mt-4 space-y-2 border-t border-border pt-3 text-sm">
264
+ {order.payments.map((p) => (
265
+ <li key={p.id} className="text-muted-foreground">
266
+ <span className="font-medium text-foreground">
267
+ {p.paymentMethod}
268
+ </span>
269
+ {' · '}
270
+ {money(p.amount)}
271
+ <span className="text-muted-foreground">
272
+ {' '}
273
+ ({p.paymentStatus})
274
+ </span>
275
+ </li>
276
+ ))}
277
+ </ul>
278
+ ) : null}
279
+ </section>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ )
285
+ }
@@ -0,0 +1,192 @@
1
+ import { ChevronRight, Loader2, Package } from 'lucide-react'
2
+ import {
3
+ customerOrderStatusTone,
4
+ formatCustomerOrderStatus,
5
+ formatOrderDate,
6
+ type CustomerOrderListItem,
7
+ } from '@ecomiq/core'
8
+ import { Link } from '../../link'
9
+ import { buttonVariants, cn } from '../../ui'
10
+ import { formatMoney } from '../checkout/format'
11
+
12
+ function statusBadgeClass(
13
+ tone: ReturnType<typeof customerOrderStatusTone>,
14
+ ): string {
15
+ switch (tone) {
16
+ case 'success':
17
+ return 'bg-emerald-50 text-emerald-800 ring-emerald-200'
18
+ case 'warning':
19
+ return 'bg-amber-50 text-amber-900 ring-amber-200'
20
+ case 'danger':
21
+ return 'bg-red-50 text-red-800 ring-red-200'
22
+ case 'info':
23
+ return 'bg-sky-50 text-sky-900 ring-sky-200'
24
+ default:
25
+ return 'bg-muted text-foreground ring-border'
26
+ }
27
+ }
28
+
29
+ export type CustomerOrdersPanelProps = {
30
+ orders?: CustomerOrderListItem[]
31
+ isLoading: boolean
32
+ errorMessage?: string | null
33
+ /** Prefijo de ruta, p. ej. `/account/orders` → enlace a `{base}/{orderId}`. */
34
+ orderDetailBasePath: string
35
+ shopHref?: string
36
+ language?: string
37
+ }
38
+
39
+ export function CustomerOrdersPanel({
40
+ orders,
41
+ isLoading,
42
+ errorMessage,
43
+ orderDetailBasePath,
44
+ shopHref = '/',
45
+ language = 'es-PE',
46
+ }: CustomerOrdersPanelProps) {
47
+ const list = orders ?? []
48
+ const base = orderDetailBasePath.replace(/\/$/, '')
49
+
50
+ return (
51
+ <div className="space-y-6">
52
+ <div>
53
+ <h2 className="text-lg font-medium text-foreground">Mis pedidos</h2>
54
+ <p className="mt-1 text-sm text-muted-foreground">
55
+ Rastrea envíos, revisa totales y vuelve a ver lo que compraste.
56
+ </p>
57
+ </div>
58
+
59
+ {isLoading ? (
60
+ <div className="flex items-center gap-2 rounded-lg border border-border bg-background px-4 py-8 text-sm text-muted-foreground">
61
+ <Loader2 className="size-4 shrink-0 animate-spin" />
62
+ Cargando pedidos…
63
+ </div>
64
+ ) : null}
65
+
66
+ {errorMessage && !isLoading ? (
67
+ <p className="rounded-md border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm text-destructive">
68
+ {errorMessage}
69
+ </p>
70
+ ) : null}
71
+
72
+ {!isLoading && !errorMessage && list.length === 0 ? (
73
+ <div className="rounded-lg border border-dashed border-border bg-muted/30 px-6 py-12 text-center">
74
+ <Package className="mx-auto mb-3 size-12 text-muted-foreground/70" />
75
+ <p className="text-base font-medium text-foreground">
76
+ Aún no tienes pedidos
77
+ </p>
78
+ <p className="mt-2 text-sm text-muted-foreground">
79
+ Cuando compres en esta tienda, aparecerán aquí con su estado de
80
+ envío.
81
+ </p>
82
+ <Link to={shopHref} className={cn(buttonVariants(), 'mt-6')}>
83
+ Seguir comprando
84
+ </Link>
85
+ </div>
86
+ ) : null}
87
+
88
+ {!isLoading && list.length > 0 ? (
89
+ <ul className="space-y-4">
90
+ {list.map((order) => {
91
+ const tone = customerOrderStatusTone(order.status)
92
+ const visibleItems = order.items.slice(0, 4)
93
+ const extraCount = Math.max(0, order.items.length - visibleItems.length)
94
+
95
+ return (
96
+ <li
97
+ key={order.orderId}
98
+ className="overflow-hidden rounded-lg border border-border bg-background"
99
+ >
100
+ <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border bg-muted/40 px-4 py-3 sm:px-5">
101
+ <div className="min-w-0 space-y-0.5">
102
+ <p className="text-sm text-muted-foreground">
103
+ Pedido realizado el{' '}
104
+ <span className="font-medium text-foreground">
105
+ {formatOrderDate(order.orderDate)}
106
+ </span>
107
+ </p>
108
+ <p className="text-sm font-semibold text-foreground">
109
+ N.º de pedido: {order.orderNumber}
110
+ </p>
111
+ </div>
112
+ <div className="flex flex-wrap items-center gap-3">
113
+ <span
114
+ className={cn(
115
+ 'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset',
116
+ statusBadgeClass(tone),
117
+ )}
118
+ >
119
+ {formatCustomerOrderStatus(order.status)}
120
+ </span>
121
+ <p className="text-base font-semibold text-foreground">
122
+ {formatMoney(order.total, language, order.currencyCode)}
123
+ </p>
124
+ </div>
125
+ </div>
126
+
127
+ <div className="px-4 py-4 sm:px-5">
128
+ <div className="flex flex-wrap gap-3">
129
+ {visibleItems.map((item) => (
130
+ <div
131
+ key={item.id}
132
+ className="flex w-full max-w-[280px] gap-3 sm:w-auto sm:min-w-[220px] sm:flex-1"
133
+ >
134
+ <div className="size-16 shrink-0 overflow-hidden rounded-md border border-border bg-muted/40">
135
+ {item.thumbnail ? (
136
+ <img
137
+ src={item.thumbnail}
138
+ alt=""
139
+ className="size-full object-cover"
140
+ />
141
+ ) : (
142
+ <div className="flex size-full items-center justify-center text-muted-foreground">
143
+ <Package className="size-6" />
144
+ </div>
145
+ )}
146
+ </div>
147
+ <div className="min-w-0 flex-1 text-sm">
148
+ <p className="line-clamp-2 font-medium text-foreground">
149
+ {item.title}
150
+ </p>
151
+ {item.variantTitle ? (
152
+ <p className="text-muted-foreground">
153
+ {item.variantTitle}
154
+ </p>
155
+ ) : null}
156
+ <p className="text-muted-foreground">
157
+ Cant.: {item.quantity}
158
+ </p>
159
+ </div>
160
+ </div>
161
+ ))}
162
+ {extraCount > 0 ? (
163
+ <p className="self-center text-sm text-muted-foreground">
164
+ +{extraCount} producto{extraCount === 1 ? '' : 's'} más
165
+ </p>
166
+ ) : null}
167
+ </div>
168
+
169
+ {order.shippingAddressSummary ? (
170
+ <p className="mt-3 line-clamp-2 text-xs text-muted-foreground">
171
+ Enviar a: {order.shippingAddressSummary}
172
+ </p>
173
+ ) : null}
174
+
175
+ <div className="mt-4 flex flex-wrap gap-2 border-t border-border pt-4">
176
+ <Link
177
+ to={`${base}/${encodeURIComponent(order.orderId)}`}
178
+ className={cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'gap-1')}
179
+ >
180
+ Ver detalles del pedido
181
+ <ChevronRight className="size-4" />
182
+ </Link>
183
+ </div>
184
+ </div>
185
+ </li>
186
+ )
187
+ })}
188
+ </ul>
189
+ ) : null}
190
+ </div>
191
+ )
192
+ }