@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,21 @@
1
+ import { T } from "@lib/boutique/i18n-client"
2
+ import { Metadata } from "next"
3
+
4
+ import InteractiveLink from "@modules/common/components/interactive-link"
5
+
6
+ export const metadata: Metadata = {
7
+ title: "404",
8
+ description: "Something went wrong",
9
+ }
10
+
11
+ export default function NotFound() {
12
+ return (
13
+ <div className="flex flex-col gap-4 items-center justify-center min-h-[calc(100vh-64px)]">
14
+ <h1 className="text-2xl-semi text-ui-fg-base"><T k="notFound.title" /></h1>
15
+ <p className="text-small-regular text-ui-fg-base">
16
+ <T k="notFound.text" />
17
+ </p>
18
+ <InteractiveLink href="/"><T k="notFound.cta" /></InteractiveLink>
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,5 @@
1
+ import SkeletonOrderConfirmed from "@modules/skeletons/templates/skeleton-order-confirmed"
2
+
3
+ export default function Loading() {
4
+ return <SkeletonOrderConfirmed />
5
+ }
@@ -0,0 +1,23 @@
1
+ import { t } from "@lib/boutique/i18n"
2
+ import { retrieveOrder } from "@lib/data/orders"
3
+ import OrderCompletedTemplate from "@modules/order/templates/order-completed-template"
4
+ import { Metadata } from "next"
5
+ import { notFound } from "next/navigation"
6
+
7
+ type Props = {
8
+ params: Promise<{ id: string }>
9
+ }
10
+ export async function generateMetadata(): Promise<Metadata> {
11
+ return { title: t("order.confirmed") }
12
+ }
13
+
14
+ export default async function OrderConfirmedPage(props: Props) {
15
+ const params = await props.params
16
+ const order = await retrieveOrder(params.id).catch(() => null)
17
+
18
+ if (!order) {
19
+ return notFound()
20
+ }
21
+
22
+ return <OrderCompletedTemplate order={order} />
23
+ }
@@ -0,0 +1,42 @@
1
+ import { T } from "@lib/boutique/i18n-client"
2
+ import { acceptTransferRequest } from "@lib/data/orders"
3
+ import { Heading, Text } from "@medusajs/ui"
4
+ import TransferImage from "@modules/order/components/transfer-image"
5
+
6
+ export default async function TransferPage({
7
+ params,
8
+ }: {
9
+ params: { id: string; token: string }
10
+ }) {
11
+ const { id, token } = params
12
+
13
+ const { success, error } = await acceptTransferRequest(id, token)
14
+
15
+ return (
16
+ <div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
17
+ <TransferImage />
18
+ <div className="flex flex-col gap-y-6">
19
+ {success && (
20
+ <>
21
+ <Heading level="h1" className="text-xl text-zinc-900">
22
+ <T k="order.transferAccepted" />
23
+ </Heading>
24
+ <Text className="text-zinc-600">
25
+ Order {id} has been successfully transfered to the new owner.
26
+ </Text>
27
+ </>
28
+ )}
29
+ {!success && (
30
+ <>
31
+ <Text className="text-zinc-600">
32
+ <T k="order.transferAcceptError" />
33
+ </Text>
34
+ {error && (
35
+ <Text className="text-red-500">Error message: {error}</Text>
36
+ )}
37
+ </>
38
+ )}
39
+ </div>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,42 @@
1
+ import { T } from "@lib/boutique/i18n-client"
2
+ import { declineTransferRequest } from "@lib/data/orders"
3
+ import { Heading, Text } from "@medusajs/ui"
4
+ import TransferImage from "@modules/order/components/transfer-image"
5
+
6
+ export default async function TransferPage({
7
+ params,
8
+ }: {
9
+ params: { id: string; token: string }
10
+ }) {
11
+ const { id, token } = params
12
+
13
+ const { success, error } = await declineTransferRequest(id, token)
14
+
15
+ return (
16
+ <div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
17
+ <TransferImage />
18
+ <div className="flex flex-col gap-y-6">
19
+ {success && (
20
+ <>
21
+ <Heading level="h1" className="text-xl text-zinc-900">
22
+ <T k="order.transferDeclined" />
23
+ </Heading>
24
+ <Text className="text-zinc-600">
25
+ Transfer of order {id} has been successfully declined.
26
+ </Text>
27
+ </>
28
+ )}
29
+ {!success && (
30
+ <>
31
+ <Text className="text-zinc-600">
32
+ <T k="order.transferDeclineError" />
33
+ </Text>
34
+ {error && (
35
+ <Text className="text-red-500">Error message: {error}</Text>
36
+ )}
37
+ </>
38
+ )}
39
+ </div>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,35 @@
1
+ import { T } from "@lib/boutique/i18n-client"
2
+ import { Heading, Text } from "@medusajs/ui"
3
+ import TransferActions from "@modules/order/components/transfer-actions"
4
+ import TransferImage from "@modules/order/components/transfer-image"
5
+
6
+ export default async function TransferPage({
7
+ params,
8
+ }: {
9
+ params: { id: string; token: string }
10
+ }) {
11
+ const { id, token } = params
12
+
13
+ return (
14
+ <div className="flex flex-col gap-y-4 items-start w-2/5 mx-auto mt-10 mb-20">
15
+ <TransferImage />
16
+ <div className="flex flex-col gap-y-6">
17
+ <Heading level="h1" className="text-xl text-zinc-900">
18
+ <T k="order.transferTitle" vars={{ id }} />
19
+ </Heading>
20
+ <Text className="text-zinc-600">
21
+ <T k="order.transferText1" vars={{ id }} />
22
+ </Text>
23
+ <div className="w-full h-px bg-zinc-200" />
24
+ <Text className="text-zinc-600">
25
+ <T k="order.transferText2" />
26
+ </Text>
27
+ <Text className="text-zinc-600">
28
+ <T k="order.transferText3" />
29
+ </Text>
30
+ <div className="w-full h-px bg-zinc-200" />
31
+ <TransferActions id={id} token={token} />
32
+ </div>
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,32 @@
1
+ import { Suspense } from "react"
2
+ import { Metadata } from "next"
3
+ import Link from "next/link"
4
+ import { t } from "@lib/boutique/i18n"
5
+ import TrackForm from "@modules/ub/track-form"
6
+
7
+ export async function generateMetadata(): Promise<Metadata> {
8
+ return { title: t("track.title") }
9
+ }
10
+
11
+ /** Suivi de commande sans compte : n° de commande + e-mail (vérifiés par le backend). */
12
+ export default function TrackOrderPage() {
13
+ return (
14
+ <section className="ub-section">
15
+ <div className="ub-wrap" style={{ maxWidth: 720, display: "grid", gap: 16 }}>
16
+ <h1 className="ub-head" style={{ fontSize: 36, lineHeight: "44px" }}>
17
+ {t("track.title")}
18
+ </h1>
19
+ <p>{t("track.intro")}</p>
20
+ <Suspense fallback={<div aria-busy="true" style={{ minHeight: 200 }} />}>
21
+ <TrackForm />
22
+ </Suspense>
23
+ <p style={{ fontSize: 14 }}>
24
+ {t("track.account")}{" "}
25
+ <Link href="/account" style={{ textDecoration: "underline" }}>
26
+ {t("track.cta")}
27
+ </Link>
28
+ </p>
29
+ </div>
30
+ </section>
31
+ )
32
+ }
@@ -0,0 +1,68 @@
1
+ import { Suspense } from "react"
2
+ import { connection } from "next/server"
3
+
4
+ import { campaignStatus, getCampaign } from "@lib/boutique/campaign"
5
+ import { t } from "@lib/boutique/i18n"
6
+ import { listCategories } from "@lib/data/categories"
7
+ import { listProducts } from "@lib/data/products"
8
+ import { Hero, PromoBanner, SectionHead, TrustBar } from "@modules/ub/campaign-blocks"
9
+ import CampaignEnded from "@modules/ub/campaign-ended"
10
+ import CategoryTiles from "@modules/ub/category-tiles"
11
+ import { ProductGrid } from "@modules/ub/product-card"
12
+
13
+ async function Categories({ title }: { title: string }) {
14
+ const categories = await listCategories().catch(() => [])
15
+ if (!categories.length) return null
16
+
17
+ return (
18
+ <section className="ub-section">
19
+ <div className="ub-wrap">
20
+ <SectionHead title={title} href="/store" />
21
+ <CategoryTiles categories={categories} />
22
+ </div>
23
+ </section>
24
+ )
25
+ }
26
+
27
+ async function Products({ title }: { title: string }) {
28
+ const {
29
+ response: { products },
30
+ } = await listProducts({ queryParams: { limit: 8, fields: "*categories,*collection" } })
31
+ if (!products.length) return null
32
+
33
+ return (
34
+ <section className="ub-section">
35
+ <div className="ub-wrap">
36
+ <SectionHead title={title} href="/store" />
37
+ <ProductGrid products={products} />
38
+ </div>
39
+ </section>
40
+ )
41
+ }
42
+
43
+ function GridFallback() {
44
+ return <div className="ub-section" aria-busy="true" style={{ minHeight: 320 }} />
45
+ }
46
+
47
+ export default async function Home() {
48
+ await connection()
49
+ const campaign = await getCampaign()
50
+
51
+ if (campaignStatus(campaign) === "ended") {
52
+ return <CampaignEnded nextUrl={campaign.nextCampaignUrl} />
53
+ }
54
+
55
+ return (
56
+ <>
57
+ <Hero campaign={campaign} />
58
+ <TrustBar freeShipping={campaign.freeShipping} />
59
+ <Suspense fallback={<GridFallback />}>
60
+ <Categories title={campaign.sections?.categories ?? t("section.categories")} />
61
+ </Suspense>
62
+ <PromoBanner campaign={campaign} />
63
+ <Suspense fallback={<GridFallback />}>
64
+ <Products title={campaign.sections?.products ?? t("section.products")} />
65
+ </Suspense>
66
+ </>
67
+ )
68
+ }
@@ -0,0 +1,86 @@
1
+ import { Metadata } from "next"
2
+ import { notFound } from "next/navigation"
3
+ import { listProducts } from "@lib/data/products"
4
+ import { getRegion } from "@lib/data/regions"
5
+ import ProductTemplate from "@modules/products/templates"
6
+ import { HttpTypes } from "@medusajs/types"
7
+
8
+ type Props = {
9
+ params: Promise<{ handle: string }>
10
+ searchParams: Promise<{ v_id?: string }>
11
+ }
12
+
13
+ function getImagesForVariant(
14
+ product: HttpTypes.StoreProduct,
15
+ selectedVariantId?: string
16
+ ) {
17
+ if (!selectedVariantId || !product.variants) {
18
+ return product.images
19
+ }
20
+
21
+ const variant = product.variants!.find((v) => v.id === selectedVariantId)
22
+ if (!variant || !variant.images?.length) {
23
+ return product.images
24
+ }
25
+
26
+ const imageIdsMap = new Map((variant.images ?? []).map((i) => [i.id, true]))
27
+ return product.images!.filter((i) => imageIdsMap.has(i.id))
28
+ }
29
+
30
+ export async function generateMetadata(props: Props): Promise<Metadata> {
31
+ const params = await props.params
32
+ const { handle } = params
33
+ const region = await getRegion()
34
+
35
+ if (!region) {
36
+ notFound()
37
+ }
38
+
39
+ const product = await listProducts({
40
+ queryParams: { handle },
41
+ }).then(({ response }) => response.products[0])
42
+
43
+ if (!product) {
44
+ notFound()
45
+ }
46
+
47
+ return {
48
+ title: product.title,
49
+ description: `${product.title}`,
50
+ openGraph: {
51
+ title: product.title,
52
+ description: `${product.title}`,
53
+ images: product.thumbnail ? [product.thumbnail] : [],
54
+ },
55
+ }
56
+ }
57
+
58
+ export default async function ProductPage(props: Props) {
59
+ const params = await props.params
60
+ const region = await getRegion()
61
+ const searchParams = await props.searchParams
62
+
63
+ const selectedVariantId = searchParams.v_id
64
+
65
+ if (!region) {
66
+ notFound()
67
+ }
68
+
69
+ const pricedProduct = await listProducts({
70
+ queryParams: { handle: params.handle },
71
+ }).then(({ response }) => response.products[0])
72
+
73
+ if (!pricedProduct) {
74
+ notFound()
75
+ }
76
+
77
+ const images = getImagesForVariant(pricedProduct, selectedVariantId) ?? []
78
+
79
+ return (
80
+ <ProductTemplate
81
+ product={pricedProduct}
82
+ region={region}
83
+ images={images}
84
+ />
85
+ )
86
+ }
@@ -0,0 +1,28 @@
1
+ import { t } from "@lib/boutique/i18n"
2
+ import { Metadata } from "next"
3
+
4
+ import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
5
+ import StoreTemplate from "@modules/store/templates"
6
+
7
+ export async function generateMetadata(): Promise<Metadata> {
8
+ return { title: t("footer.all") }
9
+ }
10
+
11
+ type Params = {
12
+ searchParams: Promise<{
13
+ sortBy?: SortOptions
14
+ page?: string
15
+ }>
16
+ }
17
+
18
+ export default async function StorePage(props: Params) {
19
+ const searchParams = await props.searchParams;
20
+ const { sortBy, page } = searchParams
21
+
22
+ return (
23
+ <StoreTemplate
24
+ sortBy={sortBy}
25
+ page={page}
26
+ />
27
+ )
28
+ }
@@ -0,0 +1,13 @@
1
+ import { getBoutique } from "@lib/boutique/config"
2
+
3
+ export const dynamic = "force-dynamic"
4
+
5
+ /** Healthcheck Docker : 200 si la config de la boutique est chargée. */
6
+ export function GET() {
7
+ try {
8
+ const { slug } = getBoutique()
9
+ return Response.json({ status: "ok", boutique: slug })
10
+ } catch {
11
+ return Response.json({ status: "error" }, { status: 503 })
12
+ }
13
+ }
@@ -0,0 +1,42 @@
1
+ import { getCampaign } from "@lib/boutique/campaign"
2
+ import { getBoutique } from "@lib/boutique/config"
3
+ import { themeFontClasses } from "@lib/boutique/fonts"
4
+ import { messages } from "@lib/boutique/i18n"
5
+ import { I18nProvider } from "@lib/boutique/i18n-client"
6
+ import { Metadata } from "next"
7
+ import "styles/globals.css"
8
+ import "styles/ub/tokens.css"
9
+ import "styles/ub/bundle.css"
10
+ import "styles/ub/fonts.css"
11
+ import "styles/ub/medusa-ui.css"
12
+ import "styles/ub/overrides.css"
13
+
14
+ // Tout dépend de la boutique de l'instance (lue à l'exécution) : rien n'est pré-rendu au build.
15
+ export const dynamic = "force-dynamic"
16
+
17
+ export async function generateMetadata(): Promise<Metadata> {
18
+ const b = getBoutique()
19
+ // Favicon = logo de la boutique s'il existe (admin → Campagnes)
20
+ const logo = await getCampaign()
21
+ .then((c) => c.logo)
22
+ .catch(() => undefined)
23
+ return {
24
+ metadataBase: new URL(b.siblings[b.slug]),
25
+ title: { default: b.storeName, template: `%s | ${b.storeName}` },
26
+ ...(logo ? { icons: { icon: logo, apple: logo } } : {}),
27
+ }
28
+ }
29
+
30
+ export default function RootLayout(props: { children: React.ReactNode }) {
31
+ const b = getBoutique()
32
+
33
+ return (
34
+ <html lang={b.locale} data-theme={b.theme.id} data-mode="light" className={themeFontClasses(b.theme)}>
35
+ <body className="ub">
36
+ <I18nProvider messages={messages("")}>
37
+ <main className="relative">{props.children}</main>
38
+ </I18nProvider>
39
+ </body>
40
+ </html>
41
+ )
42
+ }
@@ -0,0 +1,21 @@
1
+ import { Metadata } from "next"
2
+ import { t } from "@lib/boutique/i18n"
3
+ import Button from "@modules/ub/button"
4
+
5
+ export const metadata: Metadata = {
6
+ title: "404",
7
+ }
8
+
9
+ export default function NotFound() {
10
+ return (
11
+ <section className="ub-section">
12
+ <div className="ub-wrap" style={{ display: "grid", gap: 16, justifyItems: "center", textAlign: "center", padding: "96px 24px" }}>
13
+ <h1 className="ub-head" style={{ fontSize: 40, lineHeight: "48px" }}>
14
+ {t("notFound.title")}
15
+ </h1>
16
+ <p>{t("notFound.text")}</p>
17
+ <Button href="/">{t("notFound.cta")}</Button>
18
+ </div>
19
+ </section>
20
+ )
21
+ }
@@ -0,0 +1,18 @@
1
+ import { getCampaign } from "@lib/boutique/campaign"
2
+ import { getBoutique } from "@lib/boutique/config"
3
+
4
+ /**
5
+ * On charge et valide la config de la boutique (project.json, thème, publishable key,
6
+ * fichier de campagne). En cas d'erreur, le process s'arrête avec un message clair
7
+ * au lieu de servir une boutique cassée. Rien à valider pendant `next build`.
8
+ */
9
+ if (process.env.NEXT_PHASE !== "phase-production-build") {
10
+ try {
11
+ const b = getBoutique()
12
+ await getCampaign()
13
+ console.log(`[boutique] ${b.slug} — thème ${b.theme.id}, ${b.locale}, ${b.currency.toUpperCase()}`)
14
+ } catch (e) {
15
+ console.error(`\n${(e as Error).message}\n`)
16
+ process.exit(1)
17
+ }
18
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Démarrage de l'instance : voir instrumentation-node.ts (validation de la
3
+ * config de la boutique, arrêt du process si elle est invalide).
4
+ */
5
+ export async function register() {
6
+ if (process.env.NEXT_RUNTIME === "nodejs") {
7
+ await import("./instrumentation-node")
8
+ }
9
+ }
@@ -0,0 +1,116 @@
1
+ import "server-only"
2
+
3
+ import fs from "node:fs"
4
+ import path from "node:path"
5
+ import { cache } from "react"
6
+ import { sdk } from "@lib/config"
7
+ import { CONFIG_DIR, THEMES_DIR, BoutiqueConfigError, getBoutique, parseOrThrow } from "./config"
8
+ import { campaignFileSchema, campaignTextsSchema, type CampaignTexts } from "./schema"
9
+
10
+ export type Campaign = CampaignTexts & {
11
+ startsAt: string
12
+ endsAt: string
13
+ /** URL de la boutique suivante du client, si définie */
14
+ nextCampaignUrl?: string
15
+ /** Seuil de livraison offerte, déjà formaté (ex. "$50") */
16
+ freeShipping?: string
17
+ /** Logo (URL) ; absent = pastille de couleur + nom */
18
+ logo?: string
19
+ /** Compte à rebours : undefined = selon le thème (CSS), true = toujours, false = jamais */
20
+ showCountdown?: boolean
21
+ }
22
+
23
+ function readJsonIfExists(file: string): unknown | undefined {
24
+ if (!fs.existsSync(file)) return undefined
25
+ try {
26
+ return JSON.parse(fs.readFileSync(file, "utf8"))
27
+ } catch (e) {
28
+ throw new BoutiqueConfigError(`JSON invalide dans ${file} : ${(e as Error).message}`)
29
+ }
30
+ }
31
+
32
+ /** Textes par défaut du thème dans la langue du projet, repli en anglais. */
33
+ function themeTexts(themeId: string, language: string): CampaignTexts {
34
+ for (const lang of [language, "en"]) {
35
+ const file = path.join(THEMES_DIR, themeId, `campaign.${lang}.json`)
36
+ const data = readJsonIfExists(file)
37
+ if (data !== undefined) return parseOrThrow(campaignTextsSchema, data, file)
38
+ }
39
+ throw new BoutiqueConfigError(`aucun texte par défaut pour le thème « ${themeId} » (campaign.en.json manquant)`)
40
+ }
41
+
42
+ /** Remplace {discount}, {endDate}, {freeShipping} ; une variable inconnue ou vide disparaît (jamais de clé à l'écran). */
43
+ function fill(text: string, vars: Record<string, string | undefined>): string {
44
+ return text
45
+ .replace(/\{(\w+)\}/g, (_, k: string) => vars[k] ?? "")
46
+ .replace(/\s{2,}/g, " ")
47
+ .trim()
48
+ }
49
+
50
+ function fillAll<T>(value: T, vars: Record<string, string | undefined>): T {
51
+ if (typeof value === "string") return fill(value, vars) as T
52
+ if (Array.isArray(value)) return value.map((v) => fillAll(v, vars)) as T
53
+ if (value && typeof value === "object") {
54
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, fillAll(v, vars)])) as T
55
+ }
56
+ return value
57
+ }
58
+
59
+ /**
60
+ * Campagne enregistrée pour la boutique. Source de vérité : la base Medusa, modifiée dans
61
+ * l'admin « Campagnes » (GET /store/campaign, cache 60 s : une modification apparaît en
62
+ * une minute sans redémarrage). Secours : config/campaigns/<slug>.json, valeur initiale
63
+ * importée par le seed, si le backend n'a pas encore de campagne pour cette boutique.
64
+ */
65
+ async function storedCampaign(slug: string): Promise<unknown> {
66
+ try {
67
+ const { campaign } = await sdk.client.fetch<{ campaign: unknown }>("/store/campaign", {
68
+ method: "GET",
69
+ next: { revalidate: 60 },
70
+ cache: "force-cache",
71
+ })
72
+ return campaign
73
+ } catch {
74
+ const file = path.join(/*turbopackIgnore: true*/ CONFIG_DIR, "campaigns", `${slug}.json`)
75
+ const raw = readJsonIfExists(file)
76
+ if (raw === undefined) {
77
+ throw new BoutiqueConfigError(`aucune campagne pour ${slug} : ni dans le backend, ni dans ${file}`)
78
+ }
79
+ return raw
80
+ }
81
+ }
82
+
83
+ /** Campagne affichée : textes enregistrés + textes par défaut du thème + variables. */
84
+ export const getCampaign = cache(async (): Promise<Campaign> => {
85
+ const b = getBoutique()
86
+ const defaults = themeTexts(b.theme.id, b.language)
87
+ const own = parseOrThrow(campaignFileSchema, await storedCampaign(b.slug), `campagne ${b.slug}`)
88
+
89
+ const endDate = new Intl.DateTimeFormat(b.locale, { month: "long", day: "numeric" }).format(new Date(own.endsAt))
90
+ const vars = { discount: own.vars?.discount, freeShipping: own.vars?.freeShipping, endDate }
91
+
92
+ const texts: CampaignTexts = {
93
+ topbar: own.topbar ?? defaults.topbar,
94
+ hero: { ...defaults.hero, ...own.hero },
95
+ promo: { ...defaults.promo, ...own.promo },
96
+ sections: defaults.sections || own.sections ? { ...defaults.sections!, ...own.sections } : undefined,
97
+ }
98
+
99
+ return {
100
+ ...fillAll(texts, vars),
101
+ startsAt: own.startsAt,
102
+ endsAt: own.endsAt,
103
+ nextCampaignUrl: own.nextCampaign ? b.siblings[own.nextCampaign] : undefined,
104
+ freeShipping: own.vars?.freeShipping,
105
+ logo: own.logo,
106
+ showCountdown: own.showCountdown,
107
+ }
108
+ })
109
+
110
+ export type CampaignStatus = "upcoming" | "live" | "ended"
111
+
112
+ export function campaignStatus(c: Pick<Campaign, "startsAt" | "endsAt">, now = Date.now()): CampaignStatus {
113
+ if (now < Date.parse(c.startsAt)) return "upcoming"
114
+ if (now > Date.parse(c.endsAt)) return "ended"
115
+ return "live"
116
+ }