@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,65 @@
1
+ import { Table } from "@medusajs/ui"
2
+
3
+ import repeat from "@lib/util/repeat"
4
+ import SkeletonCartItem from "@modules/skeletons/components/skeleton-cart-item"
5
+ import SkeletonCodeForm from "@modules/skeletons/components/skeleton-code-form"
6
+ import SkeletonOrderSummary from "@modules/skeletons/components/skeleton-order-summary"
7
+
8
+ const SkeletonCartPage = () => {
9
+ return (
10
+ <div className="py-12">
11
+ <div className="content-container">
12
+ <div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40">
13
+ <div className="flex flex-col bg-white p-6 gap-y-6">
14
+ <div className="bg-white flex items-start justify-between">
15
+ <div className="flex flex-col gap-y-2">
16
+ <div className="w-60 h-8 bg-gray-200 animate-pulse" />
17
+ <div className="w-48 h-6 bg-gray-200 animate-pulse" />
18
+ </div>
19
+ <div>
20
+ <div className="w-14 h-8 bg-gray-200 animate-pulse" />
21
+ </div>
22
+ </div>
23
+ <div>
24
+ <div className="pb-3 flex items-center">
25
+ <div className="w-20 h-12 bg-gray-200 animate-pulse" />
26
+ </div>
27
+ <Table>
28
+ <Table.Header className="border-t-0">
29
+ <Table.Row>
30
+ <Table.HeaderCell className="!pl-0">
31
+ <div className="w-10 h-6 bg-gray-200 animate-pulse" />
32
+ </Table.HeaderCell>
33
+ <Table.HeaderCell></Table.HeaderCell>
34
+ <Table.HeaderCell>
35
+ <div className="w-16 h-6 bg-gray-200 animate-pulse" />
36
+ </Table.HeaderCell>
37
+ <Table.HeaderCell>
38
+ <div className="w-12 h-6 bg-gray-200 animate-pulse" />
39
+ </Table.HeaderCell>
40
+ <Table.HeaderCell className="!pr-0">
41
+ <div className="flex justify-end">
42
+ <div className="w-12 h-6 bg-gray-200 animate-pulse" />
43
+ </div>
44
+ </Table.HeaderCell>
45
+ </Table.Row>
46
+ </Table.Header>
47
+ <Table.Body>
48
+ {repeat(4).map((index) => (
49
+ <SkeletonCartItem key={index} />
50
+ ))}
51
+ </Table.Body>
52
+ </Table>
53
+ </div>
54
+ </div>
55
+ <div className="flex flex-col gap-y-8">
56
+ <SkeletonOrderSummary />
57
+ <SkeletonCodeForm />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ export default SkeletonCartPage
@@ -0,0 +1,21 @@
1
+ import SkeletonOrderConfirmedHeader from "@modules/skeletons/components/skeleton-order-confirmed-header"
2
+ import SkeletonOrderInformation from "@modules/skeletons/components/skeleton-order-information"
3
+ import SkeletonOrderItems from "@modules/skeletons/components/skeleton-order-items"
4
+
5
+ const SkeletonOrderConfirmed = () => {
6
+ return (
7
+ <div className="bg-gray-50 py-6 min-h-[calc(100vh-64px)] animate-pulse">
8
+ <div className="content-container flex justify-center">
9
+ <div className="max-w-4xl h-full bg-white w-full p-10">
10
+ <SkeletonOrderConfirmedHeader />
11
+
12
+ <SkeletonOrderItems />
13
+
14
+ <SkeletonOrderInformation />
15
+ </div>
16
+ </div>
17
+ </div>
18
+ )
19
+ }
20
+
21
+ export default SkeletonOrderConfirmed
@@ -0,0 +1,23 @@
1
+ import repeat from "@lib/util/repeat"
2
+ import SkeletonProductPreview from "@modules/skeletons/components/skeleton-product-preview"
3
+
4
+ const SkeletonProductGrid = ({
5
+ numberOfProducts = 8,
6
+ }: {
7
+ numberOfProducts?: number
8
+ }) => {
9
+ return (
10
+ <ul
11
+ className="grid grid-cols-2 small:grid-cols-3 medium:grid-cols-4 gap-x-6 gap-y-8 flex-1"
12
+ data-testid="products-list-loader"
13
+ >
14
+ {repeat(numberOfProducts).map((index) => (
15
+ <li key={index}>
16
+ <SkeletonProductPreview />
17
+ </li>
18
+ ))}
19
+ </ul>
20
+ )
21
+ }
22
+
23
+ export default SkeletonProductGrid
@@ -0,0 +1,114 @@
1
+ "use client"
2
+
3
+ import { clx } from "@medusajs/ui"
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
5
+
6
+ export function Pagination({
7
+ page,
8
+ totalPages,
9
+ 'data-testid': dataTestid
10
+ }: {
11
+ page: number
12
+ totalPages: number
13
+ 'data-testid'?: string
14
+ }) {
15
+ const router = useRouter()
16
+ const pathname = usePathname()
17
+ const searchParams = useSearchParams()
18
+
19
+ // Helper function to generate an array of numbers within a range
20
+ const arrayRange = (start: number, stop: number) =>
21
+ Array.from({ length: stop - start + 1 }, (_, index) => start + index)
22
+
23
+ // Function to handle page changes
24
+ const handlePageChange = (newPage: number) => {
25
+ const params = new URLSearchParams(searchParams)
26
+ params.set("page", newPage.toString())
27
+ router.push(`${pathname}?${params.toString()}`)
28
+ }
29
+
30
+ // Function to render a page button
31
+ const renderPageButton = (
32
+ p: number,
33
+ label: string | number,
34
+ isCurrent: boolean
35
+ ) => (
36
+ <button
37
+ key={p}
38
+ className={clx("txt-xlarge-plus text-ui-fg-muted", {
39
+ "text-ui-fg-base hover:text-ui-fg-subtle": isCurrent,
40
+ })}
41
+ disabled={isCurrent}
42
+ onClick={() => handlePageChange(p)}
43
+ >
44
+ {label}
45
+ </button>
46
+ )
47
+
48
+ // Function to render ellipsis
49
+ const renderEllipsis = (key: string) => (
50
+ <span
51
+ key={key}
52
+ className="txt-xlarge-plus text-ui-fg-muted items-center cursor-default"
53
+ >
54
+ ...
55
+ </span>
56
+ )
57
+
58
+ // Function to render page buttons based on the current page and total pages
59
+ const renderPageButtons = () => {
60
+ const buttons = []
61
+
62
+ if (totalPages <= 7) {
63
+ // Show all pages
64
+ buttons.push(
65
+ ...arrayRange(1, totalPages).map((p) =>
66
+ renderPageButton(p, p, p === page)
67
+ )
68
+ )
69
+ } else {
70
+ // Handle different cases for displaying pages and ellipses
71
+ if (page <= 4) {
72
+ // Show 1, 2, 3, 4, 5, ..., lastpage
73
+ buttons.push(
74
+ ...arrayRange(1, 5).map((p) => renderPageButton(p, p, p === page))
75
+ )
76
+ buttons.push(renderEllipsis("ellipsis1"))
77
+ buttons.push(
78
+ renderPageButton(totalPages, totalPages, totalPages === page)
79
+ )
80
+ } else if (page >= totalPages - 3) {
81
+ // Show 1, ..., lastpage - 4, lastpage - 3, lastpage - 2, lastpage - 1, lastpage
82
+ buttons.push(renderPageButton(1, 1, 1 === page))
83
+ buttons.push(renderEllipsis("ellipsis2"))
84
+ buttons.push(
85
+ ...arrayRange(totalPages - 4, totalPages).map((p) =>
86
+ renderPageButton(p, p, p === page)
87
+ )
88
+ )
89
+ } else {
90
+ // Show 1, ..., page - 1, page, page + 1, ..., lastpage
91
+ buttons.push(renderPageButton(1, 1, 1 === page))
92
+ buttons.push(renderEllipsis("ellipsis3"))
93
+ buttons.push(
94
+ ...arrayRange(page - 1, page + 1).map((p) =>
95
+ renderPageButton(p, p, p === page)
96
+ )
97
+ )
98
+ buttons.push(renderEllipsis("ellipsis4"))
99
+ buttons.push(
100
+ renderPageButton(totalPages, totalPages, totalPages === page)
101
+ )
102
+ }
103
+ }
104
+
105
+ return buttons
106
+ }
107
+
108
+ // Render the component
109
+ return (
110
+ <div className="flex justify-center w-full mt-12">
111
+ <div className="flex gap-3 items-end" data-testid={dataTestid}>{renderPageButtons()}</div>
112
+ </div>
113
+ )
114
+ }
@@ -0,0 +1,41 @@
1
+ "use client"
2
+
3
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
4
+ import { useCallback } from "react"
5
+
6
+ import SortProducts, { SortOptions } from "./sort-products"
7
+
8
+ type RefinementListProps = {
9
+ sortBy: SortOptions
10
+ search?: boolean
11
+ 'data-testid'?: string
12
+ }
13
+
14
+ const RefinementList = ({ sortBy, 'data-testid': dataTestId }: RefinementListProps) => {
15
+ const router = useRouter()
16
+ const pathname = usePathname()
17
+ const searchParams = useSearchParams()
18
+
19
+ const createQueryString = useCallback(
20
+ (name: string, value: string) => {
21
+ const params = new URLSearchParams(searchParams)
22
+ params.set(name, value)
23
+
24
+ return params.toString()
25
+ },
26
+ [searchParams]
27
+ )
28
+
29
+ const setQueryParams = (name: string, value: string) => {
30
+ const query = createQueryString(name, value)
31
+ router.push(`${pathname}?${query}`)
32
+ }
33
+
34
+ return (
35
+ <div className="flex small:flex-col gap-12 py-4 mb-8 small:px-0 pl-6 small:min-w-[250px] small:ml-[1.675rem]">
36
+ <SortProducts sortBy={sortBy} setQueryParams={setQueryParams} data-testid={dataTestId} />
37
+ </div>
38
+ )
39
+ }
40
+
41
+ export default RefinementList
@@ -0,0 +1,48 @@
1
+ "use client"
2
+
3
+ import FilterRadioGroup from "@modules/common/components/filter-radio-group"
4
+
5
+ export type SortOptions = "price_asc" | "price_desc" | "created_at"
6
+
7
+ type SortProductsProps = {
8
+ sortBy: SortOptions
9
+ setQueryParams: (name: string, value: SortOptions) => void
10
+ "data-testid"?: string
11
+ }
12
+
13
+ const sortOptions = [
14
+ {
15
+ value: "created_at",
16
+ label: "store.latest",
17
+ },
18
+ {
19
+ value: "price_asc",
20
+ label: "store.priceAsc",
21
+ },
22
+ {
23
+ value: "price_desc",
24
+ label: "store.priceDesc",
25
+ },
26
+ ]
27
+
28
+ const SortProducts = ({
29
+ "data-testid": dataTestId,
30
+ sortBy,
31
+ setQueryParams,
32
+ }: SortProductsProps) => {
33
+ const handleChange = (value: SortOptions) => {
34
+ setQueryParams("sortBy", value)
35
+ }
36
+
37
+ return (
38
+ <FilterRadioGroup
39
+ title="store.sortBy"
40
+ items={sortOptions}
41
+ value={sortBy}
42
+ handleChange={handleChange}
43
+ data-testid={dataTestId}
44
+ />
45
+ )
46
+ }
47
+
48
+ export default SortProducts
@@ -0,0 +1,42 @@
1
+ import { Suspense } from "react"
2
+
3
+ import { t } from "@lib/boutique/i18n"
4
+
5
+ import SkeletonProductGrid from "@modules/skeletons/templates/skeleton-product-grid"
6
+ import RefinementList from "@modules/store/components/refinement-list"
7
+ import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
8
+
9
+ import PaginatedProducts from "./paginated-products"
10
+
11
+ const StoreTemplate = ({
12
+ sortBy,
13
+ page,
14
+ }: {
15
+ sortBy?: SortOptions
16
+ page?: string
17
+ }) => {
18
+ const pageNumber = page ? parseInt(page) : 1
19
+ const sort = sortBy || "created_at"
20
+
21
+ return (
22
+ <div
23
+ className="flex flex-col small:flex-row small:items-start py-6 content-container"
24
+ data-testid="category-container"
25
+ >
26
+ <RefinementList sortBy={sort} />
27
+ <div className="w-full">
28
+ <div className="mb-8 text-2xl-semi">
29
+ <h1 className="ub-head" data-testid="store-page-title">{t("footer.all")}</h1>
30
+ </div>
31
+ <Suspense fallback={<SkeletonProductGrid />}>
32
+ <PaginatedProducts
33
+ sortBy={sort}
34
+ page={pageNumber}
35
+ />
36
+ </Suspense>
37
+ </div>
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export default StoreTemplate
@@ -0,0 +1,80 @@
1
+ import { listProductsWithSort } from "@lib/data/products"
2
+ import { getRegion } from "@lib/data/regions"
3
+ import { ProductGrid } from "@modules/ub/product-card"
4
+ import { Pagination } from "@modules/store/components/pagination"
5
+ import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
6
+
7
+ const PRODUCT_LIMIT = 12
8
+
9
+ type PaginatedProductsParams = {
10
+ limit: number
11
+ collection_id?: string[]
12
+ category_id?: string[]
13
+ id?: string[]
14
+ order?: string
15
+ }
16
+
17
+ export default async function PaginatedProducts({
18
+ sortBy,
19
+ page,
20
+ collectionId,
21
+ categoryId,
22
+ productsIds,
23
+ }: {
24
+ sortBy?: SortOptions
25
+ page: number
26
+ collectionId?: string
27
+ categoryId?: string
28
+ productsIds?: string[]
29
+ }) {
30
+ const queryParams: PaginatedProductsParams = {
31
+ limit: 12,
32
+ }
33
+
34
+ if (collectionId) {
35
+ queryParams["collection_id"] = [collectionId]
36
+ }
37
+
38
+ if (categoryId) {
39
+ queryParams["category_id"] = [categoryId]
40
+ }
41
+
42
+ if (productsIds) {
43
+ queryParams["id"] = productsIds
44
+ }
45
+
46
+ if (sortBy === "created_at") {
47
+ queryParams["order"] = "created_at"
48
+ }
49
+
50
+ const region = await getRegion()
51
+
52
+ if (!region) {
53
+ return null
54
+ }
55
+
56
+ let {
57
+ response: { products, count },
58
+ } = await listProductsWithSort({
59
+ page,
60
+ queryParams,
61
+ sortBy,
62
+ })
63
+
64
+ const totalPages = Math.ceil(count / PRODUCT_LIMIT)
65
+
66
+ return (
67
+ <>
68
+ <div data-testid="products-list">
69
+ <ProductGrid products={products} />
70
+ </div>
71
+ {totalPages > 1 && (
72
+ <Pagination
73
+ data-testid="product-pagination"
74
+ page={page}
75
+ totalPages={totalPages}
76
+ />
77
+ )}
78
+ </>
79
+ )
80
+ }
@@ -0,0 +1,41 @@
1
+ import Link from "next/link"
2
+ import { Icon } from "./icons"
3
+
4
+ type Variant = "primary" | "dark" | "ghost"
5
+
6
+ /** Bouton des maquettes : libellé + pastille-flèche (visibles selon le preset du thème). */
7
+ export default function Button({
8
+ href,
9
+ children,
10
+ variant = "primary",
11
+ small,
12
+ full,
13
+ className = "",
14
+ }: {
15
+ href: string
16
+ children: React.ReactNode
17
+ variant?: Variant
18
+ small?: boolean
19
+ full?: boolean
20
+ className?: string
21
+ }) {
22
+ const cls = ["ub-btn", `ub-btn--${variant}`, small && "ub-btn--sm", full && "ub-btn--full", className]
23
+ .filter(Boolean)
24
+ .join(" ")
25
+
26
+ return (
27
+ <Link className={cls} href={href}>
28
+ <span>{children}</span>
29
+ {variant === "primary" && (
30
+ <>
31
+ <span className="ub-btn__icon">
32
+ <Icon name="arrowUpRight" />
33
+ </span>
34
+ <span className="ub-btn__arrow" style={small ? { width: 28, height: 28 } : undefined}>
35
+ <Icon name="arrowUpRight" />
36
+ </span>
37
+ </>
38
+ )}
39
+ </Link>
40
+ )
41
+ }
@@ -0,0 +1,197 @@
1
+ import Link from "next/link"
2
+ import { connection } from "next/server"
3
+ import type { Campaign } from "@lib/boutique/campaign"
4
+ import { t } from "@lib/boutique/i18n"
5
+ import Button from "./button"
6
+ import Countdown from "./countdown"
7
+ import { Icon, type IconName } from "./icons"
8
+
9
+ /** Titre avec le mot mis en couleur : `highlight` est retrouvé dans `title` (sinon ajouté à la fin), rendu en <em>. Jamais de HTML. */
10
+ export function HighlightedTitle({ title, highlight }: { title: string; highlight?: string }) {
11
+ if (!highlight) return <>{title}</>
12
+
13
+ const i = title.indexOf(highlight)
14
+ if (i < 0) {
15
+ return (
16
+ <>
17
+ {title} <em>{highlight}</em>
18
+ </>
19
+ )
20
+ }
21
+ return (
22
+ <>
23
+ {title.slice(0, i)}
24
+ <em>{highlight}</em>
25
+ {title.slice(i + highlight.length)}
26
+ </>
27
+ )
28
+ }
29
+
30
+ async function CampaignCountdown({ endsAt }: { endsAt: string }) {
31
+ await connection()
32
+ return (
33
+ <Countdown
34
+ endsAt={endsAt}
35
+ serverNow={Date.now()}
36
+ labels={{
37
+ label: t("countdown.label"),
38
+ days: t("countdown.days"),
39
+ hours: t("countdown.hours"),
40
+ mins: t("countdown.mins"),
41
+ secs: t("countdown.secs"),
42
+ }}
43
+ />
44
+ )
45
+ }
46
+
47
+ export function Hero({ campaign }: { campaign: Campaign }) {
48
+ const { hero } = campaign
49
+ // Un nom de fichier (seed/images/…) n'est une image qu'une fois importé dans le stockage
50
+ // par le backend (étape 2) : seules les URL absolues sont affichées.
51
+ const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
52
+ const trust: [IconName, string, string][] = [
53
+ ["truck", t("hero.trust.shipping"), t("hero.trust.shippingSub")],
54
+ ["lock", t("hero.trust.secure"), t("hero.trust.secureSub")],
55
+ ["returns", t("hero.trust.returns"), t("hero.trust.returnsSub")],
56
+ ]
57
+
58
+ return (
59
+ <section className="ub-hero">
60
+ <div className="ub-wrap ub-hero__in">
61
+ <div className="ub-hero__text">
62
+ {hero.eyebrow && (
63
+ <p className="ub-eyebrow">
64
+ <span className="ub-eyebrow__dot" />
65
+ <span>{hero.eyebrow}</span>
66
+ </p>
67
+ )}
68
+ <h1 className="ub-hero__title ub-head">
69
+ <HighlightedTitle title={hero.title} highlight={hero.highlight} />
70
+ </h1>
71
+ {hero.sub && <p className="ub-hero__sub">{hero.sub}</p>}
72
+ {/* Compte à rebours : selon le thème (CSS), sauf choix explicite dans l'admin */}
73
+ {campaign.showCountdown !== false && (
74
+ <div className={`ub-hero__cd${campaign.showCountdown ? " ub-cd-on" : ""}`}>
75
+ <CampaignCountdown endsAt={campaign.endsAt} />
76
+ </div>
77
+ )}
78
+ <div className="ub-hero__actions">
79
+ <Button href="/store">{hero.cta}</Button>
80
+ {hero.cta2 && (
81
+ <Button href="/store" variant="ghost">
82
+ {hero.cta2}
83
+ </Button>
84
+ )}
85
+ </div>
86
+ <ul className="ub-hero__trust">
87
+ {trust.map(([icon, title, sub]) => (
88
+ <li key={icon}>
89
+ <Icon name={icon} />
90
+ <span>
91
+ {title}
92
+ <small>{sub}</small>
93
+ </span>
94
+ </li>
95
+ ))}
96
+ </ul>
97
+ </div>
98
+ <div className="ub-hero__media">
99
+ <div className="ub-hero__blob" />
100
+ {image ? (
101
+ // eslint-disable-next-line @next/next/no-img-element
102
+ <img className="ub-hero__photo" src={image} alt={t("hero.photo")} />
103
+ ) : (
104
+ <div className="ub-hero__photo ub-ph" role="img" aria-label={t("hero.photo")} />
105
+ )}
106
+ {hero.bubble.some(Boolean) && (
107
+ <div className="ub-bubble">
108
+ <span>{hero.bubble[0]}</span>
109
+ <b>{hero.bubble[1]}</b>
110
+ <span>{hero.bubble[2]}</span>
111
+ </div>
112
+ )}
113
+ </div>
114
+ </div>
115
+ </section>
116
+ )
117
+ }
118
+
119
+ export function TrustBar({ freeShipping }: { freeShipping?: string }) {
120
+ const items: [IconName, string, string][] = [
121
+ ["truck", t("trust.shipping"), freeShipping ? t("trust.shippingSub", { freeShipping }) : t("trust.shippingSubAll")],
122
+ ["shield", t("trust.secure"), t("trust.secureSub")],
123
+ ["returns", t("trust.returns"), t("trust.returnsSub")],
124
+ ["headset", t("trust.support"), t("trust.supportSub")],
125
+ ]
126
+
127
+ return (
128
+ <section className="ub-section" style={{ paddingBottom: 0 }}>
129
+ <div className="ub-wrap">
130
+ <ul className="ub-trust">
131
+ {items.map(([icon, title, sub]) => (
132
+ <li key={icon}>
133
+ <span className="ub-trust__ic">
134
+ <Icon name={icon} />
135
+ </span>
136
+ <span>
137
+ <b>{title}</b>
138
+ <small>{sub}</small>
139
+ </span>
140
+ </li>
141
+ ))}
142
+ </ul>
143
+ </div>
144
+ </section>
145
+ )
146
+ }
147
+
148
+ export function PromoBanner({ campaign }: { campaign: Campaign }) {
149
+ const { promo } = campaign
150
+ const promoImage = promo.image && /^https?:\/\//.test(promo.image) ? promo.image : undefined
151
+
152
+ return (
153
+ <section className="ub-section" style={{ paddingTop: 0 }}>
154
+ <div className="ub-wrap">
155
+ <div className="ub-promo">
156
+ {promo.seal && <div className="ub-promo__seal">{promo.seal}</div>}
157
+ <div className="ub-promo__text">
158
+ <p className="ub-eyebrow">
159
+ <span>{promo.eyebrow}</span>
160
+ </p>
161
+ <div>
162
+ <h2 className="ub-head">{promo.title}</h2>
163
+ <p>{promo.text}</p>
164
+ </div>
165
+ {campaign.showCountdown !== false && (
166
+ <div className={`ub-promo__cd${campaign.showCountdown ? " ub-cd-on" : ""}`}>
167
+ <CampaignCountdown endsAt={campaign.endsAt} />
168
+ </div>
169
+ )}
170
+ <Button href="/store">{promo.cta}</Button>
171
+ </div>
172
+ {promoImage ? (
173
+ <div className="ub-promo__media" style={{ display: "block", overflow: "hidden" }}>
174
+ {/* eslint-disable-next-line @next/next/no-img-element */}
175
+ <img src={promoImage} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
176
+ </div>
177
+ ) : (
178
+ <div className="ub-promo__media ub-ph" />
179
+ )}
180
+ </div>
181
+ </div>
182
+ </section>
183
+ )
184
+ }
185
+
186
+ export function SectionHead({ title, href }: { title: string; href?: string }) {
187
+ return (
188
+ <div className="ub-sec-head">
189
+ <h2 className="ub-head">{title}</h2>
190
+ {href && (
191
+ <Link className="ub-more" href={href}>
192
+ {t("section.viewAll")} <Icon name="arrowRight" />
193
+ </Link>
194
+ )}
195
+ </div>
196
+ )
197
+ }