@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,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "usine-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Backend Usine à Boutiques (Medusa v2) — un backend partagé par toutes les boutiques du projet",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "medusa build",
|
|
8
|
+
"dev": "medusa develop",
|
|
9
|
+
"start": "medusa start",
|
|
10
|
+
"seed": "medusa exec ./src/scripts/seed.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@aws-sdk/client-s3": "3.1141.0",
|
|
14
|
+
"@medusajs/admin-sdk": "2.21.2",
|
|
15
|
+
"@medusajs/cli": "2.21.2",
|
|
16
|
+
"@medusajs/framework": "2.21.2",
|
|
17
|
+
"@medusajs/medusa": "2.21.2",
|
|
18
|
+
"csv-parse": "7.0.3",
|
|
19
|
+
"resend": "6.30.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@swc/core": "1.16.2",
|
|
23
|
+
"@types/node": "24.19.0",
|
|
24
|
+
"@types/react": "18.3.31",
|
|
25
|
+
"@types/react-dom": "18.3.7",
|
|
26
|
+
"prop-types": "15.8.1",
|
|
27
|
+
"react": "18.3.1",
|
|
28
|
+
"react-dom": "18.3.1",
|
|
29
|
+
"ts-node": "10.9.2",
|
|
30
|
+
"typescript": "5.9.3",
|
|
31
|
+
"vite": "5.4.21"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"overrides": {
|
|
37
|
+
"lodash": "4.18.1"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Admin Customizations
|
|
2
|
+
|
|
3
|
+
You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities.
|
|
4
|
+
|
|
5
|
+
> Learn more about Admin Extensions in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin).
|
|
6
|
+
|
|
7
|
+
## Example: Create a Widget
|
|
8
|
+
|
|
9
|
+
A widget is a React component that can be injected into an existing page in the admin dashboard.
|
|
10
|
+
|
|
11
|
+
For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
|
|
12
|
+
|
|
13
|
+
```tsx title="src/admin/widgets/product-widget.tsx"
|
|
14
|
+
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
|
15
|
+
|
|
16
|
+
// The widget
|
|
17
|
+
const ProductWidget = () => {
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<h2>Product Widget</h2>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The widget's configurations
|
|
26
|
+
export const config = defineWidgetConfig({
|
|
27
|
+
zone: "product.details.after",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default ProductWidget
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This inserts a widget with the text “Product Widget” at the end of a product’s details page.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Trash } from "@medusajs/icons"
|
|
2
|
+
import { Button, Label, toast } from "@medusajs/ui"
|
|
3
|
+
import { useMutation } from "@tanstack/react-query"
|
|
4
|
+
import { useTranslation } from "react-i18next"
|
|
5
|
+
import { sdk } from "../lib/client"
|
|
6
|
+
|
|
7
|
+
const IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]
|
|
8
|
+
const IMAGE_MAX = 5 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Champ image : envoi au stockage du projet (module File de Medusa) puis URL renvoyée.
|
|
12
|
+
* Types jpg/png/webp, 5 Mo max (vérifiés ici ; le backend revérifie l'extension de l'URL).
|
|
13
|
+
*/
|
|
14
|
+
export function ImageField({
|
|
15
|
+
label,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
previewClassName = "h-20 w-32 object-cover",
|
|
19
|
+
}: {
|
|
20
|
+
label: string
|
|
21
|
+
value: string
|
|
22
|
+
onChange: (url: string) => void
|
|
23
|
+
previewClassName?: string
|
|
24
|
+
}) {
|
|
25
|
+
const { t } = useTranslation()
|
|
26
|
+
|
|
27
|
+
const upload = useMutation({
|
|
28
|
+
mutationFn: (file: File) => sdk.admin.upload.create({ files: [file] }),
|
|
29
|
+
onSuccess: ({ files }) => onChange(files[0].url),
|
|
30
|
+
onError: (e: Error) => toast.error(e.message),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const onFile = (file?: File) => {
|
|
34
|
+
if (!file) return
|
|
35
|
+
if (!IMAGE_TYPES.includes(file.type)) return toast.error(t("campaigns.errors.imageType"))
|
|
36
|
+
if (file.size > IMAGE_MAX) return toast.error(t("campaigns.errors.imageSize"))
|
|
37
|
+
upload.mutate(file)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex flex-col gap-y-2">
|
|
42
|
+
<Label size="small">{label}</Label>
|
|
43
|
+
{value && (
|
|
44
|
+
<div className="flex items-center gap-3">
|
|
45
|
+
<img src={value} alt="" className={`rounded-md bg-ui-bg-subtle ${previewClassName}`} />
|
|
46
|
+
<Button size="small" variant="secondary" onClick={() => onChange("")}>
|
|
47
|
+
<Trash />
|
|
48
|
+
{t("campaigns.fields.removeImage")}
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<input
|
|
53
|
+
type="file"
|
|
54
|
+
accept={IMAGE_TYPES.join(",")}
|
|
55
|
+
disabled={upload.isPending}
|
|
56
|
+
onChange={(e) => onFile(e.target.files?.[0])}
|
|
57
|
+
className="txt-compact-small text-ui-fg-subtle"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Admin Customizations Translations
|
|
2
|
+
|
|
3
|
+
The Medusa Admin dashboard supports multiple languages for its interface. Medusa uses [react-i18next](https://react.i18next.com/) to manage translations in the admin dashboard.
|
|
4
|
+
|
|
5
|
+
To add translations, create JSON translation files for each language under the `src/admin/i18n/json` directory. For example, create the `src/admin/i18n/json/en.json` file with the following content:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"brands": {
|
|
10
|
+
"title": "Brands",
|
|
11
|
+
"description": "Manage your product brands"
|
|
12
|
+
},
|
|
13
|
+
"done": "Done"
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then, export the translations in `src/admin/i18n/index.ts`:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import en from "./json/en.json" with { type: "json" }
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
en: {
|
|
24
|
+
translation: en,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Finally, use translations in your admin widgets and routes using the `useTranslation` hook:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
|
33
|
+
import { Button, Container, Heading } from "@medusajs/ui"
|
|
34
|
+
import { useTranslation } from "react-i18next"
|
|
35
|
+
|
|
36
|
+
const ProductWidget = () => {
|
|
37
|
+
const { t } = useTranslation()
|
|
38
|
+
return (
|
|
39
|
+
<Container className="p-0">
|
|
40
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
41
|
+
<Heading level="h2">{t("brands.title")}</Heading>
|
|
42
|
+
<p>{t("brands.description")}</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="flex justify-end px-6 py-4">
|
|
45
|
+
<Button variant="primary">{t("done")}</Button>
|
|
46
|
+
</div>
|
|
47
|
+
</Container>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const config = defineWidgetConfig({
|
|
52
|
+
zone: "product.details.before",
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export default ProductWidget
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Learn more about translating admin extensions in the [Translate Admin Customizations](https://docs.medusajs.com/learn/fundamentals/admin/translations) documentation.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import en from "./json/en.json" with { type: "json" }
|
|
2
|
+
import fr from "./json/fr.json" with { type: "json" }
|
|
3
|
+
|
|
4
|
+
// Libellés de la page « Campagnes » : langue de l'utilisateur admin, anglais par défaut.
|
|
5
|
+
export default {
|
|
6
|
+
en: { translation: en },
|
|
7
|
+
fr: { translation: fr },
|
|
8
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"campaigns": {
|
|
3
|
+
"nav": "Campaigns",
|
|
4
|
+
"title": "Campaigns",
|
|
5
|
+
"description": "Texts, dates and hero image of each of your stores. Changes appear on the store within a minute.",
|
|
6
|
+
"store": "Store",
|
|
7
|
+
"dates": "Dates",
|
|
8
|
+
"status": "Status",
|
|
9
|
+
"edit": "Edit",
|
|
10
|
+
"noCampaign": "No campaign",
|
|
11
|
+
"upcoming": "Upcoming",
|
|
12
|
+
"live": "Live",
|
|
13
|
+
"ended": "Ended",
|
|
14
|
+
"save": "Save",
|
|
15
|
+
"cancel": "Cancel",
|
|
16
|
+
"saved": "Campaign saved",
|
|
17
|
+
"reset": "Reset to theme texts",
|
|
18
|
+
"resetDone": "Theme texts restored",
|
|
19
|
+
"emptyHint": "Leave empty to use the theme text.",
|
|
20
|
+
"sections": {
|
|
21
|
+
"dates": "Dates",
|
|
22
|
+
"hero": "Hero",
|
|
23
|
+
"topbar": "Top bar messages",
|
|
24
|
+
"promo": "Promo banner",
|
|
25
|
+
"vars": "Variables used by theme texts",
|
|
26
|
+
"identity": "Identity"
|
|
27
|
+
},
|
|
28
|
+
"fields": {
|
|
29
|
+
"startsAt": "Starts at",
|
|
30
|
+
"endsAt": "Ends at",
|
|
31
|
+
"nextCampaign": "Next campaign (store shown when this one ends)",
|
|
32
|
+
"none": "None",
|
|
33
|
+
"discount": "Discount (e.g. 30%)",
|
|
34
|
+
"freeShipping": "Free shipping threshold (e.g. $50)",
|
|
35
|
+
"eyebrow": "Eyebrow",
|
|
36
|
+
"title": "Title",
|
|
37
|
+
"highlight": "Highlighted words (colored in the title)",
|
|
38
|
+
"sub": "Subtitle",
|
|
39
|
+
"cta": "Button",
|
|
40
|
+
"cta2": "Second button",
|
|
41
|
+
"bubble": "Bubble (3 short texts)",
|
|
42
|
+
"text": "Text",
|
|
43
|
+
"seal": "Seal",
|
|
44
|
+
"message": "Message {{n}}",
|
|
45
|
+
"heroImage": "Hero image (jpg, png or webp, max 5 MB)",
|
|
46
|
+
"removeImage": "Remove image",
|
|
47
|
+
"logo": "Logo (also used as the browser icon)",
|
|
48
|
+
"countdown": "Countdown",
|
|
49
|
+
"countdownTheme": "As the theme",
|
|
50
|
+
"countdownYes": "Always show",
|
|
51
|
+
"countdownNo": "Never show",
|
|
52
|
+
"promoImage": "Promo banner image"
|
|
53
|
+
},
|
|
54
|
+
"errors": {
|
|
55
|
+
"imageType": "Image must be jpg, png or webp",
|
|
56
|
+
"imageSize": "Image must be 5 MB or less",
|
|
57
|
+
"dates": "The start date must be before the end date"
|
|
58
|
+
},
|
|
59
|
+
"category": {
|
|
60
|
+
"title": "Category image",
|
|
61
|
+
"hint": "Shown on the category tiles of the stores.",
|
|
62
|
+
"save": "Save image",
|
|
63
|
+
"saved": "Image saved"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"campaigns": {
|
|
3
|
+
"nav": "Campagnes",
|
|
4
|
+
"title": "Campagnes",
|
|
5
|
+
"description": "Textes, dates et visuel de chacune de vos boutiques. Les modifications apparaissent sur la boutique en une minute.",
|
|
6
|
+
"store": "Boutique",
|
|
7
|
+
"dates": "Dates",
|
|
8
|
+
"status": "Statut",
|
|
9
|
+
"edit": "Modifier",
|
|
10
|
+
"noCampaign": "Aucune campagne",
|
|
11
|
+
"upcoming": "À venir",
|
|
12
|
+
"live": "En cours",
|
|
13
|
+
"ended": "Terminée",
|
|
14
|
+
"save": "Enregistrer",
|
|
15
|
+
"cancel": "Annuler",
|
|
16
|
+
"saved": "Campagne enregistrée",
|
|
17
|
+
"reset": "Réinitialiser aux textes du thème",
|
|
18
|
+
"resetDone": "Textes du thème rétablis",
|
|
19
|
+
"emptyHint": "Laissez vide pour garder le texte du thème.",
|
|
20
|
+
"sections": {
|
|
21
|
+
"dates": "Dates",
|
|
22
|
+
"hero": "Bannière principale",
|
|
23
|
+
"topbar": "Messages de la barre du haut",
|
|
24
|
+
"promo": "Bandeau promo",
|
|
25
|
+
"vars": "Variables des textes du thème",
|
|
26
|
+
"identity": "Identité"
|
|
27
|
+
},
|
|
28
|
+
"fields": {
|
|
29
|
+
"startsAt": "Début",
|
|
30
|
+
"endsAt": "Fin",
|
|
31
|
+
"nextCampaign": "Campagne suivante (boutique proposée à la fin de celle-ci)",
|
|
32
|
+
"none": "Aucune",
|
|
33
|
+
"discount": "Remise (ex. 30%)",
|
|
34
|
+
"freeShipping": "Seuil de livraison offerte (ex. 50 €)",
|
|
35
|
+
"eyebrow": "Sur-titre",
|
|
36
|
+
"title": "Titre",
|
|
37
|
+
"highlight": "Mots mis en couleur dans le titre",
|
|
38
|
+
"sub": "Sous-titre",
|
|
39
|
+
"cta": "Bouton",
|
|
40
|
+
"cta2": "Second bouton",
|
|
41
|
+
"bubble": "Bulle (3 textes courts)",
|
|
42
|
+
"text": "Texte",
|
|
43
|
+
"seal": "Pastille",
|
|
44
|
+
"message": "Message {{n}}",
|
|
45
|
+
"heroImage": "Visuel principal (jpg, png ou webp, 5 Mo max)",
|
|
46
|
+
"removeImage": "Retirer le visuel",
|
|
47
|
+
"logo": "Logo (sert aussi d'icône de l'onglet)",
|
|
48
|
+
"countdown": "Compte à rebours",
|
|
49
|
+
"countdownTheme": "Comme le thème",
|
|
50
|
+
"countdownYes": "Toujours afficher",
|
|
51
|
+
"countdownNo": "Ne jamais afficher",
|
|
52
|
+
"promoImage": "Image du bandeau promo"
|
|
53
|
+
},
|
|
54
|
+
"errors": {
|
|
55
|
+
"imageType": "L'image doit être en jpg, png ou webp",
|
|
56
|
+
"imageSize": "L'image doit faire 5 Mo maximum",
|
|
57
|
+
"dates": "La date de début doit précéder la date de fin"
|
|
58
|
+
},
|
|
59
|
+
"category": {
|
|
60
|
+
"title": "Image de la catégorie",
|
|
61
|
+
"hint": "Affichée sur les tuiles catégories des boutiques.",
|
|
62
|
+
"save": "Enregistrer l'image",
|
|
63
|
+
"saved": "Image enregistrée"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { ArrowPath } from "@medusajs/icons"
|
|
2
|
+
import { Button, Drawer, Input, Label, Select, Text, Textarea, toast } from "@medusajs/ui"
|
|
3
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
import { useTranslation } from "react-i18next"
|
|
6
|
+
import { ImageField } from "../../components/image-field"
|
|
7
|
+
import { sdk } from "../../lib/client"
|
|
8
|
+
|
|
9
|
+
type Texts = {
|
|
10
|
+
topbar?: string[]
|
|
11
|
+
hero?: Partial<Record<"eyebrow" | "title" | "highlight" | "sub" | "cta" | "cta2", string>> & { bubble?: string[]; image?: string }
|
|
12
|
+
promo?: Partial<Record<"eyebrow" | "title" | "text" | "cta" | "seal" | "image", string>>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Campaign = Texts & {
|
|
16
|
+
startsAt: string
|
|
17
|
+
endsAt: string
|
|
18
|
+
nextCampaign?: string
|
|
19
|
+
vars?: { discount?: string; freeShipping?: string }
|
|
20
|
+
logo?: string
|
|
21
|
+
showCountdown?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Boutique = {
|
|
25
|
+
sales_channel_id: string
|
|
26
|
+
slug: string
|
|
27
|
+
description?: string | null
|
|
28
|
+
campaign: Campaign | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// datetime-local (heure locale) <-> ISO
|
|
32
|
+
const toLocal = (iso?: string) => {
|
|
33
|
+
if (!iso) return ""
|
|
34
|
+
const d = new Date(iso)
|
|
35
|
+
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16)
|
|
36
|
+
}
|
|
37
|
+
const fromLocal = (v: string) => new Date(v).toISOString()
|
|
38
|
+
|
|
39
|
+
// Champ vide = texte du thème : on n'envoie que les champs remplis.
|
|
40
|
+
const clean = <T extends Record<string, unknown>>(o: T): Partial<T> | undefined => {
|
|
41
|
+
const out = Object.fromEntries(
|
|
42
|
+
Object.entries(o).filter(([, v]) => (Array.isArray(v) ? v.some((x) => String(x).trim()) : typeof v === "string" ? v.trim() : v !== undefined))
|
|
43
|
+
) as Partial<T>
|
|
44
|
+
return Object.keys(out).length ? out : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Panneau d'édition de la campagne d'une boutique. */
|
|
48
|
+
export function CampaignDrawer({
|
|
49
|
+
boutique,
|
|
50
|
+
others,
|
|
51
|
+
queryKey,
|
|
52
|
+
onClose,
|
|
53
|
+
}: {
|
|
54
|
+
boutique: Boutique
|
|
55
|
+
others: string[]
|
|
56
|
+
queryKey: string[]
|
|
57
|
+
onClose: () => void
|
|
58
|
+
}) {
|
|
59
|
+
const { t } = useTranslation()
|
|
60
|
+
const queryClient = useQueryClient()
|
|
61
|
+
const c = boutique.campaign
|
|
62
|
+
|
|
63
|
+
const [startsAt, setStartsAt] = useState(toLocal(c?.startsAt))
|
|
64
|
+
const [endsAt, setEndsAt] = useState(toLocal(c?.endsAt))
|
|
65
|
+
const [nextCampaign, setNextCampaign] = useState(c?.nextCampaign ?? "")
|
|
66
|
+
const [vars, setVars] = useState({ discount: c?.vars?.discount ?? "", freeShipping: c?.vars?.freeShipping ?? "" })
|
|
67
|
+
const [topbar, setTopbar] = useState<string[]>([0, 1, 2].map((i) => c?.topbar?.[i] ?? ""))
|
|
68
|
+
const [hero, setHero] = useState({
|
|
69
|
+
eyebrow: c?.hero?.eyebrow ?? "",
|
|
70
|
+
title: c?.hero?.title ?? "",
|
|
71
|
+
highlight: c?.hero?.highlight ?? "",
|
|
72
|
+
sub: c?.hero?.sub ?? "",
|
|
73
|
+
cta: c?.hero?.cta ?? "",
|
|
74
|
+
cta2: c?.hero?.cta2 ?? "",
|
|
75
|
+
})
|
|
76
|
+
const [bubble, setBubble] = useState<string[]>([0, 1, 2].map((i) => c?.hero?.bubble?.[i] ?? ""))
|
|
77
|
+
const [promo, setPromo] = useState({
|
|
78
|
+
eyebrow: c?.promo?.eyebrow ?? "",
|
|
79
|
+
title: c?.promo?.title ?? "",
|
|
80
|
+
text: c?.promo?.text ?? "",
|
|
81
|
+
cta: c?.promo?.cta ?? "",
|
|
82
|
+
seal: c?.promo?.seal ?? "",
|
|
83
|
+
})
|
|
84
|
+
const [heroImage, setHeroImage] = useState(c?.hero?.image ?? "")
|
|
85
|
+
const [promoImage, setPromoImage] = useState(c?.promo?.image ?? "")
|
|
86
|
+
const [logo, setLogo] = useState(c?.logo ?? "")
|
|
87
|
+
const [countdown, setCountdown] = useState(c?.showCountdown == null ? "theme" : c.showCountdown ? "yes" : "no")
|
|
88
|
+
|
|
89
|
+
const save = useMutation({
|
|
90
|
+
mutationFn: (body: Record<string, unknown>) =>
|
|
91
|
+
sdk.client.fetch(`/admin/boutique-campaigns/${boutique.sales_channel_id}`, { method: "POST", body }),
|
|
92
|
+
onSuccess: () => {
|
|
93
|
+
queryClient.invalidateQueries({ queryKey })
|
|
94
|
+
toast.success(t("campaigns.saved"))
|
|
95
|
+
onClose()
|
|
96
|
+
},
|
|
97
|
+
onError: (e: Error) => toast.error(e.message),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Images et options conservées par « Réinitialiser » (seuls les textes reviennent au thème)
|
|
101
|
+
const media = () => ({
|
|
102
|
+
heroImage: heroImage || null,
|
|
103
|
+
promoImage: promoImage || null,
|
|
104
|
+
logo: logo || null,
|
|
105
|
+
showCountdown: countdown === "theme" ? null : countdown === "yes",
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const dates = () => {
|
|
109
|
+
if (!startsAt || !endsAt || new Date(startsAt) >= new Date(endsAt)) {
|
|
110
|
+
toast.error(t("campaigns.errors.dates"))
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
return { startsAt: fromLocal(startsAt), endsAt: fromLocal(endsAt) }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const submit = () => {
|
|
117
|
+
const d = dates()
|
|
118
|
+
if (!d) return
|
|
119
|
+
save.mutate({
|
|
120
|
+
...d,
|
|
121
|
+
nextCampaign: nextCampaign || null,
|
|
122
|
+
vars: clean(vars) ?? null,
|
|
123
|
+
...media(),
|
|
124
|
+
topbar: topbar.some((m) => m.trim()) ? topbar.filter((m) => m.trim()) : undefined,
|
|
125
|
+
hero: clean({ ...hero, bubble: bubble.some((b) => b.trim()) ? bubble : undefined }),
|
|
126
|
+
promo: clean(promo),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Vide tous les textes : la boutique reprend les textes par défaut de son thème (dates et visuel gardés). */
|
|
131
|
+
const reset = () => {
|
|
132
|
+
const d = dates()
|
|
133
|
+
if (!d) return
|
|
134
|
+
save.mutate({ ...d, nextCampaign: nextCampaign || null, vars: clean(vars) ?? null, ...media() }, {
|
|
135
|
+
onSuccess: () => toast.success(t("campaigns.resetDone")),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const field = (label: string, value: string, onChange: (v: string) => void, multiline = false) => (
|
|
140
|
+
<div className="flex flex-col gap-y-1">
|
|
141
|
+
<Label size="small">{label}</Label>
|
|
142
|
+
{multiline ? (
|
|
143
|
+
<Textarea value={value} onChange={(e) => onChange(e.target.value)} />
|
|
144
|
+
) : (
|
|
145
|
+
<Input size="small" value={value} onChange={(e) => onChange(e.target.value)} />
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const section = (title: string, children: React.ReactNode) => (
|
|
151
|
+
<div className="flex flex-col gap-y-3">
|
|
152
|
+
<Text size="small" weight="plus" leading="compact">
|
|
153
|
+
{title}
|
|
154
|
+
</Text>
|
|
155
|
+
{children}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Drawer open onOpenChange={(open) => !open && onClose()}>
|
|
161
|
+
<Drawer.Content>
|
|
162
|
+
<Drawer.Header>
|
|
163
|
+
<Drawer.Title>{boutique.slug}</Drawer.Title>
|
|
164
|
+
<Drawer.Description>{t("campaigns.emptyHint")}</Drawer.Description>
|
|
165
|
+
</Drawer.Header>
|
|
166
|
+
<Drawer.Body className="flex flex-col gap-y-8 overflow-y-auto">
|
|
167
|
+
{section(
|
|
168
|
+
t("campaigns.sections.identity"),
|
|
169
|
+
<>
|
|
170
|
+
<ImageField label={t("campaigns.fields.logo")} value={logo} onChange={setLogo} previewClassName="h-12 w-auto object-contain p-1" />
|
|
171
|
+
<div className="flex flex-col gap-y-1">
|
|
172
|
+
<Label size="small">{t("campaigns.fields.countdown")}</Label>
|
|
173
|
+
<Select value={countdown} onValueChange={setCountdown}>
|
|
174
|
+
<Select.Trigger>
|
|
175
|
+
<Select.Value />
|
|
176
|
+
</Select.Trigger>
|
|
177
|
+
<Select.Content>
|
|
178
|
+
<Select.Item value="theme">{t("campaigns.fields.countdownTheme")}</Select.Item>
|
|
179
|
+
<Select.Item value="yes">{t("campaigns.fields.countdownYes")}</Select.Item>
|
|
180
|
+
<Select.Item value="no">{t("campaigns.fields.countdownNo")}</Select.Item>
|
|
181
|
+
</Select.Content>
|
|
182
|
+
</Select>
|
|
183
|
+
</div>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{section(
|
|
188
|
+
t("campaigns.sections.dates"),
|
|
189
|
+
<>
|
|
190
|
+
<div className="grid grid-cols-2 gap-3">
|
|
191
|
+
<div className="flex flex-col gap-y-1">
|
|
192
|
+
<Label size="small">{t("campaigns.fields.startsAt")}</Label>
|
|
193
|
+
<Input size="small" type="datetime-local" value={startsAt} onChange={(e) => setStartsAt(e.target.value)} />
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex flex-col gap-y-1">
|
|
196
|
+
<Label size="small">{t("campaigns.fields.endsAt")}</Label>
|
|
197
|
+
<Input size="small" type="datetime-local" value={endsAt} onChange={(e) => setEndsAt(e.target.value)} />
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex flex-col gap-y-1">
|
|
201
|
+
<Label size="small">{t("campaigns.fields.nextCampaign")}</Label>
|
|
202
|
+
<Select value={nextCampaign || "none"} onValueChange={(v) => setNextCampaign(v === "none" ? "" : v)}>
|
|
203
|
+
<Select.Trigger>
|
|
204
|
+
<Select.Value />
|
|
205
|
+
</Select.Trigger>
|
|
206
|
+
<Select.Content>
|
|
207
|
+
<Select.Item value="none">{t("campaigns.fields.none")}</Select.Item>
|
|
208
|
+
{others.map((slug) => (
|
|
209
|
+
<Select.Item key={slug} value={slug}>
|
|
210
|
+
{slug}
|
|
211
|
+
</Select.Item>
|
|
212
|
+
))}
|
|
213
|
+
</Select.Content>
|
|
214
|
+
</Select>
|
|
215
|
+
</div>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{section(
|
|
220
|
+
t("campaigns.sections.vars"),
|
|
221
|
+
<div className="grid grid-cols-2 gap-3">
|
|
222
|
+
{field(t("campaigns.fields.discount"), vars.discount, (v) => setVars({ ...vars, discount: v }))}
|
|
223
|
+
{field(t("campaigns.fields.freeShipping"), vars.freeShipping, (v) => setVars({ ...vars, freeShipping: v }))}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{section(
|
|
228
|
+
t("campaigns.sections.hero"),
|
|
229
|
+
<>
|
|
230
|
+
{field(t("campaigns.fields.eyebrow"), hero.eyebrow, (v) => setHero({ ...hero, eyebrow: v }))}
|
|
231
|
+
{field(t("campaigns.fields.title"), hero.title, (v) => setHero({ ...hero, title: v }))}
|
|
232
|
+
{field(t("campaigns.fields.highlight"), hero.highlight, (v) => setHero({ ...hero, highlight: v }))}
|
|
233
|
+
{field(t("campaigns.fields.sub"), hero.sub, (v) => setHero({ ...hero, sub: v }), true)}
|
|
234
|
+
<div className="grid grid-cols-2 gap-3">
|
|
235
|
+
{field(t("campaigns.fields.cta"), hero.cta, (v) => setHero({ ...hero, cta: v }))}
|
|
236
|
+
{field(t("campaigns.fields.cta2"), hero.cta2, (v) => setHero({ ...hero, cta2: v }))}
|
|
237
|
+
</div>
|
|
238
|
+
<div className="flex flex-col gap-y-1">
|
|
239
|
+
<Label size="small">{t("campaigns.fields.bubble")}</Label>
|
|
240
|
+
<div className="grid grid-cols-3 gap-2">
|
|
241
|
+
{bubble.map((b, i) => (
|
|
242
|
+
<Input key={i} size="small" value={b} onChange={(e) => setBubble(bubble.map((x, j) => (j === i ? e.target.value : x)))} />
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<ImageField label={t("campaigns.fields.heroImage")} value={heroImage} onChange={setHeroImage} />
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{section(
|
|
251
|
+
t("campaigns.sections.topbar"),
|
|
252
|
+
<>{topbar.map((m, i) => field(t("campaigns.fields.message", { n: i + 1 }), m, (v) => setTopbar(topbar.map((x, j) => (j === i ? v : x)))))}</>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{section(
|
|
256
|
+
t("campaigns.sections.promo"),
|
|
257
|
+
<>
|
|
258
|
+
{field(t("campaigns.fields.eyebrow"), promo.eyebrow, (v) => setPromo({ ...promo, eyebrow: v }))}
|
|
259
|
+
{field(t("campaigns.fields.title"), promo.title, (v) => setPromo({ ...promo, title: v }))}
|
|
260
|
+
{field(t("campaigns.fields.text"), promo.text, (v) => setPromo({ ...promo, text: v }), true)}
|
|
261
|
+
<div className="grid grid-cols-2 gap-3">
|
|
262
|
+
{field(t("campaigns.fields.cta"), promo.cta, (v) => setPromo({ ...promo, cta: v }))}
|
|
263
|
+
{field(t("campaigns.fields.seal"), promo.seal, (v) => setPromo({ ...promo, seal: v }))}
|
|
264
|
+
</div>
|
|
265
|
+
<ImageField label={t("campaigns.fields.promoImage")} value={promoImage} onChange={setPromoImage} />
|
|
266
|
+
</>
|
|
267
|
+
)}
|
|
268
|
+
</Drawer.Body>
|
|
269
|
+
<Drawer.Footer>
|
|
270
|
+
<Button size="small" variant="secondary" onClick={reset} disabled={save.isPending} className="mr-auto">
|
|
271
|
+
<ArrowPath />
|
|
272
|
+
{t("campaigns.reset")}
|
|
273
|
+
</Button>
|
|
274
|
+
<Drawer.Close asChild>
|
|
275
|
+
<Button size="small" variant="secondary" disabled={save.isPending}>
|
|
276
|
+
{t("campaigns.cancel")}
|
|
277
|
+
</Button>
|
|
278
|
+
</Drawer.Close>
|
|
279
|
+
<Button size="small" onClick={submit} isLoading={save.isPending} disabled={save.isPending}>
|
|
280
|
+
{t("campaigns.save")}
|
|
281
|
+
</Button>
|
|
282
|
+
</Drawer.Footer>
|
|
283
|
+
</Drawer.Content>
|
|
284
|
+
</Drawer>
|
|
285
|
+
)
|
|
286
|
+
}
|