@salifbiaye/create-boutique 0.1.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.
Files changed (316) hide show
  1. package/README.md +1 -0
  2. package/cli/dist/commands.js +382 -0
  3. package/cli/dist/create.js +194 -0
  4. package/cli/dist/docker.js +19 -0
  5. package/cli/dist/generate.js +322 -0
  6. package/cli/dist/guide.js +72 -0
  7. package/cli/dist/index.js +156 -0
  8. package/cli/dist/paths.js +29 -0
  9. package/cli/dist/ports.js +54 -0
  10. package/cli/dist/project.js +119 -0
  11. package/cli/dist/themes.js +24 -0
  12. package/docs/fiche-client.en.html +137 -0
  13. package/docs/fiche-client.html +137 -0
  14. package/docs/guide-complet.en.html +259 -0
  15. package/docs/guide-complet.fr.html +259 -0
  16. package/package.json +51 -0
  17. package/templates/backend/.dockerignore +9 -0
  18. package/templates/backend/Dockerfile +30 -0
  19. package/templates/backend/README.md +78 -0
  20. package/templates/backend/TEMPLATE_VERSION +1 -0
  21. package/templates/backend/docker/entrypoint.sh +28 -0
  22. package/templates/backend/instrumentation.ts +24 -0
  23. package/templates/backend/medusa-config.ts +114 -0
  24. package/templates/backend/package-lock.json +16222 -0
  25. package/templates/backend/package.json +39 -0
  26. package/templates/backend/src/admin/README.md +33 -0
  27. package/templates/backend/src/admin/components/image-field.tsx +61 -0
  28. package/templates/backend/src/admin/i18n/README.md +58 -0
  29. package/templates/backend/src/admin/i18n/index.ts +8 -0
  30. package/templates/backend/src/admin/i18n/json/en.json +66 -0
  31. package/templates/backend/src/admin/i18n/json/fr.json +66 -0
  32. package/templates/backend/src/admin/lib/client.ts +9 -0
  33. package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +286 -0
  34. package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +113 -0
  35. package/templates/backend/src/admin/tsconfig.json +24 -0
  36. package/templates/backend/src/admin/vite-env.d.ts +1 -0
  37. package/templates/backend/src/admin/widgets/category-image.tsx +56 -0
  38. package/templates/backend/src/api/README.md +135 -0
  39. package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +30 -0
  40. package/templates/backend/src/api/admin/boutique-campaigns/route.ts +24 -0
  41. package/templates/backend/src/api/middlewares.ts +28 -0
  42. package/templates/backend/src/api/rate-limit.ts +30 -0
  43. package/templates/backend/src/api/route.ts +6 -0
  44. package/templates/backend/src/api/store/campaign/route.ts +25 -0
  45. package/templates/backend/src/api/store/order-lookup/route.ts +74 -0
  46. package/templates/backend/src/emails/en.json +64 -0
  47. package/templates/backend/src/emails/fr.json +64 -0
  48. package/templates/backend/src/jobs/README.md +38 -0
  49. package/templates/backend/src/lib/emails.ts +94 -0
  50. package/templates/backend/src/lib/notify.ts +148 -0
  51. package/templates/backend/src/lib/project.ts +146 -0
  52. package/templates/backend/src/links/README.md +26 -0
  53. package/templates/backend/src/links/campaign-sales-channel.ts +9 -0
  54. package/templates/backend/src/modules/README.md +117 -0
  55. package/templates/backend/src/modules/campaign/index.ts +9 -0
  56. package/templates/backend/src/modules/campaign/migrations/.snapshot-boutique-campaign.json +272 -0
  57. package/templates/backend/src/modules/campaign/migrations/Migration20260928141440.ts +16 -0
  58. package/templates/backend/src/modules/campaign/migrations/Migration20260928145243.ts +13 -0
  59. package/templates/backend/src/modules/campaign/models/campaign.ts +28 -0
  60. package/templates/backend/src/modules/campaign/service.ts +6 -0
  61. package/templates/backend/src/modules/campaign/validators.ts +94 -0
  62. package/templates/backend/src/modules/resend/index.ts +6 -0
  63. package/templates/backend/src/modules/resend/service.ts +63 -0
  64. package/templates/backend/src/scripts/README.md +63 -0
  65. package/templates/backend/src/scripts/campaign-export.ts +15 -0
  66. package/templates/backend/src/scripts/campaign-import.ts +38 -0
  67. package/templates/backend/src/scripts/price-report.ts +34 -0
  68. package/templates/backend/src/scripts/seed.ts +418 -0
  69. package/templates/backend/src/scripts/storage.ts +63 -0
  70. package/templates/backend/src/subscribers/README.md +61 -0
  71. package/templates/backend/src/subscribers/account-emails.ts +27 -0
  72. package/templates/backend/src/subscribers/fulfillment-emails.ts +42 -0
  73. package/templates/backend/src/subscribers/order-emails.ts +12 -0
  74. package/templates/backend/src/subscribers/return-refund-emails.ts +50 -0
  75. package/templates/backend/src/workflows/README.md +81 -0
  76. package/templates/backend/src/workflows/steps/upsert-campaign.ts +53 -0
  77. package/templates/backend/src/workflows/upsert-campaign.ts +30 -0
  78. package/templates/backend/tsconfig.json +36 -0
  79. package/templates/storefront/.dockerignore +7 -0
  80. package/templates/storefront/.github/ISSUE_TEMPLATE/bug-report.yml +71 -0
  81. package/templates/storefront/.prettierrc +8 -0
  82. package/templates/storefront/Dockerfile +37 -0
  83. package/templates/storefront/LICENSE +21 -0
  84. package/templates/storefront/README.md +127 -0
  85. package/templates/storefront/locales/en.json +269 -0
  86. package/templates/storefront/locales/fr.json +269 -0
  87. package/templates/storefront/next-env.d.ts +7 -0
  88. package/templates/storefront/next-sitemap.js +19 -0
  89. package/templates/storefront/next.config.js +29 -0
  90. package/templates/storefront/package-lock.json +9031 -0
  91. package/templates/storefront/package.json +49 -0
  92. package/templates/storefront/postcss.config.js +6 -0
  93. package/templates/storefront/public/favicon.ico +0 -0
  94. package/templates/storefront/scripts/sync-design.mjs +30 -0
  95. package/templates/storefront/src/app/(checkout)/checkout/page.tsx +31 -0
  96. package/templates/storefront/src/app/(checkout)/layout.tsx +29 -0
  97. package/templates/storefront/src/app/(checkout)/not-found.tsx +20 -0
  98. package/templates/storefront/src/app/(main)/account/@dashboard/addresses/page.tsx +34 -0
  99. package/templates/storefront/src/app/(main)/account/@dashboard/loading.tsx +9 -0
  100. package/templates/storefront/src/app/(main)/account/@dashboard/orders/details/[id]/page.tsx +33 -0
  101. package/templates/storefront/src/app/(main)/account/@dashboard/orders/page.tsx +37 -0
  102. package/templates/storefront/src/app/(main)/account/@dashboard/page.tsx +22 -0
  103. package/templates/storefront/src/app/(main)/account/@dashboard/profile/page.tsx +53 -0
  104. package/templates/storefront/src/app/(main)/account/@login/page.tsx +12 -0
  105. package/templates/storefront/src/app/(main)/account/layout.tsx +20 -0
  106. package/templates/storefront/src/app/(main)/account/loading.tsx +9 -0
  107. package/templates/storefront/src/app/(main)/cart/loading.tsx +5 -0
  108. package/templates/storefront/src/app/(main)/cart/not-found.tsx +21 -0
  109. package/templates/storefront/src/app/(main)/cart/page.tsx +21 -0
  110. package/templates/storefront/src/app/(main)/categories/[...category]/page.tsx +55 -0
  111. package/templates/storefront/src/app/(main)/collections/[handle]/page.tsx +55 -0
  112. package/templates/storefront/src/app/(main)/layout.tsx +43 -0
  113. package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -0
  114. package/templates/storefront/src/app/(main)/not-found.tsx +21 -0
  115. package/templates/storefront/src/app/(main)/order/[id]/confirmed/loading.tsx +5 -0
  116. package/templates/storefront/src/app/(main)/order/[id]/confirmed/page.tsx +23 -0
  117. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/accept/page.tsx +42 -0
  118. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/decline/page.tsx +42 -0
  119. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/page.tsx +35 -0
  120. package/templates/storefront/src/app/(main)/order/track/page.tsx +32 -0
  121. package/templates/storefront/src/app/(main)/page.tsx +68 -0
  122. package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +86 -0
  123. package/templates/storefront/src/app/(main)/store/page.tsx +28 -0
  124. package/templates/storefront/src/app/api/health/route.ts +13 -0
  125. package/templates/storefront/src/app/layout.tsx +42 -0
  126. package/templates/storefront/src/app/not-found.tsx +21 -0
  127. package/templates/storefront/src/app/opengraph-image.jpg +0 -0
  128. package/templates/storefront/src/app/twitter-image.jpg +0 -0
  129. package/templates/storefront/src/instrumentation-node.ts +18 -0
  130. package/templates/storefront/src/instrumentation.ts +9 -0
  131. package/templates/storefront/src/lib/boutique/campaign.ts +116 -0
  132. package/templates/storefront/src/lib/boutique/config.ts +152 -0
  133. package/templates/storefront/src/lib/boutique/fonts.ts +29 -0
  134. package/templates/storefront/src/lib/boutique/i18n-client.tsx +34 -0
  135. package/templates/storefront/src/lib/boutique/i18n.ts +43 -0
  136. package/templates/storefront/src/lib/boutique/schema.ts +131 -0
  137. package/templates/storefront/src/lib/config.ts +39 -0
  138. package/templates/storefront/src/lib/constants.tsx +75 -0
  139. package/templates/storefront/src/lib/context/modal-context.tsx +34 -0
  140. package/templates/storefront/src/lib/data/cart.ts +434 -0
  141. package/templates/storefront/src/lib/data/categories.ts +49 -0
  142. package/templates/storefront/src/lib/data/collections.ts +59 -0
  143. package/templates/storefront/src/lib/data/cookies.ts +103 -0
  144. package/templates/storefront/src/lib/data/customer.ts +261 -0
  145. package/templates/storefront/src/lib/data/fulfillment.ts +68 -0
  146. package/templates/storefront/src/lib/data/onboarding.ts +9 -0
  147. package/templates/storefront/src/lib/data/order-lookup.ts +34 -0
  148. package/templates/storefront/src/lib/data/orders.ts +112 -0
  149. package/templates/storefront/src/lib/data/payment.ts +35 -0
  150. package/templates/storefront/src/lib/data/products.ts +127 -0
  151. package/templates/storefront/src/lib/data/regions.ts +70 -0
  152. package/templates/storefront/src/lib/data/variants.ts +38 -0
  153. package/templates/storefront/src/lib/hooks/use-in-view.tsx +29 -0
  154. package/templates/storefront/src/lib/hooks/use-toggle-state.tsx +46 -0
  155. package/templates/storefront/src/lib/util/compare-addresses.ts +28 -0
  156. package/templates/storefront/src/lib/util/get-percentage-diff.ts +6 -0
  157. package/templates/storefront/src/lib/util/get-product-price.ts +79 -0
  158. package/templates/storefront/src/lib/util/isEmpty.ts +11 -0
  159. package/templates/storefront/src/lib/util/medusa-error.ts +22 -0
  160. package/templates/storefront/src/lib/util/money.ts +35 -0
  161. package/templates/storefront/src/lib/util/product.ts +5 -0
  162. package/templates/storefront/src/lib/util/repeat.ts +5 -0
  163. package/templates/storefront/src/lib/util/sort-products.ts +50 -0
  164. package/templates/storefront/src/modules/account/components/account-info/index.tsx +140 -0
  165. package/templates/storefront/src/modules/account/components/account-nav/index.tsx +198 -0
  166. package/templates/storefront/src/modules/account/components/address-book/index.tsx +28 -0
  167. package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +169 -0
  168. package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +241 -0
  169. package/templates/storefront/src/modules/account/components/login/index.tsx +65 -0
  170. package/templates/storefront/src/modules/account/components/order-card/index.tsx +88 -0
  171. package/templates/storefront/src/modules/account/components/order-overview/index.tsx +47 -0
  172. package/templates/storefront/src/modules/account/components/overview/index.tsx +169 -0
  173. package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +193 -0
  174. package/templates/storefront/src/modules/account/components/profile-email/index.tsx +75 -0
  175. package/templates/storefront/src/modules/account/components/profile-name/index.tsx +79 -0
  176. package/templates/storefront/src/modules/account/components/profile-password/index.tsx +72 -0
  177. package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +74 -0
  178. package/templates/storefront/src/modules/account/components/register/index.tsx +107 -0
  179. package/templates/storefront/src/modules/account/components/transfer-request-form/index.tsx +83 -0
  180. package/templates/storefront/src/modules/account/templates/account-layout.tsx +43 -0
  181. package/templates/storefront/src/modules/account/templates/login-template.tsx +27 -0
  182. package/templates/storefront/src/modules/cart/components/cart-item-select/index.tsx +75 -0
  183. package/templates/storefront/src/modules/cart/components/empty-cart-message/index.tsx +25 -0
  184. package/templates/storefront/src/modules/cart/components/item/index.tsx +144 -0
  185. package/templates/storefront/src/modules/cart/components/sign-in-prompt/index.tsx +27 -0
  186. package/templates/storefront/src/modules/cart/templates/index.tsx +51 -0
  187. package/templates/storefront/src/modules/cart/templates/items.tsx +58 -0
  188. package/templates/storefront/src/modules/cart/templates/preview.tsx +51 -0
  189. package/templates/storefront/src/modules/cart/templates/summary.tsx +50 -0
  190. package/templates/storefront/src/modules/categories/templates/index.tsx +94 -0
  191. package/templates/storefront/src/modules/checkout/components/address-select/index.tsx +117 -0
  192. package/templates/storefront/src/modules/checkout/components/addresses/index.tsx +186 -0
  193. package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +114 -0
  194. package/templates/storefront/src/modules/checkout/components/country-select/index.tsx +50 -0
  195. package/templates/storefront/src/modules/checkout/components/discount-code/index.tsx +181 -0
  196. package/templates/storefront/src/modules/checkout/components/error-message/index.tsx +13 -0
  197. package/templates/storefront/src/modules/checkout/components/payment/index.tsx +261 -0
  198. package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +195 -0
  199. package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +130 -0
  200. package/templates/storefront/src/modules/checkout/components/payment-test/index.tsx +12 -0
  201. package/templates/storefront/src/modules/checkout/components/payment-wrapper/index.tsx +42 -0
  202. package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +54 -0
  203. package/templates/storefront/src/modules/checkout/components/review/index.tsx +54 -0
  204. package/templates/storefront/src/modules/checkout/components/shipping/index.tsx +417 -0
  205. package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +219 -0
  206. package/templates/storefront/src/modules/checkout/components/submit-button/index.tsx +32 -0
  207. package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +38 -0
  208. package/templates/storefront/src/modules/checkout/templates/checkout-summary/index.tsx +31 -0
  209. package/templates/storefront/src/modules/collections/templates/index.tsx +44 -0
  210. package/templates/storefront/src/modules/common/components/cart-totals/index.tsx +84 -0
  211. package/templates/storefront/src/modules/common/components/checkbox/index.tsx +44 -0
  212. package/templates/storefront/src/modules/common/components/delete-button/index.tsx +42 -0
  213. package/templates/storefront/src/modules/common/components/divider/index.tsx +9 -0
  214. package/templates/storefront/src/modules/common/components/filter-radio-group/index.tsx +61 -0
  215. package/templates/storefront/src/modules/common/components/input/index.tsx +79 -0
  216. package/templates/storefront/src/modules/common/components/interactive-link/index.tsx +33 -0
  217. package/templates/storefront/src/modules/common/components/line-item-options/index.tsx +26 -0
  218. package/templates/storefront/src/modules/common/components/line-item-price/index.tsx +63 -0
  219. package/templates/storefront/src/modules/common/components/line-item-unit-price/index.tsx +62 -0
  220. package/templates/storefront/src/modules/common/components/localized-client-link/index.tsx +29 -0
  221. package/templates/storefront/src/modules/common/components/modal/index.tsx +118 -0
  222. package/templates/storefront/src/modules/common/components/native-select/index.tsx +76 -0
  223. package/templates/storefront/src/modules/common/components/radio/index.tsx +27 -0
  224. package/templates/storefront/src/modules/common/icons/back.tsx +37 -0
  225. package/templates/storefront/src/modules/common/icons/bancontact.tsx +26 -0
  226. package/templates/storefront/src/modules/common/icons/chevron-down.tsx +30 -0
  227. package/templates/storefront/src/modules/common/icons/eye-off.tsx +37 -0
  228. package/templates/storefront/src/modules/common/icons/eye.tsx +37 -0
  229. package/templates/storefront/src/modules/common/icons/fast-delivery.tsx +65 -0
  230. package/templates/storefront/src/modules/common/icons/ideal.tsx +26 -0
  231. package/templates/storefront/src/modules/common/icons/map-pin.tsx +37 -0
  232. package/templates/storefront/src/modules/common/icons/package.tsx +44 -0
  233. package/templates/storefront/src/modules/common/icons/paypal.tsx +30 -0
  234. package/templates/storefront/src/modules/common/icons/placeholder-image.tsx +44 -0
  235. package/templates/storefront/src/modules/common/icons/refresh.tsx +51 -0
  236. package/templates/storefront/src/modules/common/icons/spinner.tsx +37 -0
  237. package/templates/storefront/src/modules/common/icons/trash.tsx +51 -0
  238. package/templates/storefront/src/modules/common/icons/user.tsx +37 -0
  239. package/templates/storefront/src/modules/common/icons/x.tsx +37 -0
  240. package/templates/storefront/src/modules/layout/components/cart-mismatch-banner/index.tsx +59 -0
  241. package/templates/storefront/src/modules/order/components/help/index.tsx +26 -0
  242. package/templates/storefront/src/modules/order/components/item/index.tsx +57 -0
  243. package/templates/storefront/src/modules/order/components/items/index.tsx +44 -0
  244. package/templates/storefront/src/modules/order/components/onboarding-cta/index.tsx +28 -0
  245. package/templates/storefront/src/modules/order/components/order-details/index.tsx +64 -0
  246. package/templates/storefront/src/modules/order/components/order-summary/index.tsx +61 -0
  247. package/templates/storefront/src/modules/order/components/payment-details/index.tsx +64 -0
  248. package/templates/storefront/src/modules/order/components/shipping-details/index.tsx +74 -0
  249. package/templates/storefront/src/modules/order/components/transfer-actions/index.tsx +83 -0
  250. package/templates/storefront/src/modules/order/components/transfer-image/index.tsx +275 -0
  251. package/templates/storefront/src/modules/order/templates/order-completed-template.tsx +53 -0
  252. package/templates/storefront/src/modules/order/templates/order-details-template.tsx +46 -0
  253. package/templates/storefront/src/modules/products/components/thumbnail/index.tsx +70 -0
  254. package/templates/storefront/src/modules/products/templates/index.tsx +118 -0
  255. package/templates/storefront/src/modules/shipping/components/free-shipping-price-nudge/index.tsx +285 -0
  256. package/templates/storefront/src/modules/skeletons/components/skeleton-button/index.tsx +5 -0
  257. package/templates/storefront/src/modules/skeletons/components/skeleton-card-details/index.tsx +10 -0
  258. package/templates/storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx +35 -0
  259. package/templates/storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
  260. package/templates/storefront/src/modules/skeletons/components/skeleton-code-form/index.tsx +13 -0
  261. package/templates/storefront/src/modules/skeletons/components/skeleton-line-item/index.tsx +35 -0
  262. package/templates/storefront/src/modules/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
  263. package/templates/storefront/src/modules/skeletons/components/skeleton-order-information/index.tsx +36 -0
  264. package/templates/storefront/src/modules/skeletons/components/skeleton-order-items/index.tsx +43 -0
  265. package/templates/storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx +15 -0
  266. package/templates/storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx +15 -0
  267. package/templates/storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
  268. package/templates/storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
  269. package/templates/storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
  270. package/templates/storefront/src/modules/store/components/pagination/index.tsx +114 -0
  271. package/templates/storefront/src/modules/store/components/refinement-list/index.tsx +41 -0
  272. package/templates/storefront/src/modules/store/components/refinement-list/sort-products/index.tsx +48 -0
  273. package/templates/storefront/src/modules/store/templates/index.tsx +42 -0
  274. package/templates/storefront/src/modules/store/templates/paginated-products.tsx +80 -0
  275. package/templates/storefront/src/modules/ub/button.tsx +41 -0
  276. package/templates/storefront/src/modules/ub/campaign-blocks.tsx +197 -0
  277. package/templates/storefront/src/modules/ub/campaign-ended.tsx +28 -0
  278. package/templates/storefront/src/modules/ub/category-tiles.tsx +41 -0
  279. package/templates/storefront/src/modules/ub/countdown.tsx +46 -0
  280. package/templates/storefront/src/modules/ub/footer.tsx +56 -0
  281. package/templates/storefront/src/modules/ub/icons.tsx +42 -0
  282. package/templates/storefront/src/modules/ub/nav.tsx +90 -0
  283. package/templates/storefront/src/modules/ub/pdp/buy-box.tsx +140 -0
  284. package/templates/storefront/src/modules/ub/pdp/gallery.tsx +40 -0
  285. package/templates/storefront/src/modules/ub/product-card.tsx +66 -0
  286. package/templates/storefront/src/modules/ub/top-bar.tsx +20 -0
  287. package/templates/storefront/src/modules/ub/track-form.tsx +92 -0
  288. package/templates/storefront/src/proxy.ts +89 -0
  289. package/templates/storefront/src/styles/globals.css +98 -0
  290. package/templates/storefront/src/styles/ub/bundle.css +372 -0
  291. package/templates/storefront/src/styles/ub/fonts.css +17 -0
  292. package/templates/storefront/src/styles/ub/medusa-ui.css +90 -0
  293. package/templates/storefront/src/styles/ub/overrides.css +12 -0
  294. package/templates/storefront/src/styles/ub/tokens.css +9 -0
  295. package/templates/storefront/src/types/global.ts +24 -0
  296. package/templates/storefront/src/types/icon.ts +4 -0
  297. package/templates/storefront/tailwind.config.js +164 -0
  298. package/templates/storefront/themes/black-friday/campaign.en.json +31 -0
  299. package/templates/storefront/themes/black-friday/campaign.fr.json +31 -0
  300. package/templates/storefront/themes/black-friday/theme.json +9 -0
  301. package/templates/storefront/themes/cyber-monday/campaign.en.json +31 -0
  302. package/templates/storefront/themes/cyber-monday/campaign.fr.json +31 -0
  303. package/templates/storefront/themes/cyber-monday/theme.json +9 -0
  304. package/templates/storefront/themes/halloween/campaign.en.json +31 -0
  305. package/templates/storefront/themes/halloween/campaign.fr.json +31 -0
  306. package/templates/storefront/themes/halloween/theme.json +9 -0
  307. package/templates/storefront/themes/noel/campaign.en.json +31 -0
  308. package/templates/storefront/themes/noel/campaign.fr.json +31 -0
  309. package/templates/storefront/themes/noel/theme.json +9 -0
  310. package/templates/storefront/themes/nouvel-an/campaign.en.json +31 -0
  311. package/templates/storefront/themes/nouvel-an/campaign.fr.json +31 -0
  312. package/templates/storefront/themes/nouvel-an/theme.json +9 -0
  313. package/templates/storefront/themes/thanksgiving/campaign.en.json +31 -0
  314. package/templates/storefront/themes/thanksgiving/campaign.fr.json +31 -0
  315. package/templates/storefront/themes/thanksgiving/theme.json +9 -0
  316. package/templates/storefront/tsconfig.json +53 -0
