@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.
- package/README.md +1 -0
- package/cli/dist/commands.js +382 -0
- package/cli/dist/create.js +194 -0
- package/cli/dist/docker.js +19 -0
- package/cli/dist/generate.js +322 -0
- package/cli/dist/guide.js +72 -0
- package/cli/dist/index.js +156 -0
- package/cli/dist/paths.js +29 -0
- package/cli/dist/ports.js +54 -0
- package/cli/dist/project.js +119 -0
- package/cli/dist/themes.js +24 -0
- package/docs/fiche-client.en.html +137 -0
- package/docs/fiche-client.html +137 -0
- package/docs/guide-complet.en.html +259 -0
- package/docs/guide-complet.fr.html +259 -0
- package/package.json +51 -0
- package/templates/backend/.dockerignore +9 -0
- package/templates/backend/Dockerfile +30 -0
- package/templates/backend/README.md +78 -0
- package/templates/backend/TEMPLATE_VERSION +1 -0
- package/templates/backend/docker/entrypoint.sh +28 -0
- package/templates/backend/instrumentation.ts +24 -0
- package/templates/backend/medusa-config.ts +114 -0
- package/templates/backend/package-lock.json +16222 -0
- package/templates/backend/package.json +39 -0
- package/templates/backend/src/admin/README.md +33 -0
- package/templates/backend/src/admin/components/image-field.tsx +61 -0
- package/templates/backend/src/admin/i18n/README.md +58 -0
- package/templates/backend/src/admin/i18n/index.ts +8 -0
- package/templates/backend/src/admin/i18n/json/en.json +66 -0
- package/templates/backend/src/admin/i18n/json/fr.json +66 -0
- package/templates/backend/src/admin/lib/client.ts +9 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +286 -0
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +113 -0
- package/templates/backend/src/admin/tsconfig.json +24 -0
- package/templates/backend/src/admin/vite-env.d.ts +1 -0
- package/templates/backend/src/admin/widgets/category-image.tsx +56 -0
- package/templates/backend/src/api/README.md +135 -0
- package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +30 -0
- package/templates/backend/src/api/admin/boutique-campaigns/route.ts +24 -0
- package/templates/backend/src/api/middlewares.ts +28 -0
- package/templates/backend/src/api/rate-limit.ts +30 -0
- package/templates/backend/src/api/route.ts +6 -0
- package/templates/backend/src/api/store/campaign/route.ts +25 -0
- package/templates/backend/src/api/store/order-lookup/route.ts +74 -0
- package/templates/backend/src/emails/en.json +64 -0
- package/templates/backend/src/emails/fr.json +64 -0
- package/templates/backend/src/jobs/README.md +38 -0
- package/templates/backend/src/lib/emails.ts +94 -0
- package/templates/backend/src/lib/notify.ts +148 -0
- package/templates/backend/src/lib/project.ts +146 -0
- package/templates/backend/src/links/README.md +26 -0
- package/templates/backend/src/links/campaign-sales-channel.ts +9 -0
- package/templates/backend/src/modules/README.md +117 -0
- package/templates/backend/src/modules/campaign/index.ts +9 -0
- package/templates/backend/src/modules/campaign/migrations/.snapshot-boutique-campaign.json +272 -0
- package/templates/backend/src/modules/campaign/migrations/Migration20260928141440.ts +16 -0
- package/templates/backend/src/modules/campaign/migrations/Migration20260928145243.ts +13 -0
- package/templates/backend/src/modules/campaign/models/campaign.ts +28 -0
- package/templates/backend/src/modules/campaign/service.ts +6 -0
- package/templates/backend/src/modules/campaign/validators.ts +94 -0
- package/templates/backend/src/modules/resend/index.ts +6 -0
- package/templates/backend/src/modules/resend/service.ts +63 -0
- package/templates/backend/src/scripts/README.md +63 -0
- package/templates/backend/src/scripts/campaign-export.ts +15 -0
- package/templates/backend/src/scripts/campaign-import.ts +38 -0
- package/templates/backend/src/scripts/price-report.ts +34 -0
- package/templates/backend/src/scripts/seed.ts +418 -0
- package/templates/backend/src/scripts/storage.ts +63 -0
- package/templates/backend/src/subscribers/README.md +61 -0
- package/templates/backend/src/subscribers/account-emails.ts +27 -0
- package/templates/backend/src/subscribers/fulfillment-emails.ts +42 -0
- package/templates/backend/src/subscribers/order-emails.ts +12 -0
- package/templates/backend/src/subscribers/return-refund-emails.ts +50 -0
- package/templates/backend/src/workflows/README.md +81 -0
- package/templates/backend/src/workflows/steps/upsert-campaign.ts +53 -0
- package/templates/backend/src/workflows/upsert-campaign.ts +30 -0
- package/templates/backend/tsconfig.json +36 -0
- package/templates/storefront/.dockerignore +7 -0
- package/templates/storefront/.github/ISSUE_TEMPLATE/bug-report.yml +71 -0
- package/templates/storefront/.prettierrc +8 -0
- package/templates/storefront/Dockerfile +37 -0
- package/templates/storefront/LICENSE +21 -0
- package/templates/storefront/README.md +127 -0
- package/templates/storefront/locales/en.json +269 -0
- package/templates/storefront/locales/fr.json +269 -0
- package/templates/storefront/next-env.d.ts +7 -0
- package/templates/storefront/next-sitemap.js +19 -0
- package/templates/storefront/next.config.js +29 -0
- package/templates/storefront/package-lock.json +9031 -0
- package/templates/storefront/package.json +49 -0
- package/templates/storefront/postcss.config.js +6 -0
- package/templates/storefront/public/favicon.ico +0 -0
- package/templates/storefront/scripts/sync-design.mjs +30 -0
- package/templates/storefront/src/app/(checkout)/checkout/page.tsx +31 -0
- package/templates/storefront/src/app/(checkout)/layout.tsx +29 -0
- package/templates/storefront/src/app/(checkout)/not-found.tsx +20 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/addresses/page.tsx +34 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/loading.tsx +9 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/orders/details/[id]/page.tsx +33 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/orders/page.tsx +37 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/page.tsx +22 -0
- package/templates/storefront/src/app/(main)/account/@dashboard/profile/page.tsx +53 -0
- package/templates/storefront/src/app/(main)/account/@login/page.tsx +12 -0
- package/templates/storefront/src/app/(main)/account/layout.tsx +20 -0
- package/templates/storefront/src/app/(main)/account/loading.tsx +9 -0
- package/templates/storefront/src/app/(main)/cart/loading.tsx +5 -0
- package/templates/storefront/src/app/(main)/cart/not-found.tsx +21 -0
- package/templates/storefront/src/app/(main)/cart/page.tsx +21 -0
- package/templates/storefront/src/app/(main)/categories/[...category]/page.tsx +55 -0
- package/templates/storefront/src/app/(main)/collections/[handle]/page.tsx +55 -0
- package/templates/storefront/src/app/(main)/layout.tsx +43 -0
- package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/not-found.tsx +21 -0
- package/templates/storefront/src/app/(main)/order/[id]/confirmed/loading.tsx +5 -0
- package/templates/storefront/src/app/(main)/order/[id]/confirmed/page.tsx +23 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/accept/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/decline/page.tsx +42 -0
- package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/page.tsx +35 -0
- package/templates/storefront/src/app/(main)/order/track/page.tsx +32 -0
- package/templates/storefront/src/app/(main)/page.tsx +68 -0
- package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +86 -0
- package/templates/storefront/src/app/(main)/store/page.tsx +28 -0
- package/templates/storefront/src/app/api/health/route.ts +13 -0
- package/templates/storefront/src/app/layout.tsx +42 -0
- package/templates/storefront/src/app/not-found.tsx +21 -0
- package/templates/storefront/src/app/opengraph-image.jpg +0 -0
- package/templates/storefront/src/app/twitter-image.jpg +0 -0
- package/templates/storefront/src/instrumentation-node.ts +18 -0
- package/templates/storefront/src/instrumentation.ts +9 -0
- package/templates/storefront/src/lib/boutique/campaign.ts +116 -0
- package/templates/storefront/src/lib/boutique/config.ts +152 -0
- package/templates/storefront/src/lib/boutique/fonts.ts +29 -0
- package/templates/storefront/src/lib/boutique/i18n-client.tsx +34 -0
- package/templates/storefront/src/lib/boutique/i18n.ts +43 -0
- package/templates/storefront/src/lib/boutique/schema.ts +131 -0
- package/templates/storefront/src/lib/config.ts +39 -0
- package/templates/storefront/src/lib/constants.tsx +75 -0
- package/templates/storefront/src/lib/context/modal-context.tsx +34 -0
- package/templates/storefront/src/lib/data/cart.ts +434 -0
- package/templates/storefront/src/lib/data/categories.ts +49 -0
- package/templates/storefront/src/lib/data/collections.ts +59 -0
- package/templates/storefront/src/lib/data/cookies.ts +103 -0
- package/templates/storefront/src/lib/data/customer.ts +261 -0
- package/templates/storefront/src/lib/data/fulfillment.ts +68 -0
- package/templates/storefront/src/lib/data/onboarding.ts +9 -0
- package/templates/storefront/src/lib/data/order-lookup.ts +34 -0
- package/templates/storefront/src/lib/data/orders.ts +112 -0
- package/templates/storefront/src/lib/data/payment.ts +35 -0
- package/templates/storefront/src/lib/data/products.ts +127 -0
- package/templates/storefront/src/lib/data/regions.ts +70 -0
- package/templates/storefront/src/lib/data/variants.ts +38 -0
- package/templates/storefront/src/lib/hooks/use-in-view.tsx +29 -0
- package/templates/storefront/src/lib/hooks/use-toggle-state.tsx +46 -0
- package/templates/storefront/src/lib/util/compare-addresses.ts +28 -0
- package/templates/storefront/src/lib/util/get-percentage-diff.ts +6 -0
- package/templates/storefront/src/lib/util/get-product-price.ts +79 -0
- package/templates/storefront/src/lib/util/isEmpty.ts +11 -0
- package/templates/storefront/src/lib/util/medusa-error.ts +22 -0
- package/templates/storefront/src/lib/util/money.ts +35 -0
- package/templates/storefront/src/lib/util/product.ts +5 -0
- package/templates/storefront/src/lib/util/repeat.ts +5 -0
- package/templates/storefront/src/lib/util/sort-products.ts +50 -0
- package/templates/storefront/src/modules/account/components/account-info/index.tsx +140 -0
- package/templates/storefront/src/modules/account/components/account-nav/index.tsx +198 -0
- package/templates/storefront/src/modules/account/components/address-book/index.tsx +28 -0
- package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +169 -0
- package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +241 -0
- package/templates/storefront/src/modules/account/components/login/index.tsx +65 -0
- package/templates/storefront/src/modules/account/components/order-card/index.tsx +88 -0
- package/templates/storefront/src/modules/account/components/order-overview/index.tsx +47 -0
- package/templates/storefront/src/modules/account/components/overview/index.tsx +169 -0
- package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +193 -0
- package/templates/storefront/src/modules/account/components/profile-email/index.tsx +75 -0
- package/templates/storefront/src/modules/account/components/profile-name/index.tsx +79 -0
- package/templates/storefront/src/modules/account/components/profile-password/index.tsx +72 -0
- package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +74 -0
- package/templates/storefront/src/modules/account/components/register/index.tsx +107 -0
- package/templates/storefront/src/modules/account/components/transfer-request-form/index.tsx +83 -0
- package/templates/storefront/src/modules/account/templates/account-layout.tsx +43 -0
- package/templates/storefront/src/modules/account/templates/login-template.tsx +27 -0
- package/templates/storefront/src/modules/cart/components/cart-item-select/index.tsx +75 -0
- package/templates/storefront/src/modules/cart/components/empty-cart-message/index.tsx +25 -0
- package/templates/storefront/src/modules/cart/components/item/index.tsx +144 -0
- package/templates/storefront/src/modules/cart/components/sign-in-prompt/index.tsx +27 -0
- package/templates/storefront/src/modules/cart/templates/index.tsx +51 -0
- package/templates/storefront/src/modules/cart/templates/items.tsx +58 -0
- package/templates/storefront/src/modules/cart/templates/preview.tsx +51 -0
- package/templates/storefront/src/modules/cart/templates/summary.tsx +50 -0
- package/templates/storefront/src/modules/categories/templates/index.tsx +94 -0
- package/templates/storefront/src/modules/checkout/components/address-select/index.tsx +117 -0
- package/templates/storefront/src/modules/checkout/components/addresses/index.tsx +186 -0
- package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +114 -0
- package/templates/storefront/src/modules/checkout/components/country-select/index.tsx +50 -0
- package/templates/storefront/src/modules/checkout/components/discount-code/index.tsx +181 -0
- package/templates/storefront/src/modules/checkout/components/error-message/index.tsx +13 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +261 -0
- package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +195 -0
- package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +130 -0
- package/templates/storefront/src/modules/checkout/components/payment-test/index.tsx +12 -0
- package/templates/storefront/src/modules/checkout/components/payment-wrapper/index.tsx +42 -0
- package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +54 -0
- package/templates/storefront/src/modules/checkout/components/review/index.tsx +54 -0
- package/templates/storefront/src/modules/checkout/components/shipping/index.tsx +417 -0
- package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +219 -0
- package/templates/storefront/src/modules/checkout/components/submit-button/index.tsx +32 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +38 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-summary/index.tsx +31 -0
- package/templates/storefront/src/modules/collections/templates/index.tsx +44 -0
- package/templates/storefront/src/modules/common/components/cart-totals/index.tsx +84 -0
- package/templates/storefront/src/modules/common/components/checkbox/index.tsx +44 -0
- package/templates/storefront/src/modules/common/components/delete-button/index.tsx +42 -0
- package/templates/storefront/src/modules/common/components/divider/index.tsx +9 -0
- package/templates/storefront/src/modules/common/components/filter-radio-group/index.tsx +61 -0
- package/templates/storefront/src/modules/common/components/input/index.tsx +79 -0
- package/templates/storefront/src/modules/common/components/interactive-link/index.tsx +33 -0
- package/templates/storefront/src/modules/common/components/line-item-options/index.tsx +26 -0
- package/templates/storefront/src/modules/common/components/line-item-price/index.tsx +63 -0
- package/templates/storefront/src/modules/common/components/line-item-unit-price/index.tsx +62 -0
- package/templates/storefront/src/modules/common/components/localized-client-link/index.tsx +29 -0
- package/templates/storefront/src/modules/common/components/modal/index.tsx +118 -0
- package/templates/storefront/src/modules/common/components/native-select/index.tsx +76 -0
- package/templates/storefront/src/modules/common/components/radio/index.tsx +27 -0
- package/templates/storefront/src/modules/common/icons/back.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/bancontact.tsx +26 -0
- package/templates/storefront/src/modules/common/icons/chevron-down.tsx +30 -0
- package/templates/storefront/src/modules/common/icons/eye-off.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/eye.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/fast-delivery.tsx +65 -0
- package/templates/storefront/src/modules/common/icons/ideal.tsx +26 -0
- package/templates/storefront/src/modules/common/icons/map-pin.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/package.tsx +44 -0
- package/templates/storefront/src/modules/common/icons/paypal.tsx +30 -0
- package/templates/storefront/src/modules/common/icons/placeholder-image.tsx +44 -0
- package/templates/storefront/src/modules/common/icons/refresh.tsx +51 -0
- package/templates/storefront/src/modules/common/icons/spinner.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/trash.tsx +51 -0
- package/templates/storefront/src/modules/common/icons/user.tsx +37 -0
- package/templates/storefront/src/modules/common/icons/x.tsx +37 -0
- package/templates/storefront/src/modules/layout/components/cart-mismatch-banner/index.tsx +59 -0
- package/templates/storefront/src/modules/order/components/help/index.tsx +26 -0
- package/templates/storefront/src/modules/order/components/item/index.tsx +57 -0
- package/templates/storefront/src/modules/order/components/items/index.tsx +44 -0
- package/templates/storefront/src/modules/order/components/onboarding-cta/index.tsx +28 -0
- package/templates/storefront/src/modules/order/components/order-details/index.tsx +64 -0
- package/templates/storefront/src/modules/order/components/order-summary/index.tsx +61 -0
- package/templates/storefront/src/modules/order/components/payment-details/index.tsx +64 -0
- package/templates/storefront/src/modules/order/components/shipping-details/index.tsx +74 -0
- package/templates/storefront/src/modules/order/components/transfer-actions/index.tsx +83 -0
- package/templates/storefront/src/modules/order/components/transfer-image/index.tsx +275 -0
- package/templates/storefront/src/modules/order/templates/order-completed-template.tsx +53 -0
- package/templates/storefront/src/modules/order/templates/order-details-template.tsx +46 -0
- package/templates/storefront/src/modules/products/components/thumbnail/index.tsx +70 -0
- package/templates/storefront/src/modules/products/templates/index.tsx +118 -0
- package/templates/storefront/src/modules/shipping/components/free-shipping-price-nudge/index.tsx +285 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-button/index.tsx +5 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-card-details/index.tsx +10 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx +35 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-code-form/index.tsx +13 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-line-item/index.tsx +35 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-information/index.tsx +36 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-items/index.tsx +43 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx +15 -0
- package/templates/storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx +15 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
- package/templates/storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
- package/templates/storefront/src/modules/store/components/pagination/index.tsx +114 -0
- package/templates/storefront/src/modules/store/components/refinement-list/index.tsx +41 -0
- package/templates/storefront/src/modules/store/components/refinement-list/sort-products/index.tsx +48 -0
- package/templates/storefront/src/modules/store/templates/index.tsx +42 -0
- package/templates/storefront/src/modules/store/templates/paginated-products.tsx +80 -0
- package/templates/storefront/src/modules/ub/button.tsx +41 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +197 -0
- package/templates/storefront/src/modules/ub/campaign-ended.tsx +28 -0
- package/templates/storefront/src/modules/ub/category-tiles.tsx +41 -0
- package/templates/storefront/src/modules/ub/countdown.tsx +46 -0
- package/templates/storefront/src/modules/ub/footer.tsx +56 -0
- package/templates/storefront/src/modules/ub/icons.tsx +42 -0
- package/templates/storefront/src/modules/ub/nav.tsx +90 -0
- package/templates/storefront/src/modules/ub/pdp/buy-box.tsx +140 -0
- package/templates/storefront/src/modules/ub/pdp/gallery.tsx +40 -0
- package/templates/storefront/src/modules/ub/product-card.tsx +66 -0
- package/templates/storefront/src/modules/ub/top-bar.tsx +20 -0
- package/templates/storefront/src/modules/ub/track-form.tsx +92 -0
- package/templates/storefront/src/proxy.ts +89 -0
- package/templates/storefront/src/styles/globals.css +98 -0
- package/templates/storefront/src/styles/ub/bundle.css +372 -0
- package/templates/storefront/src/styles/ub/fonts.css +17 -0
- package/templates/storefront/src/styles/ub/medusa-ui.css +90 -0
- package/templates/storefront/src/styles/ub/overrides.css +12 -0
- package/templates/storefront/src/styles/ub/tokens.css +9 -0
- package/templates/storefront/src/types/global.ts +24 -0
- package/templates/storefront/src/types/icon.ts +4 -0
- package/templates/storefront/tailwind.config.js +164 -0
- package/templates/storefront/themes/black-friday/campaign.en.json +31 -0
- package/templates/storefront/themes/black-friday/campaign.fr.json +31 -0
- package/templates/storefront/themes/black-friday/theme.json +9 -0
- package/templates/storefront/themes/cyber-monday/campaign.en.json +31 -0
- package/templates/storefront/themes/cyber-monday/campaign.fr.json +31 -0
- package/templates/storefront/themes/cyber-monday/theme.json +9 -0
- package/templates/storefront/themes/halloween/campaign.en.json +31 -0
- package/templates/storefront/themes/halloween/campaign.fr.json +31 -0
- package/templates/storefront/themes/halloween/theme.json +9 -0
- package/templates/storefront/themes/noel/campaign.en.json +31 -0
- package/templates/storefront/themes/noel/campaign.fr.json +31 -0
- package/templates/storefront/themes/noel/theme.json +9 -0
- package/templates/storefront/themes/nouvel-an/campaign.en.json +31 -0
- package/templates/storefront/themes/nouvel-an/campaign.fr.json +31 -0
- package/templates/storefront/themes/nouvel-an/theme.json +9 -0
- package/templates/storefront/themes/thanksgiving/campaign.en.json +31 -0
- package/templates/storefront/themes/thanksgiving/campaign.fr.json +31 -0
- package/templates/storefront/themes/thanksgiving/theme.json +9 -0
- 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,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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|