@ecomiq/storefront 2.1.3 → 3.0.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.
Files changed (317) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -123
  3. package/dist/account-Dx4e3syi.js +258 -0
  4. package/dist/account-forms-AVk71ko7.js +39 -0
  5. package/dist/account-forms.d.ts +13 -0
  6. package/dist/account.d.ts +240 -0
  7. package/dist/address-defaults-BqjRhS82.js +14 -0
  8. package/dist/address-defaults.d.ts +11 -0
  9. package/dist/analytics/analytics-CG1Qagwl.js +148 -0
  10. package/dist/analytics/analytics.d.ts +1 -0
  11. package/dist/analytics/config-DJr-GRhs.js +62 -0
  12. package/dist/analytics/config.d.ts +22 -0
  13. package/dist/analytics/consent-B13rBpBs.js +57 -0
  14. package/dist/analytics/consent-banner-9QvRRy8v.js +239 -0
  15. package/dist/analytics/consent-banner.d.ts +20 -0
  16. package/dist/analytics/cookies-pLozllyB.js +100 -0
  17. package/dist/analytics/cookies.d.ts +6 -0
  18. package/dist/analytics/destinations-iECrlLIr.js +144 -0
  19. package/dist/analytics/events-Dt81tIp1.js +102 -0
  20. package/dist/analytics/events.d.ts +51 -0
  21. package/dist/analytics/tracker-p4yaoOyk.js +51 -0
  22. package/dist/api-cache-DsuIn5hX.js +159 -0
  23. package/dist/bank-accounts-DlEczQQ5.js +81 -0
  24. package/dist/bank-accounts.d.ts +4 -0
  25. package/dist/captcha-DAqaIx8m.js +117 -0
  26. package/dist/captcha.d.ts +34 -0
  27. package/dist/cart-D7DxZ2lD.js +30 -0
  28. package/dist/cart-actions-Bsub_FFG.js +116 -0
  29. package/dist/cart-actions.d.ts +15 -0
  30. package/dist/cart-drawer-B7PPsR69.js +22 -0
  31. package/dist/cart-drawer.d.ts +9 -0
  32. package/dist/cart-form-y9P1r_EX.js +81 -0
  33. package/dist/cart-form.d.ts +24 -0
  34. package/dist/cart-summary-ur2VWKr-.js +89 -0
  35. package/dist/cart-summary.d.ts +31 -0
  36. package/dist/cart.d.ts +4 -0
  37. package/dist/catalog-7gJ-l5hW.js +67 -0
  38. package/dist/catalog-hooks-B7ZP4wYY.js +312 -0
  39. package/dist/catalog-hooks.d.ts +111 -0
  40. package/dist/catalog.d.ts +5 -0
  41. package/dist/checkout-DVWSXANi.js +474 -0
  42. package/dist/checkout-form-CbOIdENC.js +131 -0
  43. package/dist/checkout-form.d.ts +20 -0
  44. package/dist/checkout.d.ts +86 -0
  45. package/dist/complaint-book-BvwLvfx0.js +117 -0
  46. package/dist/complaint-book.d.ts +62 -0
  47. package/dist/culqi-authentication-BGr20vAJ.js +204 -0
  48. package/dist/culqi-authentication.d.ts +15 -0
  49. package/dist/culqi-checkout-CTXpKgp_.js +55 -0
  50. package/dist/culqi-three-ds-HP3mfrvV.js +33 -0
  51. package/dist/culqi-three-ds.d.ts +29 -0
  52. package/dist/customer-avatar-CJXSDroK.js +12 -0
  53. package/dist/customer-avatar.d.ts +5 -0
  54. package/dist/customer-cookies-DHOgFAMV.js +45 -0
  55. package/dist/customer-csrf-C1oDX-Py.js +17 -0
  56. package/dist/customer-csrf.d.ts +3 -0
  57. package/dist/customer-document-fields-C1K-_DAD.js +70 -0
  58. package/dist/customer-document-fields.d.ts +9 -0
  59. package/dist/customer-login-form-C7AC6vXd.js +144 -0
  60. package/dist/customer-login-form.d.ts +17 -0
  61. package/dist/customer-registration-form-BEm_NEfP.js +93 -0
  62. package/dist/customer-registration-form.d.ts +8 -0
  63. package/dist/document-url-CGsAl5Tf.js +9 -0
  64. package/dist/documents-T1mzc_j8.js +38 -0
  65. package/dist/documents.d.ts +16 -0
  66. package/dist/entries/account.d.ts +11 -0
  67. package/dist/entries/account.js +10 -0
  68. package/dist/entries/analytics.d.ts +6 -0
  69. package/dist/entries/analytics.js +5 -0
  70. package/dist/entries/cart.d.ts +8 -0
  71. package/dist/entries/cart.js +7 -0
  72. package/dist/entries/checkout.d.ts +12 -0
  73. package/dist/entries/checkout.js +11 -0
  74. package/dist/entries/order.d.ts +11 -0
  75. package/dist/entries/order.js +11 -0
  76. package/dist/favicon-YpyhVJ9H.js +28 -0
  77. package/dist/favicon.d.ts +4 -0
  78. package/dist/form-BtQcbXlJ.js +18 -0
  79. package/dist/form-contracts-BNpHaTGJ.js +166 -0
  80. package/dist/form-contracts.d.ts +42 -0
  81. package/dist/form.d.ts +3 -0
  82. package/dist/hidden-CCWtOWKR.js +3 -0
  83. package/dist/hidden.d.ts +4 -0
  84. package/dist/index.d.ts +50 -0
  85. package/dist/index.js +50 -0
  86. package/dist/invoice-fields-BG0HjL6T.js +103 -0
  87. package/dist/invoice-fields.d.ts +39 -0
  88. package/dist/links-kFxBZfdb.js +96 -0
  89. package/dist/links.d.ts +21 -0
  90. package/dist/mercadopago-checkout-DC7RXa8k.js +27 -0
  91. package/dist/mercadopago-dialog-UTQev0MG.js +180 -0
  92. package/dist/mercadopago-dialog.d.ts +21 -0
  93. package/dist/money-DtGOtDri.js +32 -0
  94. package/dist/money.d.ts +1 -0
  95. package/dist/optimistic-cart-BveVOHuh.js +53 -0
  96. package/dist/optimistic-cart.d.ts +16 -0
  97. package/dist/order-display-DTt5WBbc.js +112 -0
  98. package/dist/order-display.d.ts +20 -0
  99. package/dist/order-draft-DxV1Uhm3.js +38 -0
  100. package/dist/order-draft.d.ts +16 -0
  101. package/dist/order-pages-Dm6l5m0m.js +38 -0
  102. package/dist/order-pages.d.ts +12 -0
  103. package/dist/order-payment-continuation-D_d5PGA4.js +212 -0
  104. package/dist/order-payment-continuation.d.ts +10 -0
  105. package/dist/order-payment-in-review-x4gdLFHZ.js +65 -0
  106. package/dist/order-payment-in-review.d.ts +5 -0
  107. package/dist/order-status-BJXLJwxq.js +162 -0
  108. package/dist/order-status.d.ts +89 -0
  109. package/dist/order-view-9IlBg5AF.js +62 -0
  110. package/dist/order-view.d.ts +12 -0
  111. package/dist/page-action-Ctkf4xC6.js +7 -0
  112. package/dist/page-seo-a5FJl8x5.js +262 -0
  113. package/dist/page-seo.d.ts +34 -0
  114. package/dist/pages-Ca_Gt6mq.js +150 -0
  115. package/dist/pages.d.ts +45 -0
  116. package/dist/payment-dialog-frame-B-Q_xQ94.js +57 -0
  117. package/dist/payment-logo-BoPaGJc_.js +46 -0
  118. package/dist/payment-logo-svgs-By9lDTLK.js +20 -0
  119. package/dist/payment-logo.d.ts +11 -0
  120. package/dist/payment-logos-BYvdwYyJ.js +23 -0
  121. package/dist/payment-methods-B8ocTgFZ.js +73 -0
  122. package/dist/payment-methods.d.ts +58 -0
  123. package/dist/payment-return-5cjypwIw.js +7 -0
  124. package/dist/place-CmW4hBWY.js +13 -0
  125. package/dist/place.d.ts +4 -0
  126. package/dist/preview-BiIBif3C.js +427 -0
  127. package/dist/preview-address-Cf0sJ-9p.js +21 -0
  128. package/dist/preview-cart-Dg2CVktS.js +376 -0
  129. package/dist/product-preview-BspMAecJ.js +51 -0
  130. package/dist/product-preview.d.ts +5 -0
  131. package/dist/product-variants-BNDHvs1y.js +46 -0
  132. package/dist/product-variants.d.ts +12 -0
  133. package/dist/public-pages-XtwlaLwt.js +194 -0
  134. package/dist/public-pages.d.ts +67 -0
  135. package/dist/registration-CpDcv8ql.js +91 -0
  136. package/dist/registration.d.ts +12 -0
  137. package/dist/request-security-CRsRMgwj.js +76 -0
  138. package/dist/rich-text-M-Ju8RL4.js +9 -0
  139. package/dist/rich-text.d.ts +5 -0
  140. package/dist/route-text-DcJw2SsK.js +9 -0
  141. package/dist/route-text.d.ts +4 -0
  142. package/dist/section-preview-CmsMl3yu.js +83 -0
  143. package/dist/section-preview.d.ts +12 -0
  144. package/dist/sections/account-addresses.d.ts +18 -0
  145. package/dist/sections/account-addresses.js +375 -0
  146. package/dist/sections/account-order.d.ts +12 -0
  147. package/dist/sections/account-order.js +592 -0
  148. package/dist/sections/account-orders.d.ts +8 -0
  149. package/dist/sections/account-orders.js +359 -0
  150. package/dist/sections/account-profile.d.ts +16 -0
  151. package/dist/sections/account-profile.js +123 -0
  152. package/dist/sections/checkout.d.ts +24 -0
  153. package/dist/sections/checkout.js +963 -0
  154. package/dist/sections/complaint-book.d.ts +8 -0
  155. package/dist/sections/complaint-book.js +538 -0
  156. package/dist/sections/design-system.d.ts +1 -0
  157. package/dist/sections/design-system.js +310 -0
  158. package/dist/sections/draft-invoice.d.ts +20 -0
  159. package/dist/sections/draft-invoice.js +158 -0
  160. package/dist/sections/order-tracking.d.ts +19 -0
  161. package/dist/sections/order-tracking.js +141 -0
  162. package/dist/sections-CNUMWTF_.js +77 -0
  163. package/dist/sections.d.ts +19 -0
  164. package/dist/seo-GpDRhAAA.js +78 -0
  165. package/dist/session-CKvmFJyg.js +379 -0
  166. package/dist/session.d.ts +24 -0
  167. package/dist/settings-B0NO2A6Y.js +78 -0
  168. package/dist/settings.d.ts +51 -0
  169. package/dist/source-marker-DhqSkW7n.js +4 -0
  170. package/dist/sources-B-3JFJfb.js +46 -0
  171. package/dist/sources.d.ts +7 -0
  172. package/dist/store-8Q-_LFrQ.js +152 -0
  173. package/dist/store-contact--hY6CoFb.js +19 -0
  174. package/dist/store-contact.d.ts +6 -0
  175. package/dist/store.d.ts +17 -0
  176. package/dist/storefront-language-DrMjVbmk.js +29 -0
  177. package/dist/storefront-language.d.ts +14 -0
  178. package/dist/storefront-text-D25ae1qf.js +11 -0
  179. package/dist/storefront-text-en-ljjjSS4q.js +1030 -0
  180. package/dist/storefront-text-pt-BtHaJLcp.js +1030 -0
  181. package/dist/storefront-text.d.ts +4 -0
  182. package/dist/stripe-dialog-Ev9s3Z62.js +396 -0
  183. package/dist/stripe-dialog.d.ts +30 -0
  184. package/dist/telemetry-HuZXWBPi.js +114 -0
  185. package/dist/telemetry.d.ts +10 -0
  186. package/dist/templates-CAqoNyTW.js +40 -0
  187. package/dist/templates.d.ts +15 -0
  188. package/dist/urls-CpGQPxvf.js +71 -0
  189. package/dist/urls.d.ts +18 -0
  190. package/dist/use-checkout-CZcxXJXi.js +42 -0
  191. package/dist/use-checkout-payment-9R-zacrJ.js +80 -0
  192. package/dist/use-checkout-payment.d.ts +18 -0
  193. package/dist/use-checkout.d.ts +36 -0
  194. package/dist/use-culqi-checkout-CrjLAipU.js +109 -0
  195. package/dist/use-order-payment-BKIz_IO0.js +45 -0
  196. package/dist/use-order-payment.d.ts +18 -0
  197. package/dist/use-payment-token-CErx-tN-.js +105 -0
  198. package/dist/vite-payment-logos-kziu7wKb.js +27 -0
  199. package/dist/vite-payment-logos.d.ts +24 -0
  200. package/dist/vite-public-assets-DW2fgtVb.js +209 -0
  201. package/dist/vite-public-assets.d.ts +29 -0
  202. package/dist/vite-sections-DTBr0-Kg.js +464 -0
  203. package/dist/vite-sections.d.ts +21 -0
  204. package/dist/vite-theme-ui-BMO-CDi3.js +41 -0
  205. package/dist/vite-theme-ui.d.ts +10 -0
  206. package/dist/vite.d.ts +36 -0
  207. package/dist/vite.js +127 -0
  208. package/dist/voucher-payment-CVFDZ1L8.js +66 -0
  209. package/dist/worker-CNi-gzCu.js +86 -0
  210. package/dist/worker.d.ts +27 -0
  211. package/package.json +100 -81
  212. package/src/sections/account-addresses.tsx +215 -0
  213. package/src/sections/account-order.tsx +424 -0
  214. package/src/sections/account-orders.tsx +259 -0
  215. package/src/sections/account-profile.tsx +70 -0
  216. package/src/sections/checkout.tsx +586 -0
  217. package/src/sections/complaint-book.tsx +325 -0
  218. package/src/sections/design-system.tsx +146 -0
  219. package/src/sections/draft-invoice.tsx +105 -0
  220. package/src/sections/order-tracking.tsx +81 -0
  221. package/src/vite-client.d.ts +4 -0
  222. package/bin/ecomiq.mjs +0 -179
  223. package/public/design-bridge.js +0 -318
  224. package/src/api-proxy.ts +0 -194
  225. package/src/auth/create-storefront-auth.ts +0 -94
  226. package/src/auth/ecomiq-oauth-password-client.ts +0 -26
  227. package/src/auth/ecomiq-oauth-password-constants.ts +0 -24
  228. package/src/auth/ecomiq-oauth-password-plugin.ts +0 -294
  229. package/src/auth/ecomiq-oauth-password-types.ts +0 -24
  230. package/src/auth/ecomiq-storefront-sign-in.ts +0 -44
  231. package/src/auth/env.ts +0 -7
  232. package/src/auth/secure-cookies.ts +0 -23
  233. package/src/auth/storefront-auth-base-url.ts +0 -117
  234. package/src/auth/storefront-auth.ts +0 -7
  235. package/src/cart-session.ts +0 -239
  236. package/src/client.ts +0 -48
  237. package/src/cloud-runtime-client.ts +0 -378
  238. package/src/codegen.mjs +0 -789
  239. package/src/components/account/customer-addresses-panel.tsx +0 -678
  240. package/src/components/account/customer-order-detail-panel.tsx +0 -285
  241. package/src/components/account/customer-orders-panel.tsx +0 -192
  242. package/src/components/account/customer-profile-panel.tsx +0 -379
  243. package/src/components/checkout/cart-sync.ts +0 -143
  244. package/src/components/checkout/checkout-contact-section.tsx +0 -76
  245. package/src/components/checkout/checkout-external-update-banner.tsx +0 -15
  246. package/src/components/checkout/checkout-form-controls.tsx +0 -139
  247. package/src/components/checkout/checkout-line-items.tsx +0 -43
  248. package/src/components/checkout/checkout-order-summary.tsx +0 -165
  249. package/src/components/checkout/checkout-payment-section.tsx +0 -212
  250. package/src/components/checkout/checkout-session-view.tsx +0 -564
  251. package/src/components/checkout/checkout-shipping-address-section.tsx +0 -400
  252. package/src/components/checkout/checkout-shipping-section.tsx +0 -98
  253. package/src/components/checkout/checkout-states.tsx +0 -73
  254. package/src/components/checkout/checkout-step-card.tsx +0 -101
  255. package/src/components/checkout/checkout-steps.ts +0 -111
  256. package/src/components/checkout/form-schema.ts +0 -281
  257. package/src/components/checkout/format.ts +0 -15
  258. package/src/components/checkout/index.ts +0 -5
  259. package/src/components/checkout/map-customer-to-checkout.ts +0 -188
  260. package/src/components/checkout/types.ts +0 -41
  261. package/src/components/checkout/use-checkout-customer-prefill.ts +0 -409
  262. package/src/components/checkout/use-checkout-geo.ts +0 -50
  263. package/src/components/checkout/use-checkout-session.ts +0 -640
  264. package/src/components/hero.tsx +0 -291
  265. package/src/config.d.ts +0 -94
  266. package/src/config.mjs +0 -146
  267. package/src/format-money.ts +0 -31
  268. package/src/health.ts +0 -90
  269. package/src/hooks/index.ts +0 -9
  270. package/src/hooks/recently-viewed.ts +0 -125
  271. package/src/index.ts +0 -354
  272. package/src/link.tsx +0 -2
  273. package/src/pages/account/account-page-skeleton.tsx +0 -69
  274. package/src/pages/account/account-page-view.tsx +0 -270
  275. package/src/pages/account/account-shell.tsx +0 -101
  276. package/src/pages/account/parse-account-route.ts +0 -36
  277. package/src/pages/account.tsx +0 -16
  278. package/src/pages/api.$.ts +0 -20
  279. package/src/pages/api.auth.$.ts +0 -16
  280. package/src/pages/checkout.$checkoutId.tsx +0 -60
  281. package/src/pages/checkout.tsx +0 -31
  282. package/src/pages/pages.$pageSlug.tsx +0 -111
  283. package/src/pages/thank-you.tsx +0 -53
  284. package/src/providers/auth.tsx +0 -96
  285. package/src/providers/cart.tsx +0 -275
  286. package/src/providers/ecomiq.ts +0 -137
  287. package/src/providers/index.ts +0 -26
  288. package/src/proxy-api.ts +0 -20
  289. package/src/public-config.ts +0 -24
  290. package/src/review-submission.ts +0 -89
  291. package/src/runtime/context.ts +0 -2
  292. package/src/runtime/default-layout.tsx +0 -64
  293. package/src/runtime/head.ts +0 -149
  294. package/src/runtime/page.tsx +0 -143
  295. package/src/runtime/postcss-browser-stub.mjs +0 -5
  296. package/src/server-config-store.ts +0 -35
  297. package/src/server.ts +0 -301
  298. package/src/storefront-definition/contract.ts +0 -1
  299. package/src/storefront-definition/data.ts +0 -188
  300. package/src/storefront-definition/index.ts +0 -2
  301. package/src/storefront-definition/renderer.tsx +0 -1481
  302. package/src/storefront-layout.ts +0 -59
  303. package/src/storefront-runtime.ts +0 -173
  304. package/src/styles.css +0 -97
  305. package/src/ui/badge.tsx +0 -32
  306. package/src/ui/button.tsx +0 -31
  307. package/src/ui/card.tsx +0 -31
  308. package/src/ui/carousel.tsx +0 -218
  309. package/src/ui/cn.ts +0 -6
  310. package/src/ui/index.ts +0 -8
  311. package/src/ui/separator.tsx +0 -6
  312. package/src/ui/skeleton.tsx +0 -6
  313. package/src/ui/storefront-error.tsx +0 -49
  314. package/src/utils.ts +0 -36
  315. package/src/vite-inline.mjs +0 -263
  316. package/templates/__root.tsx +0 -83
  317. package/templates/router.tsx +0 -19