@@ -0,0 +1,113 @@
1
+ import { defineRouteConfig } from "@medusajs/admin-sdk"
2
+ import { PencilSquare, Sparkles, Spinner } from "@medusajs/icons"
3
+ import { Badge, Button, Container, Heading, Table, Text } from "@medusajs/ui"
4
+ import { useQuery } from "@tanstack/react-query"
5
+ import { useState } from "react"
6
+ import { useTranslation } from "react-i18next"
7
+ import { sdk } from "../../lib/client"
8
+ import { CampaignDrawer, type Boutique } from "./campaign-drawer"
9
+
10
+ const QUERY_KEY = ["boutique-campaigns"]
11
+
12
+ function status(c: Boutique["campaign"]): "upcoming" | "live" | "ended" | null {
13
+ if (!c) return null
14
+ const now = Date.now()
15
+ if (now < Date.parse(c.startsAt)) return "upcoming"
16
+ if (now > Date.parse(c.endsAt)) return "ended"
17
+ return "live"
18
+ }
19
+
20
+ const COLORS = { upcoming: "blue", live: "green", ended: "grey" } as const
21
+
22
+ /** Page « Campagnes » : une ligne par boutique du projet, édition dans un panneau latéral. */
23
+ const CampaignsPage = () => {
24
+ const { t, i18n } = useTranslation()
25
+ const [editing, setEditing] = useState<Boutique | null>(null)
26
+
27
+ const { data, isLoading } = useQuery({
28
+ queryKey: QUERY_KEY,
29
+ queryFn: () => sdk.client.fetch<{ boutiques: Boutique[] }>("/admin/boutique-campaigns"),
30
+ })
31
+
32
+ const date = (iso: string) => new Date(iso).toLocaleDateString(i18n.language, { dateStyle: "medium" })
33
+
34
+ return (
35
+ <Container className="divide-y p-0">
36
+ <div className="px-6 py-4">
37
+ <Heading level="h1">{t("campaigns.title")}</Heading>
38
+ <Text size="small" className="text-ui-fg-subtle mt-1">
39
+ {t("campaigns.description")}
40
+ </Text>
41
+ </div>
42
+
43
+ {isLoading ? (
44
+ <div className="flex justify-center px-6 py-8">
45
+ <Spinner className="animate-spin" />
46
+ </div>
47
+ ) : (
48
+ <Table>
49
+ <Table.Header>
50
+ <Table.Row>
51
+ <Table.HeaderCell>{t("campaigns.store")}</Table.HeaderCell>
52
+ <Table.HeaderCell>{t("campaigns.dates")}</Table.HeaderCell>
53
+ <Table.HeaderCell>{t("campaigns.status")}</Table.HeaderCell>
54
+ <Table.HeaderCell />
55
+ </Table.Row>
56
+ </Table.Header>
57
+ <Table.Body>
58
+ {data?.boutiques.map((b) => {
59
+ const s = status(b.campaign)
60
+ return (
61
+ <Table.Row key={b.sales_channel_id}>
62
+ <Table.Cell>
63
+ <Text size="small" weight="plus" leading="compact">
64
+ {b.slug}
65
+ </Text>
66
+ {b.description && (
67
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
68
+ {b.description}
69
+ </Text>
70
+ )}
71
+ </Table.Cell>
72
+ <Table.Cell>
73
+ {b.campaign ? `${date(b.campaign.startsAt)} → ${date(b.campaign.endsAt)}` : t("campaigns.noCampaign")}
74
+ </Table.Cell>
75
+ <Table.Cell>
76
+ {s && (
77
+ <Badge size="2xsmall" color={COLORS[s]}>
78
+ {t(`campaigns.${s}`)}
79
+ </Badge>
80
+ )}
81
+ </Table.Cell>
82
+ <Table.Cell className="text-right">
83
+ <Button size="small" variant="secondary" onClick={() => setEditing(b)}>
84
+ <PencilSquare />
85
+ {t("campaigns.edit")}
86
+ </Button>
87
+ </Table.Cell>
88
+ </Table.Row>
89
+ )
90
+ })}
91
+ </Table.Body>
92
+ </Table>
93
+ )}
94
+
95
+ {editing && (
96
+ <CampaignDrawer
97
+ boutique={editing}
98
+ others={(data?.boutiques ?? []).filter((b) => b.sales_channel_id !== editing.sales_channel_id).map((b) => b.slug)}
99
+ queryKey={QUERY_KEY}
100
+ onClose={() => setEditing(null)}
101
+ />
102
+ )}
103
+ </Container>
104
+ )
105
+ }
106
+
107
+ export const config = defineRouteConfig({
108
+ label: "campaigns.nav",
109
+ translationNs: "translation",
110
+ icon: Sparkles,
111
+ })
112
+
113
+ export default CampaignsPage
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true
22
+ },
23
+ "include": ["."]
24
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,56 @@
1
+ import { defineWidgetConfig } from "@medusajs/admin-sdk"
2
+ import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"
3
+ import { Button, Container, Heading, Text, toast } from "@medusajs/ui"
4
+ import { useMutation, useQueryClient } from "@tanstack/react-query"
5
+ import { useState } from "react"
6
+ import { useTranslation } from "react-i18next"
7
+ import { ImageField } from "../components/image-field"
8
+ import { sdk } from "../lib/client"
9
+
10
+ /**
11
+ * Image d'une catégorie (tuiles « Shop by category » des boutiques).
12
+ * Medusa n'a pas de champ image pour les catégories : on la range dans metadata.image.
13
+ */
14
+ const CategoryImageWidget = ({ data: category }: DetailWidgetProps<HttpTypes.AdminProductCategory>) => {
15
+ const { t } = useTranslation()
16
+ const queryClient = useQueryClient()
17
+ const saved = typeof category.metadata?.image === "string" ? category.metadata.image : ""
18
+ const [image, setImage] = useState(saved)
19
+
20
+ const save = useMutation({
21
+ mutationFn: () =>
22
+ sdk.admin.productCategory.update(category.id, {
23
+ metadata: { ...(category.metadata ?? {}), image: image || null },
24
+ }),
25
+ onSuccess: () => {
26
+ queryClient.invalidateQueries({ queryKey: ["product_categories"] })
27
+ toast.success(t("campaigns.category.saved"))
28
+ },
29
+ onError: (e: Error) => toast.error(e.message),
30
+ })
31
+
32
+ return (
33
+ <Container className="divide-y p-0">
34
+ <div className="px-6 py-4">
35
+ <Heading level="h2">{t("campaigns.category.title")}</Heading>
36
+ <Text size="small" leading="compact" className="text-ui-fg-subtle mt-1">
37
+ {t("campaigns.category.hint")}
38
+ </Text>
39
+ </div>
40
+ <div className="flex flex-col gap-y-3 px-6 py-4">
41
+ <ImageField label={t("campaigns.category.title")} value={image} onChange={setImage} />
42
+ <div className="flex justify-end">
43
+ <Button size="small" onClick={() => save.mutate()} isLoading={save.isPending} disabled={save.isPending || image === saved}>
44
+ {t("campaigns.category.save")}
45
+ </Button>
46
+ </div>
47
+ </div>
48
+ </Container>
49
+ )
50
+ }
51
+
52
+ export const config = defineWidgetConfig({
53
+ zone: "product_category.details.side",
54
+ })
55
+
56
+ export default CategoryImageWidget
@@ -0,0 +1,135 @@
1
+ # Custom API Routes
2
+
3
+ An API Route is a REST API endpoint.
4
+
5
+ An API Route is created in a TypeScript or JavaScript file under the `/src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`.
6
+
7
+ > Learn more about API Routes in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes)
8
+
9
+ For example, to create a `GET` API Route at `/store/hello-world`, create the file `src/api/store/hello-world/route.ts` with the following content:
10
+
11
+ ```ts
12
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
13
+
14
+ export async function GET(req: MedusaRequest, res: MedusaResponse) {
15
+ res.json({
16
+ message: "Hello world!",
17
+ });
18
+ }
19
+ ```
20
+
21
+ ## Supported HTTP methods
22
+
23
+ The file based routing supports the following HTTP methods:
24
+
25
+ - GET
26
+ - POST
27
+ - PUT
28
+ - PATCH
29
+ - DELETE
30
+ - OPTIONS
31
+ - HEAD
32
+
33
+ You can define a handler for each of these methods by exporting a function with the name of the method in the paths `route.ts` file.
34
+
35
+ For example:
36
+
37
+ ```ts
38
+ import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
39
+
40
+ export async function GET(req: MedusaRequest, res: MedusaResponse) {
41
+ // Handle GET requests
42
+ }
43
+
44
+ export async function POST(req: MedusaRequest, res: MedusaResponse) {
45
+ // Handle POST requests
46
+ }
47
+
48
+ export async function PUT(req: MedusaRequest, res: MedusaResponse) {
49
+ // Handle PUT requests
50
+ }
51
+ ```
52
+
53
+ ## Parameters
54
+
55
+ To create an API route that accepts a path parameter, create a directory within the route's path whose name is of the format `[param]`.
56
+
57
+ For example, if you want to define a route that takes a `productId` parameter, you can do so by creating a file called `/api/products/[productId]/route.ts`:
58
+
59
+ ```ts
60
+ import type {
61
+ MedusaRequest,
62
+ MedusaResponse,
63
+ } from "@medusajs/framework/http"
64
+
65
+ export async function GET(req: MedusaRequest, res: MedusaResponse) {
66
+ const { productId } = req.params;
67
+
68
+ res.json({
69
+ message: `You're looking for product ${productId}`
70
+ })
71
+ }
72
+ ```
73
+
74
+ To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`.
75
+
76
+ For example, if you want to define a route that takes both a `productId` and a `variantId` parameter, you can do so by creating a file called `/api/products/[productId]/variants/[variantId]/route.ts`.
77
+
78
+ ## Using the container
79
+
80
+ The Medusa container is available on `req.scope`. Use it to access modules' main services and other registered resources:
81
+
82
+ ```ts
83
+ import type {
84
+ MedusaRequest,
85
+ MedusaResponse,
86
+ } from "@medusajs/framework/http"
87
+
88
+ export const GET = async (
89
+ req: MedusaRequest,
90
+ res: MedusaResponse
91
+ ) => {
92
+ const productModuleService = req.scope.resolve("product")
93
+
94
+ const [, count] = await productModuleService.listAndCount()
95
+
96
+ res.json({
97
+ count,
98
+ })
99
+ }
100
+ ```
101
+
102
+ ## Middleware
103
+
104
+ You can apply middleware to your routes by creating a file called `/api/middlewares.ts`. This file must export a configuration object with what middleware you want to apply to which routes.
105
+
106
+ For example, if you want to apply a custom middleware function to the `/store/custom` route, you can do so by adding the following to your `/api/middlewares.ts` file:
107
+
108
+ ```ts
109
+ import { defineMiddlewares } from "@medusajs/framework/http"
110
+ import type {
111
+ MedusaRequest,
112
+ MedusaResponse,
113
+ MedusaNextFunction,
114
+ } from "@medusajs/framework/http";
115
+
116
+ async function logger(
117
+ req: MedusaRequest,
118
+ res: MedusaResponse,
119
+ next: MedusaNextFunction
120
+ ) {
121
+ console.log("Request received");
122
+ next();
123
+ }
124
+
125
+ export default defineMiddlewares({
126
+ routes: [
127
+ {
128
+ matcher: "/store/custom",
129
+ middlewares: [logger],
130
+ },
131
+ ],
132
+ })
133
+ ```
134
+
135
+ The `matcher` property can be either a string or a regular expression. The `middlewares` property accepts an array of middleware functions.
@@ -0,0 +1,30 @@
1
+ import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import { ContainerRegistrationKeys, MedusaError } from "@medusajs/framework/utils"
3
+ import { toCampaignJson, type CampaignInput } from "../../../../modules/campaign/validators"
4
+ import { upsertCampaignWorkflow } from "../../../../workflows/upsert-campaign"
5
+
6
+ async function channelOrThrow(req: AuthenticatedMedusaRequest<unknown>) {
7
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
8
+ const { data } = await query.graph({
9
+ entity: "sales_channel",
10
+ fields: ["id", "name", "boutique_campaign.*"],
11
+ filters: { id: req.params.salesChannelId },
12
+ })
13
+ if (!data.length) throw new MedusaError(MedusaError.Types.NOT_FOUND, "Unknown store")
14
+ return data[0]
15
+ }
16
+
17
+ /** GET /admin/boutique-campaigns/:salesChannelId */
18
+ export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
19
+ const channel = await channelOrThrow(req)
20
+ res.json({ slug: channel.name, campaign: channel.boutique_campaign ? toCampaignJson(channel.boutique_campaign) : null })
21
+ }
22
+
23
+ /** POST /admin/boutique-campaigns/:salesChannelId : enregistre la campagne (corps validé par zod, cf. middlewares). */
24
+ export async function POST(req: AuthenticatedMedusaRequest<CampaignInput>, res: MedusaResponse) {
25
+ const channel = await channelOrThrow(req)
26
+ const { result } = await upsertCampaignWorkflow(req.scope).run({
27
+ input: { sales_channel_id: channel.id, campaign: req.validatedBody },
28
+ })
29
+ res.json({ slug: channel.name, campaign: toCampaignJson(result) })
30
+ }
@@ -0,0 +1,24 @@
1
+ import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
3
+ import { toCampaignJson } from "../../../modules/campaign/validators"
4
+
5
+ /** GET /admin/boutique-campaigns : les boutiques du projet (sales channels) et leur campagne. */
6
+ export async function GET(req: AuthenticatedMedusaRequest, res: MedusaResponse) {
7
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
8
+
9
+ const { data: channels } = await query.graph({
10
+ entity: "sales_channel",
11
+ fields: ["id", "name", "description", "boutique_campaign.*"],
12
+ })
13
+
14
+ res.json({
15
+ boutiques: channels
16
+ .filter((c) => c.name !== "Default Sales Channel")
17
+ .map((c) => ({
18
+ sales_channel_id: c.id,
19
+ slug: c.name,
20
+ description: c.description,
21
+ campaign: c.boutique_campaign ? toCampaignJson(c.boutique_campaign) : null,
22
+ })),
23
+ })
24
+ }
@@ -0,0 +1,28 @@
1
+ import { defineMiddlewares, validateAndTransformBody } from "@medusajs/framework/http"
2
+ import { CampaignInput } from "../modules/campaign/validators"
3
+ import { rateLimit } from "./rate-limit"
4
+ import { OrderLookup } from "./store/order-lookup/route"
5
+
6
+ // Anti force brute (par IP) : connexion admin, mot de passe oublié, suivi de commande.
7
+ const loginLimit = rateLimit({ name: "connexion", max: 10, windowMs: 15 * 60 * 1000 })
8
+ const resetLimit = rateLimit({ name: "mot de passe oublié", max: 5, windowMs: 60 * 60 * 1000 })
9
+ const lookupLimit = rateLimit({ name: "suivi de commande", max: 20, windowMs: 10 * 60 * 1000 })
10
+
11
+ export default defineMiddlewares({
12
+ routes: [
13
+ {
14
+ // Routes /admin/* : authentification admin Medusa appliquée par défaut.
15
+ matcher: "/admin/boutique-campaigns/:salesChannelId",
16
+ method: "POST",
17
+ middlewares: [validateAndTransformBody(CampaignInput)],
18
+ },
19
+ { matcher: "/auth/user/emailpass", method: "POST", middlewares: [loginLimit] },
20
+ { matcher: "/auth/user/emailpass/reset-password", method: "POST", middlewares: [resetLimit] },
21
+ { matcher: "/auth/customer/emailpass", method: "POST", middlewares: [loginLimit] },
22
+ {
23
+ matcher: "/store/order-lookup",
24
+ method: "POST",
25
+ middlewares: [lookupLimit, validateAndTransformBody(OrderLookup)],
26
+ },
27
+ ],
28
+ })
@@ -0,0 +1,30 @@
1
+ import type { MedusaNextFunction, MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+
3
+ /**
4
+ * Limite de tentatives par adresse IP (fenêtre glissante, en mémoire : un seul backend par projet).
5
+ * Utilisée pour la connexion admin, le mot de passe oublié et le suivi de commande (anti force brute).
6
+ * Derrière un reverse proxy, l'IP réelle vient de X-Forwarded-For.
7
+ */
8
+ export function rateLimit({ max, windowMs, name }: { max: number; windowMs: number; name: string }) {
9
+ const hits = new Map<string, number[]>()
10
+
11
+ return (req: MedusaRequest, res: MedusaResponse, next: MedusaNextFunction) => {
12
+ const forwarded = req.headers["x-forwarded-for"]
13
+ const ip = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim() || req.ip || "unknown"
14
+ const now = Date.now()
15
+ const recent = (hits.get(ip) ?? []).filter((t) => now - t < windowMs)
16
+
17
+ if (recent.length >= max) {
18
+ const retry = Math.ceil((windowMs - (now - recent[0])) / 1000)
19
+ res.setHeader("Retry-After", String(retry))
20
+ res.status(429).json({ type: "too_many_requests", message: `Trop de tentatives (${name}). Réessayez dans ${Math.ceil(retry / 60)} min.` })
21
+ return
22
+ }
23
+
24
+ recent.push(now)
25
+ hits.set(ip, recent)
26
+ // Nettoyage occasionnel pour ne pas garder les IP inactives en mémoire
27
+ if (hits.size > 10000) for (const [k, v] of hits) if (!v.some((t) => now - t < windowMs)) hits.delete(k)
28
+ next()
29
+ }
30
+ }
@@ -0,0 +1,6 @@
1
+ import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
2
+
3
+ /** La racine du backend n'a pas de page : on renvoie vers l'admin (/app). */
4
+ export async function GET(_req: MedusaRequest, res: MedusaResponse) {
5
+ res.redirect("/app")
6
+ }
@@ -0,0 +1,25 @@
1
+ import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import { ContainerRegistrationKeys, MedusaError } from "@medusajs/framework/utils"
3
+ import { toCampaignJson } from "../../../modules/campaign/validators"
4
+
5
+ /**
6
+ * GET /store/campaign : campagne de LA boutique qui appelle, déterminée par sa publishable key
7
+ * (sales channel associé). Aucun paramètre ne permet de lire la campagne d'une autre boutique.
8
+ */
9
+ export async function GET(req: MedusaStoreRequest, res: MedusaResponse) {
10
+ const salesChannelIds = req.publishable_key_context?.sales_channel_ids ?? []
11
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
12
+
13
+ const { data } = await query.graph({
14
+ entity: "boutique_campaign",
15
+ fields: ["starts_at", "ends_at", "next_campaign", "texts", "vars", "hero_image", "logo", "promo_image", "show_countdown"],
16
+ filters: { sales_channel_id: salesChannelIds },
17
+ })
18
+
19
+ if (!data.length) {
20
+ throw new MedusaError(MedusaError.Types.NOT_FOUND, "No campaign for this store")
21
+ }
22
+
23
+ res.setHeader("Cache-Control", "public, max-age=60")
24
+ res.json({ campaign: toCampaignJson(data[0]) })
25
+ }
@@ -0,0 +1,74 @@
1
+ import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http"
2
+ import { ContainerRegistrationKeys, MedusaError } from "@medusajs/framework/utils"
3
+ import { z } from "@medusajs/framework/zod"
4
+
5
+ export const OrderLookup = z.object({
6
+ number: z.coerce.number().int().positive().max(1e9),
7
+ email: z.email().max(200),
8
+ })
9
+ export type OrderLookup = z.infer<typeof OrderLookup>
10
+
11
+ /**
12
+ * POST /store/order-lookup : suivi de commande sans compte (n° + e-mail de la commande).
13
+ * Réponse seulement si l'e-mail correspond ET si la commande vient de la boutique qui interroge
14
+ * (sales channel de la publishable key). Sinon, même erreur « introuvable » (rien ne fuit).
15
+ * Données renvoyées limitées au suivi : pas d'adresse ni de téléphone. Limité en tentatives.
16
+ */
17
+ export async function POST(req: MedusaStoreRequest<OrderLookup>, res: MedusaResponse) {
18
+ const { number, email } = req.validatedBody
19
+ const channels = req.publishable_key_context?.sales_channel_ids ?? []
20
+ const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)
21
+
22
+ const { data } = await query.graph({
23
+ entity: "order",
24
+ fields: [
25
+ "display_id",
26
+ "email",
27
+ "created_at",
28
+ "status",
29
+ "currency_code",
30
+ "total",
31
+ "sales_channel_id",
32
+ "items.product_title",
33
+ "items.variant_title",
34
+ "items.quantity",
35
+ "fulfillments.shipped_at",
36
+ "fulfillments.delivered_at",
37
+ "fulfillments.canceled_at",
38
+ "fulfillments.labels.tracking_number",
39
+ "fulfillments.labels.tracking_url",
40
+ ],
41
+ // display_id est un nombre en base (le type généré le déclare en texte)
42
+ filters: { display_id: number as unknown as string, sales_channel_id: channels },
43
+ })
44
+
45
+ const order = data.find((o) => o.email?.toLowerCase() === email.toLowerCase())
46
+ if (!order) throw new MedusaError(MedusaError.Types.NOT_FOUND, "Order not found")
47
+
48
+ const shipments = (order.fulfillments ?? []).filter((f) => f && !f.canceled_at)
49
+ // Étape affichée au client, déduite des expéditions
50
+ const step =
51
+ order.status === "canceled"
52
+ ? "canceled"
53
+ : shipments.some((f) => f!.delivered_at)
54
+ ? "delivered"
55
+ : shipments.some((f) => f!.shipped_at)
56
+ ? "shipped"
57
+ : "placed"
58
+
59
+ res.json({
60
+ order: {
61
+ number: order.display_id,
62
+ created_at: order.created_at,
63
+ step,
64
+ currency_code: order.currency_code,
65
+ total: order.total,
66
+ items: (order.items ?? []).filter(Boolean).map((i) => ({ title: i!.product_title, variant: i!.variant_title, quantity: i!.quantity })),
67
+ shipments: shipments.map((f) => ({
68
+ shipped_at: f!.shipped_at,
69
+ delivered_at: f!.delivered_at,
70
+ tracking: (f!.labels ?? []).filter(Boolean).map((l) => ({ number: l!.tracking_number, url: l!.tracking_url })),
71
+ })),
72
+ },
73
+ })
74
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "order_placed": {
3
+ "subject": "Order #{number} confirmed",
4
+ "title": "Thank you for your order!",
5
+ "intro": "Hi {name}, we have received your order #{number}. Here is a summary."
6
+ },
7
+ "order_shipped": {
8
+ "subject": "Your order #{number} is on its way",
9
+ "title": "Your package is on its way",
10
+ "intro": "Good news {name}: your order #{number} has shipped.",
11
+ "tracking": "Tracking number: {code}",
12
+ "track": "Track my package"
13
+ },
14
+ "order_delivered": {
15
+ "subject": "Your order #{number} has been delivered",
16
+ "title": "Your order has been delivered",
17
+ "intro": "Hi {name}, your order #{number} has been delivered. We hope you love it!"
18
+ },
19
+ "order_canceled": {
20
+ "subject": "Your order #{number} has been canceled",
21
+ "title": "Order canceled",
22
+ "intro": "Hi {name}, your order #{number} has been canceled. If you were charged, you will be refunded."
23
+ },
24
+ "refund": {
25
+ "subject": "Refund for your order #{number}",
26
+ "title": "Refund issued",
27
+ "intro": "Hi {name}, we have refunded {amount} for your order #{number}. It can take a few days to appear on your account."
28
+ },
29
+ "return_requested": {
30
+ "subject": "Return requested for order #{number}",
31
+ "title": "Your return request",
32
+ "intro": "Hi {name}, we have registered a return for your order #{number}. Please send the items back in their original packaging."
33
+ },
34
+ "return_received": {
35
+ "subject": "We received your return for order #{number}",
36
+ "title": "Return received",
37
+ "intro": "Hi {name}, we have received the items you returned for order #{number}. Your refund is being processed."
38
+ },
39
+ "merchant_new_order": {
40
+ "subject": "New order #{number} — {total}",
41
+ "title": "New order on {store}",
42
+ "intro": "Order #{number} from {email}, total {total}."
43
+ },
44
+ "common": {
45
+ "item": "Item",
46
+ "qty": "Qty",
47
+ "total": "Total",
48
+ "shippingTo": "Shipping to",
49
+ "questions": "Questions? Just reply to this email.",
50
+ "customer": "customer"
51
+ },
52
+ "password_reset": {
53
+ "subject": "Reset your {store} admin password",
54
+ "title": "Reset your password",
55
+ "intro": "We received a request to reset the password of your {store} admin account. The link is valid for a limited time. If you did not ask for it, ignore this email.",
56
+ "button": "Choose a new password"
57
+ },
58
+ "team_invite": {
59
+ "subject": "You are invited to the {store} admin",
60
+ "title": "Join the {store} team",
61
+ "intro": "You have been invited to manage the {store} stores. Click the button to create your account.",
62
+ "button": "Accept the invitation"
63
+ }
64
+ }