@@ -0,0 +1,325 @@
1
+ import { COMPLAINT_BOOK_ATTACHMENT_TYPES, formatOrderDate, type ComplaintBook as ComplaintBookConfiguration } from "@ecomiq/core";
2
+ import { Fragment, useRef, useState, type ComponentProps } from "react";
3
+ import { Form, useNavigation } from "react-router";
4
+ import { Button } from "@theme/ui/button";
5
+ import { Checkbox } from "@theme/ui/checkbox";
6
+ import { Input } from "@theme/ui/input";
7
+ import { Label } from "@theme/ui/label";
8
+ import { RadioGroup, RadioGroupItem } from "@theme/ui/radio-group";
9
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@theme/ui/select";
10
+ import { Textarea } from "@theme/ui/textarea";
11
+ import {
12
+ Captcha,
13
+ COMPLAINT_DOCUMENT_TYPES,
14
+ identityDocumentRule,
15
+ useStorefrontText,
16
+ complaintBookAttachmentError,
17
+ complaintBookNumber,
18
+ CustomerCsrfField,
19
+ money,
20
+ type ComplaintBookActionData,
21
+ type ComplaintBookData,
22
+ type SectionCatalog,
23
+ type SectionRoot,
24
+ } from "../index";
25
+
26
+ export const catalog: SectionCatalog = {
27
+ name: "Libro de Reclamaciones",
28
+ description: "Formulario de reclamos y quejas con la hoja para imprimir.",
29
+ };
30
+
31
+ type Filed = Extract<ComplaintBookActionData, { entry: unknown }>;
32
+ type Row = readonly [string, string | null];
33
+
34
+ const TYPES = [
35
+ { value: "claim", label: "Reclamo" },
36
+ { value: "complaint", label: "Queja" },
37
+ ] as const;
38
+ const ITEMS = [
39
+ { value: "product", label: "Producto" },
40
+ { value: "service", label: "Servicio" },
41
+ ] as const;
42
+ const LEGEND = "mb-3 text-lg font-medium";
43
+ const ATTACHMENT_ACCEPT = [...COMPLAINT_BOOK_ATTACHMENT_TYPES, ".jpg", ".jpeg", ".png", ".webp", ".pdf"].join(",");
44
+
45
+ function Rows({ title, rows }: { title: string; rows: readonly Row[] }) {
46
+ return (
47
+ <div className="flex flex-col gap-2">
48
+ <h3 className="font-medium">{title}</h3>
49
+ <dl className="grid gap-x-6 gap-y-1.5 text-sm sm:grid-cols-[11rem_minmax(0,1fr)]">
50
+ {rows.map(([label, value]) =>
51
+ value ? (
52
+ <Fragment key={label}>
53
+ <dt className="text-muted-foreground">{label}</dt>
54
+ <dd className="break-words whitespace-pre-line">{value}</dd>
55
+ </Fragment>
56
+ ) : null,
57
+ )}
58
+ </dl>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function providerRows(book: ComplaintBookConfiguration): Row[] {
64
+ return [
65
+ ["Razón social", book.providerName],
66
+ ["RUC", book.providerTaxId],
67
+ ["Dirección", book.providerAddress],
68
+ ];
69
+ }
70
+
71
+ function Field({ label, name, className, ...props }: ComponentProps<"input"> & { label: string; name: string }) {
72
+ return (
73
+ <div className={`flex flex-col gap-2 ${className ?? ""}`}>
74
+ <Label htmlFor={`complaint-${name}`}>{label}</Label>
75
+ <Input id={`complaint-${name}`} name={name} {...props} />
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function Area({ label, name, maxLength }: { label: string; name: string; maxLength: number }) {
81
+ return (
82
+ <div className="flex flex-col gap-2">
83
+ <Label htmlFor={`complaint-${name}`}>{label}</Label>
84
+ <Textarea id={`complaint-${name}`} name={name} rows={4} maxLength={maxLength} required />
85
+ </div>
86
+ );
87
+ }
88
+
89
+ function Choices({ name, options }: { name: string; options: readonly { value: string; label: string }[] }) {
90
+ return (
91
+ <RadioGroup name={name} defaultValue={options[0]?.value} required className="flex flex-wrap gap-6">
92
+ {options.map((option) => (
93
+ <div key={option.value} className="flex items-center gap-2">
94
+ <RadioGroupItem id={`complaint-${name}-${option.value}`} value={option.value} />
95
+ <Label htmlFor={`complaint-${name}-${option.value}`}>{option.label}</Label>
96
+ </div>
97
+ ))}
98
+ </RadioGroup>
99
+ );
100
+ }
101
+
102
+ function Attachments() {
103
+ const storefrontTranslate = useStorefrontText();
104
+ const input = useRef<HTMLInputElement>(null);
105
+ const [files, setFiles] = useState<File[]>([]);
106
+ const [error, setError] = useState<string | null>(null);
107
+
108
+ function attach(next: File[]) {
109
+ const transfer = new DataTransfer();
110
+ for (const file of next) transfer.items.add(file);
111
+ if (input.current) input.current.files = transfer.files;
112
+ setFiles(next);
113
+ }
114
+
115
+ function pick(picked: FileList | null) {
116
+ const next = [...files, ...Array.from(picked ?? [])];
117
+ const invalid = complaintBookAttachmentError(next);
118
+ setError(invalid);
119
+ attach(invalid ? files : next);
120
+ }
121
+
122
+ function remove(index: number) {
123
+ setError(null);
124
+ attach(files.filter((_, position) => position !== index));
125
+ }
126
+
127
+ return (
128
+ <div className="flex flex-col gap-2">
129
+ <Field ref={input} label={storefrontTranslate("Adjuntos (opcional)")} name="files" type="file" multiple accept={ATTACHMENT_ACCEPT} aria-describedby="complaint-files-help" onChange={(event) => pick(event.currentTarget.files)} />
130
+ <p id="complaint-files-help" className="text-sm text-muted-foreground">
131
+ {storefrontTranslate("Hasta 3 fotos o PDF de 5 MB.")} </p>
132
+ {error ? (
133
+ <p role="alert" className="text-sm text-destructive">
134
+ {storefrontTranslate(error)}
135
+ </p>
136
+ ) : null}
137
+ {files.length > 0 ? (
138
+ <ul className="flex flex-col gap-1 text-sm">
139
+ {files.map((file, index) => (
140
+ <li key={`${index}-${file.name}`} className="flex items-center justify-between gap-3">
141
+ <span className="min-w-0 truncate">{file.name}</span>
142
+ <Button type="button" variant="ghost" size="sm" aria-label={storefrontTranslate("Quitar {value0}", { value0: file.name })} onClick={() => remove(index)}>
143
+ {storefrontTranslate("Quitar")}</Button>
144
+ </li>
145
+ ))}
146
+ </ul>
147
+ ) : null}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ function ComplaintForm({ book, error, countries }: { book: ComplaintBookConfiguration; error: string | null; countries: ComplaintBookData["countries"] }) {
153
+ const storefrontTranslate = useStorefrontText();
154
+ const busy = useNavigation().state !== "idle";
155
+ const [kind, setKind] = useState<keyof typeof COMPLAINT_DOCUMENT_TYPES>("nationalId");
156
+ const [countryCode, setCountryCode] = useState("");
157
+ const [minor, setMinor] = useState(false);
158
+ const rule = identityDocumentRule(COMPLAINT_DOCUMENT_TYPES[kind]);
159
+ return (
160
+ <Form method="post" encType="multipart/form-data" className="flex flex-col gap-8">
161
+ <CustomerCsrfField />
162
+ <fieldset>
163
+ <legend className={LEGEND}>{storefrontTranslate("Tipo")}</legend>
164
+ <Choices name="type" options={TYPES} />
165
+ </fieldset>
166
+ <fieldset className="grid gap-4 sm:grid-cols-2">
167
+ <legend className={LEGEND}>{storefrontTranslate("Tus datos")}</legend>
168
+ <Field label={storefrontTranslate("Nombre completo")} name="consumerName" autoComplete="name" maxLength={200} required className="sm:col-span-2" />
169
+ <div className="flex flex-col gap-2">
170
+ <Label htmlFor="complaint-documentType">{storefrontTranslate("Tipo de documento")}</Label>
171
+ <Select name="documentType" value={kind} onValueChange={(value) => setKind(value === "taxId" || value === "foreignId" || value === "passport" ? value : "nationalId")}>
172
+ <SelectTrigger id="complaint-documentType" className="w-full">
173
+ <SelectValue />
174
+ </SelectTrigger>
175
+ <SelectContent>
176
+ {Object.entries(COMPLAINT_DOCUMENT_TYPES).map(([item, type]) => (
177
+ <SelectItem key={item} value={item}>
178
+ {identityDocumentRule(type).label}
179
+ </SelectItem>
180
+ ))}
181
+ </SelectContent>
182
+ </Select>
183
+ </div>
184
+ <label className="flex flex-col gap-2">{storefrontTranslate("País emisor del documento")} <select name="documentCountryCode" required value={countryCode} onChange={(event) => setCountryCode(event.currentTarget.value)} className="h-10 rounded-md border bg-background px-3 text-base md:text-sm">
185
+ <option value="">{storefrontTranslate("Elige…")}</option>
186
+ {countries.map((country) => <option key={country.code} value={country.code}>{country.name}</option>)}
187
+ </select>
188
+ </label>
189
+ <Field label={storefrontTranslate("Número de documento")} name="documentNumber" inputMode={rule.inputMode} maxLength={rule.maxLength} pattern={rule.pattern} title={storefrontTranslate(rule.error)} autoComplete="off" required />
190
+ <Field label={storefrontTranslate("Dirección")} name="consumerAddress" autoComplete="street-address" maxLength={300} required className="sm:col-span-2" />
191
+ <Field label={storefrontTranslate("Celular")} name="consumerPhone" type="tel" autoComplete="tel" maxLength={30} required />
192
+ <Field label={storefrontTranslate("Correo")} name="consumerEmail" type="email" autoComplete="email" maxLength={254} required />
193
+ <div className="flex items-center gap-2 sm:col-span-2">
194
+ <Checkbox id="complaint-minor" name="minor" checked={minor} onCheckedChange={(checked) => setMinor(checked === true)} />
195
+ <Label htmlFor="complaint-minor">{storefrontTranslate("Soy menor de edad")}</Label>
196
+ </div>
197
+ </fieldset>
198
+ {minor ? (
199
+ <fieldset className="grid gap-4 sm:grid-cols-2">
200
+ <legend className={LEGEND}>{storefrontTranslate("Padre, madre o apoderado")}</legend>
201
+ <Field label={storefrontTranslate("Nombre completo")} name="guardianName" maxLength={200} required className="sm:col-span-2" />
202
+ <Field label={storefrontTranslate("Dirección")} name="guardianAddress" maxLength={300} required className="sm:col-span-2" />
203
+ <Field label={storefrontTranslate("Celular")} name="guardianPhone" type="tel" maxLength={30} required />
204
+ <Field label={storefrontTranslate("Correo")} name="guardianEmail" type="email" maxLength={254} required />
205
+ </fieldset>
206
+ ) : null}
207
+ <fieldset className="grid gap-4 sm:grid-cols-2">
208
+ <legend className={LEGEND}>{storefrontTranslate("Producto o servicio")}</legend>
209
+ <div className="sm:col-span-2">
210
+ <Choices name="itemKind" options={ITEMS} />
211
+ </div>
212
+ <Field label={storefrontTranslate("Descripción")} name="itemDescription" maxLength={500} required className="sm:col-span-2" />
213
+ <Field label={storefrontTranslate("Monto reclamado en {value0} (opcional)", { value0: book.currency })} name="itemAmount" type="number" inputMode="decimal" min={0} step="0.01" />
214
+ <Field label={storefrontTranslate("N.° de pedido (opcional)")} name="orderNumber" maxLength={50} />
215
+ </fieldset>
216
+ <fieldset className="flex flex-col gap-4">
217
+ <legend className={LEGEND}>{storefrontTranslate("Detalle")}</legend>
218
+ <Area label={storefrontTranslate("Qué pasó")} name="detail" maxLength={4000} />
219
+ <Area label={storefrontTranslate("Qué solicitas")} name="request" maxLength={2000} />
220
+ <Attachments />
221
+ </fieldset>
222
+ <div className="flex items-center gap-2">
223
+ <Checkbox id="complaint-confirmed" name="confirmed" required />
224
+ <Label htmlFor="complaint-confirmed">{storefrontTranslate("Declaro que los datos son verdaderos.")}</Label>
225
+ </div>
226
+ <Captcha captcha={book.captcha} action="complaint_book" errorClass="text-sm text-destructive" busy={busy} />
227
+ {error ? (
228
+ <p role="alert" className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
229
+ {storefrontTranslate(error)}
230
+ </p>
231
+ ) : null}
232
+ <Button type="submit" disabled={busy || !book.captcha.isConfigured} className="sm:w-fit">
233
+ {busy ? "Enviando…" : "Enviar"}
234
+ </Button>
235
+ </Form>
236
+ );
237
+ }
238
+
239
+ function Sheet({ book, filed, language, timeZone }: { book: ComplaintBookConfiguration; filed: Filed; language: string; timeZone: string }) {
240
+ const storefrontTranslate = useStorefrontText();
241
+ const { entry, sheet, attachments } = filed;
242
+ const { consumer, guardian, item } = sheet;
243
+ const type = TYPES.find((option) => option.value === sheet.type)?.label ?? "";
244
+ const date = (iso: string) => formatOrderDate(iso, language, timeZone);
245
+ return (
246
+ <article className="flex flex-col gap-6 rounded-lg border bg-card p-6 text-card-foreground md:p-8">
247
+ <div className="flex flex-wrap items-start justify-between gap-3">
248
+ <div className="flex flex-col gap-1">
249
+ <p className="text-sm text-muted-foreground">{storefrontTranslate("Hoja de reclamación ·")} {type}</p>
250
+ <h2 className="font-heading text-3xl">{complaintBookNumber(entry.number)}</h2>
251
+ </div>
252
+ <Button type="button" variant="outline" onClick={() => window.print()} className="print:hidden">
253
+ {storefrontTranslate("Imprimir")} </Button>
254
+ </div>
255
+ <Rows
256
+ title={storefrontTranslate("Fechas")}
257
+ rows={[
258
+ ["Presentado", date(entry.filedOn)],
259
+ ["Respuesta hasta", date(entry.responseDueOn)],
260
+ ]}
261
+ />
262
+ <Rows title={storefrontTranslate("Proveedor")} rows={providerRows(book)} />
263
+ <Rows
264
+ title={storefrontTranslate("Consumidor")}
265
+ rows={[
266
+ [storefrontTranslate("Nombre"), consumer.name],
267
+ [identityDocumentRule(COMPLAINT_DOCUMENT_TYPES[consumer.documentType]).label, consumer.documentNumber],
268
+ [storefrontTranslate("Dirección"), consumer.address],
269
+ [storefrontTranslate("Celular"), consumer.phone],
270
+ [storefrontTranslate("Correo"), consumer.email],
271
+ ]}
272
+ />
273
+ {guardian ? (
274
+ <Rows
275
+ title={storefrontTranslate("Padre, madre o apoderado")}
276
+ rows={[
277
+ [storefrontTranslate("Nombre"), guardian.name],
278
+ [storefrontTranslate("Dirección"), guardian.address],
279
+ [storefrontTranslate("Celular"), guardian.phone],
280
+ [storefrontTranslate("Correo"), guardian.email],
281
+ ]}
282
+ />
283
+ ) : null}
284
+ <Rows
285
+ title={ITEMS.find((option) => option.value === item.kind)?.label ?? ""}
286
+ rows={[
287
+ [storefrontTranslate("Descripción"), item.description],
288
+ ["Monto reclamado", item.amount === null ? null : money(item.amount, book.currency, language)],
289
+ ["N.° de pedido", sheet.orderNumber],
290
+ ]}
291
+ />
292
+ <Rows
293
+ title={type}
294
+ rows={[
295
+ [storefrontTranslate("Qué pasó"), sheet.detail],
296
+ [storefrontTranslate("Qué solicitas"), sheet.request],
297
+ ["Adjuntos", attachments.join("\n")],
298
+ ]}
299
+ />
300
+ </article>
301
+ );
302
+ }
303
+
304
+ export function ComplaintBook({ loaderData, actionData, ...root }: { loaderData: ComplaintBookData; actionData?: ComplaintBookActionData } & SectionRoot) {
305
+ const storefrontTranslate = useStorefrontText();
306
+ const { book, language, timeZone } = loaderData;
307
+ const filed = actionData && "entry" in actionData ? actionData : null;
308
+ const error = actionData && "error" in actionData ? actionData.error : null;
309
+ return (
310
+ <section {...root} className="mx-auto flex w-full max-w-3xl flex-col gap-8 px-4 py-12 md:py-20">
311
+ <div className="flex flex-col gap-3">
312
+ <h1 className="text-4xl leading-tight md:text-5xl">{storefrontTranslate("Libro de Reclamaciones")}</h1>
313
+ <p className="text-muted-foreground">{storefrontTranslate("Reclamo: disconformidad con el producto o servicio. Queja: malestar con la atención.")}</p>
314
+ </div>
315
+ {filed ? (
316
+ <Sheet book={book} filed={filed} language={`${language}-${loaderData.countryCode}`} timeZone={timeZone} />
317
+ ) : (
318
+ <>
319
+ <Rows title={storefrontTranslate("Proveedor")} rows={providerRows(book)} />
320
+ <ComplaintForm countries={loaderData.countries} book={book} error={error} />
321
+ </>
322
+ )}
323
+ </section>
324
+ );
325
+ }
@@ -0,0 +1,146 @@
1
+ import { useStorefrontText } from "../index";
2
+ import type { ReactNode } from "react";
3
+ import { Badge } from "@theme/ui/badge";
4
+ import { Button } from "@theme/ui/button";
5
+ import { Checkbox } from "@theme/ui/checkbox";
6
+ import { Input } from "@theme/ui/input";
7
+ import { Label } from "@theme/ui/label";
8
+ import { Textarea } from "@theme/ui/textarea";
9
+
10
+ const COLORS = [
11
+ { token: "primary", name: "Principal" },
12
+ { token: "primary-foreground", name: "Sobre principal" },
13
+ { token: "background", name: "Fondo" },
14
+ { token: "foreground", name: "Texto" },
15
+ { token: "secondary", name: "Secundario" },
16
+ { token: "muted", name: "Suave" },
17
+ { token: "muted-foreground", name: "Texto suave" },
18
+ { token: "accent", name: "Acento" },
19
+ { token: "border", name: "Borde" },
20
+ { token: "destructive", name: "Error" },
21
+ ] as const;
22
+
23
+ const SCHEMES = [
24
+ { scheme: "claro", name: "Claro" },
25
+ { scheme: "oscuro", name: "Oscuro" },
26
+ { scheme: "marca", name: "Marca" },
27
+ ] as const;
28
+
29
+ const CORNERS = [
30
+ { className: "rounded-none", name: "Sin redondeo" },
31
+ { className: "rounded-sm", name: "Pequeño" },
32
+ { className: "rounded-md", name: "Mediano" },
33
+ { className: "rounded-lg", name: "Grande" },
34
+ { className: "rounded-xl", name: "Muy grande" },
35
+ { className: "rounded-full", name: "Círculo" },
36
+ ] as const;
37
+
38
+ function Group({ title, children }: { title: string; children: ReactNode }) {
39
+ return (
40
+ <section className="flex flex-col gap-4">
41
+ <h2 className="font-body text-xs font-medium uppercase tracking-wide text-muted-foreground">{title}</h2>
42
+ {children}
43
+ </section>
44
+ );
45
+ }
46
+
47
+ export function DesignSystem() {
48
+ const storefrontTranslate = useStorefrontText();
49
+ return (
50
+ <div data-ecomiq-design="" className="fixed inset-0 z-[100] overflow-y-auto bg-background text-foreground">
51
+ <div className="mx-auto flex max-w-5xl flex-col gap-12 px-6 py-10">
52
+ <header className="flex flex-col gap-1">
53
+ <h1 className="font-heading text-3xl">Sistema de diseño</h1>
54
+ <p className="text-sm text-muted-foreground">Colores, letras y piezas de tu tema. Los cambios del panel Tema se ven aquí al instante.</p>
55
+ </header>
56
+
57
+ <Group title="Colores">
58
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
59
+ {COLORS.map((color) => (
60
+ <div key={color.token} className="flex flex-col gap-2">
61
+ <div className="h-16 rounded-md border border-border" style={{ background: `var(--${color.token})` }} />
62
+ <div className="flex flex-col">
63
+ <span className="text-sm font-medium">{color.name}</span>
64
+ <code className="text-xs text-muted-foreground">--{color.token}</code>
65
+ </div>
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </Group>
70
+
71
+ <Group title="Esquemas">
72
+ <div className="grid gap-3 md:grid-cols-3">
73
+ {SCHEMES.map((item) => (
74
+ <div key={item.scheme} data-scheme={item.scheme} className="flex flex-col gap-3 rounded-lg border border-border p-5">
75
+ <span className="text-xs text-muted-foreground">{item.name}</span>
76
+ <p className="font-heading text-xl">Nueva colección</p>
77
+ <p className="text-sm text-muted-foreground">Piezas hechas para durar.</p>
78
+ <Button size="sm" className="self-start">
79
+ {storefrontTranslate("Comprar")}</Button>
80
+ </div>
81
+ ))}
82
+ </div>
83
+ </Group>
84
+
85
+ <Group title="Tipografía">
86
+ <div className="flex flex-col gap-3">
87
+ <p className="font-heading text-5xl leading-tight">Títulos que se recuerdan</p>
88
+ <p className="font-heading text-3xl">Subtítulo de sección</p>
89
+ <p className="font-heading text-xl">Título de tarjeta</p>
90
+ <p className="max-w-2xl text-base">Este es el texto de tu tienda: descripciones de productos, políticas y todo lo que lee tu cliente antes de comprar.</p>
91
+ <p className="text-sm text-muted-foreground">Texto pequeño para notas y ayudas.</p>
92
+ </div>
93
+ </Group>
94
+
95
+ <Group title="Botones">
96
+ <div className="flex flex-wrap items-center gap-3">
97
+ <Button>{storefrontTranslate("Principal")}</Button>
98
+ <Button variant="secondary">Secundario</Button>
99
+ <Button variant="outline">Con borde</Button>
100
+ <Button variant="ghost">Sin fondo</Button>
101
+ <Button variant="link">Enlace</Button>
102
+ </div>
103
+ <div className="flex flex-wrap items-center gap-3">
104
+ <Button size="sm">Pequeño</Button>
105
+ <Button>Normal</Button>
106
+ <Button size="lg">Grande</Button>
107
+ </div>
108
+ </Group>
109
+
110
+ <Group title="Campos">
111
+ <div className="grid max-w-2xl gap-4 sm:grid-cols-2">
112
+ <div className="flex flex-col gap-2">
113
+ <Label htmlFor="ecomiq-design-email">{storefrontTranslate("Correo")}</Label>
114
+ <Input id="ecomiq-design-email" placeholder={storefrontTranslate("tu@correo.com")} />
115
+ </div>
116
+ <div className="flex flex-col gap-2">
117
+ <Label htmlFor="ecomiq-design-note">Nota</Label>
118
+ <Textarea id="ecomiq-design-note" placeholder="Escribe aquí" rows={2} />
119
+ </div>
120
+ <div className="flex items-center gap-2">
121
+ <Checkbox id="ecomiq-design-news" defaultChecked />
122
+ <Label htmlFor="ecomiq-design-news">Quiero recibir novedades</Label>
123
+ </div>
124
+ <div className="flex flex-wrap items-center gap-2">
125
+ <Badge>Nuevo</Badge>
126
+ <Badge variant="secondary">Envío gratis</Badge>
127
+ <Badge variant="outline">{storefrontTranslate("Agotado")}</Badge>
128
+ <Badge variant="destructive">-20 %</Badge>
129
+ </div>
130
+ </div>
131
+ </Group>
132
+
133
+ <Group title="Esquinas">
134
+ <div className="flex flex-wrap gap-4">
135
+ {CORNERS.map((corner) => (
136
+ <div key={corner.className} className="flex flex-col items-center gap-2">
137
+ <div className={`size-16 border border-border bg-muted ${corner.className}`} />
138
+ <span className="text-xs text-muted-foreground">{corner.name}</span>
139
+ </div>
140
+ ))}
141
+ </div>
142
+ </Group>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,105 @@
1
+ import { Form, Link, useNavigation } from "react-router";
2
+ import { Button } from "@theme/ui/button";
3
+ import { useStorefrontText, CustomerCsrfField, money, setting, type SectionCatalog, type SectionRoot } from "../index";
4
+ import { orderSummaryLines, type OrderDraftActionData, type OrderDraftData } from "../entries/order";
5
+
6
+ export const catalog: SectionCatalog = {
7
+ name: "Pedido por pagar",
8
+ description: "Productos y totales de un pedido por pagar, con el botón para pagarlo.",
9
+ };
10
+
11
+ export const settings = {
12
+ invoice: {
13
+ eyebrow: setting.text("Factura", "Antetítulo"),
14
+ title: setting.text("Tu pedido", "Título"),
15
+ description: setting.text("Revisa los productos y paga cuando quieras.", "Descripción"),
16
+ pay: setting.text("Pagar pedido", "Botón"),
17
+ note: setting.text("Se respetarán los importes y el envío de este pedido.", "Nota"),
18
+ split: setting.text("Tu compra se dividió en varios pedidos. Revisa y paga cada uno.", "Pedidos separados"),
19
+ closed: setting.text("Este pedido ya no está disponible para pagar. Escríbenos si necesitas ayuda.", "Pedido cerrado"),
20
+ },
21
+ };
22
+
23
+ export function DraftInvoice({ loaderData, actionData, ...root }: { loaderData: OrderDraftData; actionData?: OrderDraftActionData } & SectionRoot) {
24
+ const storefrontTranslate = useStorefrontText();
25
+ const { draft, payable, language } = loaderData;
26
+ const orderPaths = actionData?.orderPaths ?? loaderData.orderPaths;
27
+ const busy = useNavigation().state !== "idle";
28
+ const price = (value: number) => money(value, draft.currencyCode, language);
29
+ return (
30
+ <section {...root} className="mx-auto flex w-full max-w-2xl flex-col gap-8 px-4 py-12 md:py-20">
31
+ <div className="flex flex-col gap-3">
32
+ <p className="text-xs font-medium tracking-widest text-muted-foreground uppercase">
33
+ {settings.invoice.eyebrow} {draft.draftNumber}
34
+ </p>
35
+ <h1 className="text-4xl leading-tight md:text-5xl">{settings.invoice.title}</h1>
36
+ <p className="text-muted-foreground">{settings.invoice.description}</p>
37
+ </div>
38
+ <ul className="divide-y rounded-lg border bg-card text-card-foreground">
39
+ {draft.items.map((item) => (
40
+ <li key={item.id} className="flex items-center gap-4 p-4">
41
+ <span className="block w-16 shrink-0 overflow-hidden rounded-md bg-muted">
42
+ {item.thumbnail ? (
43
+ <img src={item.thumbnail} alt="" width={64} height={80} loading="lazy" decoding="async" className="aspect-[4/5] w-full object-cover" />
44
+ ) : (
45
+ <span className="block aspect-[4/5]" />
46
+ )}
47
+ </span>
48
+ <span className="flex flex-1 flex-col gap-0.5">
49
+ <span className="font-medium">{item.title}</span>
50
+ {item.subtitle ? <span className="text-sm text-muted-foreground">{item.subtitle}</span> : null}
51
+ <span className="text-sm text-muted-foreground tabular-nums">×{item.quantity}</span>
52
+ </span>
53
+ <span className="font-medium tabular-nums">{price(item.total)}</span>
54
+ </li>
55
+ ))}
56
+ </ul>
57
+ <dl className="flex flex-col gap-2.5 rounded-lg border bg-card p-6 text-sm text-card-foreground">
58
+ {orderSummaryLines(draft, { discount: "Descuento" }).map((line) =>
59
+ line.key === "total" ? (
60
+ <div key={line.key} className="flex items-baseline justify-between border-t pt-3 font-medium">
61
+ <dt>{storefrontTranslate(line.label)}</dt>
62
+ <dd className="font-heading text-xl tabular-nums">{price(line.amount ?? 0)}</dd>
63
+ </div>
64
+ ) : (
65
+ <div key={line.key} className="flex justify-between">
66
+ <dt className="text-muted-foreground">{storefrontTranslate(line.label)}</dt>
67
+ <dd className="tabular-nums">{line.amount === null ? storefrontTranslate(line.text ?? "") : price(line.amount)}</dd>
68
+ </div>
69
+ ),
70
+ )}
71
+ </dl>
72
+ {orderPaths.length > 0 ? (
73
+ <nav aria-label={storefrontTranslate("Pedidos para pagar")} className="flex flex-col gap-3 rounded-lg border p-5">
74
+ <p>{settings.invoice.split}</p>
75
+ <ul className="flex flex-col gap-3">
76
+ {orderPaths.map((path, index) => (
77
+ <li key={path}>
78
+ <Link to={path} className="font-medium underline underline-offset-4">
79
+ {storefrontTranslate("Ver y pagar pedido")} {index + 1}
80
+ </Link>
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ </nav>
85
+ ) : payable ? (
86
+ <Form method="post" className="flex flex-col gap-3">
87
+ <CustomerCsrfField />
88
+ {actionData?.error ? (
89
+ <p role="alert" className="text-sm text-destructive">
90
+ {storefrontTranslate(actionData.error)}
91
+ </p>
92
+ ) : null}
93
+ <Button type="submit" size="lg" disabled={busy}>
94
+ {busy ? storefrontTranslate("Preparando…") : settings.invoice.pay}
95
+ </Button>
96
+ <p className="text-center text-xs text-muted-foreground">{settings.invoice.note}</p>
97
+ </Form>
98
+ ) : (
99
+ <p role="status" className="rounded-md bg-muted px-4 py-3 text-sm">
100
+ {settings.invoice.closed}
101
+ </p>
102
+ )}
103
+ </section>
104
+ );
105
+ }
@@ -0,0 +1,81 @@
1
+ import { formatTrackingStatus } from "@ecomiq/core";
2
+ import { Form } from "react-router";
3
+ import { Button } from "@theme/ui/button";
4
+ import { Input } from "@theme/ui/input";
5
+ import { setting, type SectionCatalog, type SectionRoot, type TrackingData } from "../index";
6
+
7
+ export const catalog: SectionCatalog = {
8
+ name: "Seguimiento",
9
+ description: "Buscador por código para seguir el envío de un pedido.",
10
+ };
11
+
12
+ export const settings = {
13
+ tracking: {
14
+ eyebrow: setting.text("Envíos", "Antetítulo"),
15
+ title: setting.text("Sigue tu pedido", "Título"),
16
+ description: setting.text("Ingresa el código de seguimiento que te enviamos por correo.", "Descripción"),
17
+ field: setting.text("Código de seguimiento", "Campo"),
18
+ button: setting.text("Buscar", "Botón"),
19
+ missing: setting.text("No encontramos ese código. Revísalo e intenta de nuevo.", "Código no encontrado"),
20
+ shipment: setting.text("Envío", "Envío sin courier"),
21
+ carrier: setting.text("Ver en el courier", "Enlace al courier"),
22
+ },
23
+ };
24
+
25
+ export function OrderTracking({ loaderData, ...root }: { loaderData: TrackingData } & SectionRoot) {
26
+ const { code, tracking, missing, language, timeZone } = loaderData;
27
+ const date = new Intl.DateTimeFormat(language, { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", timeZone });
28
+ return (
29
+ <section {...root} className="mx-auto flex w-full max-w-2xl flex-col gap-8 px-4 py-12 md:py-20">
30
+ <div className="flex flex-col gap-3">
31
+ <p className="text-xs font-medium tracking-widest text-muted-foreground uppercase">{settings.tracking.eyebrow}</p>
32
+ <h1 className="text-4xl leading-tight md:text-5xl">{settings.tracking.title}</h1>
33
+ <p className="text-muted-foreground">{settings.tracking.description}</p>
34
+ </div>
35
+ <Form method="get" action="/track" className="flex flex-col gap-2 sm:flex-row">
36
+ <Input name="code" defaultValue={code} placeholder={settings.tracking.field} aria-label={settings.tracking.field} required />
37
+ <Button type="submit">{settings.tracking.button}</Button>
38
+ </Form>
39
+ {missing ? (
40
+ <p role="alert" className="rounded-md bg-destructive/10 px-4 py-3 text-sm text-destructive">
41
+ {settings.tracking.missing}
42
+ </p>
43
+ ) : null}
44
+ {tracking ? (
45
+ <div className="flex flex-col gap-6 rounded-lg border bg-card p-6 text-card-foreground md:p-8">
46
+ <div className="flex flex-wrap items-start justify-between gap-3">
47
+ <div className="flex flex-col gap-1">
48
+ <p className="text-sm text-muted-foreground">
49
+ {tracking.carrierName ?? settings.tracking.shipment} · {tracking.trackingNumber}
50
+ </p>
51
+ <p className="font-heading text-3xl">{formatTrackingStatus(tracking.status)}</p>
52
+ </div>
53
+ {tracking.trackingLink ? (
54
+ <a href={tracking.trackingLink} target="_blank" rel="noopener noreferrer" className="text-sm font-medium underline underline-offset-4">
55
+ {settings.tracking.carrier}
56
+ </a>
57
+ ) : null}
58
+ </div>
59
+ {tracking.message ? <p className="text-sm text-muted-foreground">{tracking.message}</p> : null}
60
+ <ol className="flex flex-col">
61
+ {tracking.events.map((event, index) => (
62
+ <li key={`${event.name}-${index}`} className="relative flex gap-4 pb-6 text-sm last:pb-0">
63
+ {index < tracking.events.length - 1 ? <span aria-hidden="true" className="absolute top-4 left-[5px] h-full w-px bg-border" /> : null}
64
+ <span aria-hidden="true" className="relative mt-1.5 size-2.5 shrink-0 rounded-full bg-primary" />
65
+ <span className="flex flex-col gap-0.5">
66
+ <span className="font-medium">{event.name}</span>
67
+ {event.description ? <span className="text-muted-foreground">{event.description}</span> : null}
68
+ {event.date ? (
69
+ <time dateTime={event.date} className="text-xs text-muted-foreground">
70
+ {date.format(new Date(event.date))}
71
+ </time>
72
+ ) : null}
73
+ </span>
74
+ </li>
75
+ ))}
76
+ </ol>
77
+ </div>
78
+ ) : null}
79
+ </section>
80
+ );
81
+ }
@@ -0,0 +1,4 @@
1
+ declare module "virtual:ecomiq-links" {
2
+ const links: readonly import("@ecomiq/core").CatalogLink[];
3
+ export default links;
4
+ }