@salifbiaye/create-boutique 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/cli/dist/commands.js +98 -7
  2. package/cli/dist/create.js +56 -10
  3. package/cli/dist/generate.js +43 -35
  4. package/cli/dist/guide.js +2 -2
  5. package/cli/dist/index.js +20 -2
  6. package/cli/dist/project.js +42 -3
  7. package/package.json +1 -1
  8. package/templates/backend/src/admin/i18n/json/en.json +12 -2
  9. package/templates/backend/src/admin/i18n/json/fr.json +12 -2
  10. package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +27 -17
  11. package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +49 -6
  12. package/templates/backend/src/api/admin/active-boutique/route.ts +25 -0
  13. package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +7 -1
  14. package/templates/backend/src/api/admin/boutique-campaigns/route.ts +6 -0
  15. package/templates/backend/src/api/middlewares.ts +2 -0
  16. package/templates/backend/src/lib/active.ts +41 -0
  17. package/templates/backend/src/lib/project.ts +26 -1
  18. package/templates/backend/src/scripts/rewrite-file-urls.ts +56 -0
  19. package/templates/backend/src/scripts/seed.ts +8 -0
  20. package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +4 -1
  21. package/templates/storefront/src/lib/boutique/config.ts +71 -23
  22. package/templates/storefront/src/lib/boutique/schema.ts +4 -1
  23. package/templates/storefront/src/lib/config.ts +37 -8
  24. package/templates/storefront/src/lib/data/cart.ts +4 -1
  25. package/templates/storefront/src/lib/data/cookies.ts +14 -2
  26. package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +6 -1
  27. package/templates/storefront/src/proxy.ts +2 -1
  28. package/templates/storefront/src/styles/ub/motion.css +2 -1
  29. package/templates/storefront/src/styles/ub/tech.css +3 -3
@@ -33,6 +33,9 @@ const overridable = {
33
33
  export const projectSchema = z.object({
34
34
  project: z.string().regex(SLUG),
35
35
  templateVersion: z.string().optional(),
36
+ /** single : un seul site (site.port, site.url) qui affiche la boutique active (choisie dans l'admin) */
37
+ mode: z.enum(["multi", "single"]).default("multi"),
38
+ site: z.object({ port: z.number().int().min(1).max(65535), url: z.url().optional() }).optional(),
36
39
  defaults: z
37
40
  .object({
38
41
  ...overridable,
@@ -48,7 +51,7 @@ export const projectSchema = z.object({
48
51
  .object({
49
52
  slug: z.string().regex(SLUG, "slug : minuscules, chiffres et tirets"),
50
53
  theme: z.string().regex(SLUG),
51
- port: z.number().int().min(1).max(65535),
54
+ port: z.number().int().min(1).max(65535).optional(),
52
55
  url: z.url().optional(),
53
56
  storeName: overridable.storeName.optional(),
54
57
  currency: overridable.currency.optional(),
@@ -2,21 +2,45 @@ import "server-only"
2
2
 
3
3
  import { getBoutique } from "@lib/boutique/config"
4
4
  import Medusa, { FetchArgs, FetchInput } from "@medusajs/js-sdk"
5
+ import { cookies } from "next/headers"
5
6
 
6
7
  // URL interne du backend (réseau Docker) : jamais exposée au navigateur.
7
8
  const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || "http://localhost:9000"
8
9
 
9
- let client: Medusa | undefined
10
+ /** Cookie : boutique où le panier a été créé (mode single, cf. setCartId). */
11
+ export const CART_BOUTIQUE_COOKIE = "_ub_cart_boutique"
12
+
13
+ // Requêtes liées au panier : elles utilisent la clé de la boutique du panier, pas celle affichée.
14
+ const CART_PATHS = /^\/?store\/(carts|payment-collections|shipping-options)\b/
10
15
 
11
16
  /**
12
- * Client Medusa de la boutique : la publishable key vient de KEYS_DIR/<slug>,
17
+ * Mode single : un client qui a commencé son panier avant un changement de boutique le termine
18
+ * dans sa boutique d'origine (même canal de vente, mêmes prix et même livraison). Mode multi : sans effet.
19
+ */
20
+ async function cartKey(input: FetchInput): Promise<string | undefined> {
21
+ if (process.env.BOUTIQUE !== "auto" || !CART_PATHS.test(String(input))) return undefined
22
+ try {
23
+ const slug = (await cookies()).get(CART_BOUTIQUE_COOKIE)?.value
24
+ if (!slug || slug === getBoutique().slug) return undefined
25
+ return getBoutique(slug).publishableKey
26
+ } catch {
27
+ // Boutique retirée du projet, ou appel hors requête : clé de la boutique affichée
28
+ return undefined
29
+ }
30
+ }
31
+
32
+ const clients = new Map<string, Medusa>()
33
+
34
+ /**
35
+ * Client Medusa de la boutique affichée : la publishable key vient de KEYS_DIR/<slug>,
13
36
  * lue à l'exécution (pas de variable NEXT_PUBLIC_* figée au build).
14
- * Créé au premier usage pour que `next build` n'ait besoin d'aucune boutique.
37
+ * Un client par boutique (mode single : la boutique active change sans redémarrage).
15
38
  */
16
39
  function getClient(): Medusa {
17
- if (client) return client
18
-
19
40
  const boutique = getBoutique()
41
+ const existing = clients.get(boutique.slug)
42
+ if (existing) return existing
43
+
20
44
  const medusa = new Medusa({
21
45
  baseUrl: MEDUSA_BACKEND_URL,
22
46
  debug: process.env.NODE_ENV === "development",
@@ -26,12 +50,17 @@ function getClient(): Medusa {
26
50
  // Langue fixée par l'instance : les traductions Medusa suivent la langue du projet.
27
51
  const originalFetch = medusa.client.fetch.bind(medusa.client)
28
52
  medusa.client.fetch = async <T>(input: FetchInput, init?: FetchArgs): Promise<T> => {
29
- const headers = { "x-medusa-locale": boutique.locale, ...(init?.headers ?? {}) }
53
+ const key = await cartKey(input)
54
+ const headers = {
55
+ "x-medusa-locale": boutique.locale,
56
+ ...(key ? { "x-publishable-api-key": key } : {}),
57
+ ...(init?.headers ?? {}),
58
+ }
30
59
  return originalFetch(input, { ...init, headers })
31
60
  }
32
61
 
33
- client = medusa
34
- return client
62
+ clients.set(boutique.slug, medusa)
63
+ return medusa
35
64
  }
36
65
 
37
66
  export const sdk = new Proxy({} as Medusa, {
@@ -9,6 +9,7 @@ import {
9
9
  getAuthHeaders,
10
10
  getCacheOptions,
11
11
  getCacheTag,
12
+ cartFromOtherBoutique,
12
13
  getCartId,
13
14
  removeCartId,
14
15
  setCartId,
@@ -54,7 +55,9 @@ export async function retrieveCart(cartId?: string, fields?: string) {
54
55
  export async function getOrSetCart() {
55
56
  const region = await getRegion()
56
57
 
57
- let cart = await retrieveCart(undefined, "id,region_id")
58
+ // Mode single : un nouvel article ajouté après un changement de boutique démarre un panier dans la boutique en ligne
59
+ // (l'ancien panier appartient à l'autre canal de vente et ne peut pas recevoir ses produits).
60
+ let cart = (await cartFromOtherBoutique()) ? null : await retrieveCart(undefined, "id,region_id")
58
61
 
59
62
  const headers = {
60
63
  ...(await getAuthHeaders()),
@@ -1,5 +1,6 @@
1
1
  import "server-only"
2
- import { getBoutique } from "@lib/boutique/config"
2
+ import { getBoutique, isSingleSite } from "@lib/boutique/config"
3
+ import { CART_BOUTIQUE_COOKIE } from "@lib/config"
3
4
  import { cookies as nextCookies } from "next/headers"
4
5
 
5
6
  /**
@@ -7,9 +8,10 @@ import { cookies as nextCookies } from "next/headers"
7
8
  * toutes les boutiques du projet les partagent. Chaque nom est donc suffixé
8
9
  * par le slug, pour qu'un panier ou une session d'une boutique n'apparaisse
9
10
  * jamais dans une autre.
11
+ * Mode single : un seul site, noms fixes : le client reste connecté et garde son panier quand la boutique change.
10
12
  */
11
13
  export const cookieName = (base: "jwt" | "cart_id" | "cache_id" | "onboarding") =>
12
- `_medusa_${base}_${getBoutique().slug}`
14
+ `_medusa_${base}_${isSingleSite() ? "site" : getBoutique().slug}`
13
15
 
14
16
  export const getAuthHeaders = async (): Promise<
15
17
  { authorization: string } | {}
@@ -93,6 +95,15 @@ export const setCartId = async (cartId: string) => {
93
95
  ...secureCookie,
94
96
  maxAge: 60 * 60 * 24 * 7,
95
97
  })
98
+ // Boutique du panier (mode single) : le client finit son panier dans cette boutique même si le site change
99
+ cookies.set(CART_BOUTIQUE_COOKIE, getBoutique().slug, { ...secureCookie, maxAge: 60 * 60 * 24 * 7 })
100
+ }
101
+
102
+ /** Mode single : le panier en cours a été commencé dans une autre boutique que celle en ligne maintenant. */
103
+ export const cartFromOtherBoutique = async () => {
104
+ if (!isSingleSite()) return false
105
+ const slug = (await nextCookies()).get(CART_BOUTIQUE_COOKIE)?.value
106
+ return !!slug && slug !== getBoutique().slug
96
107
  }
97
108
 
98
109
  export const removeCartId = async () => {
@@ -100,4 +111,5 @@ export const removeCartId = async () => {
100
111
  cookies.set(cookieName("cart_id"), "", {
101
112
  maxAge: -1,
102
113
  })
114
+ cookies.set(CART_BOUTIQUE_COOKIE, "", { maxAge: -1 })
103
115
  }
@@ -1,3 +1,4 @@
1
+ import { useLocale } from "@lib/boutique/i18n-client"
1
2
  import { HttpTypes } from "@medusajs/types"
2
3
  import { Container } from "@medusajs/ui"
3
4
  import Checkbox from "@modules/common/components/checkbox"
@@ -19,6 +20,10 @@ const ShippingAddress = ({
19
20
  checked: boolean
20
21
  onChange: () => void
21
22
  }) => {
23
+ // Pays de la boutique (fin de la locale, ex. fr-FR → fr) proposé par défaut s'il est livré
24
+ const locale = useLocale()
25
+ const homeCountry = locale.split("-")[1]?.toLowerCase() ?? ""
26
+ const defaultCountry = cart?.region?.countries?.some((c) => c.iso_2 === homeCountry) ? homeCountry : ""
22
27
  const [formData, setFormData] = useState<Record<string, any>>({
23
28
  "shipping_address.first_name": cart?.shipping_address?.first_name || "",
24
29
  "shipping_address.last_name": cart?.shipping_address?.last_name || "",
@@ -26,7 +31,7 @@ const ShippingAddress = ({
26
31
  "shipping_address.company": cart?.shipping_address?.company || "",
27
32
  "shipping_address.postal_code": cart?.shipping_address?.postal_code || "",
28
33
  "shipping_address.city": cart?.shipping_address?.city || "",
29
- "shipping_address.country_code": cart?.shipping_address?.country_code || "",
34
+ "shipping_address.country_code": cart?.shipping_address?.country_code || defaultCountry,
30
35
  "shipping_address.province": cart?.shipping_address?.province || "",
31
36
  "shipping_address.phone": cart?.shipping_address?.phone || "",
32
37
  email: cart?.email || "",
@@ -45,7 +45,8 @@ export function proxy(request: NextRequest) {
45
45
  request.headers.set("content-security-policy", csp)
46
46
  request.headers.set("x-nonce", nonce)
47
47
 
48
- const cacheCookie = `_medusa_cache_id_${process.env.BOUTIQUE}`
48
+ // Même nom que lib/data/cookies.ts (mode single : « site », commun à toutes les boutiques)
49
+ const cacheCookie = `_medusa_cache_id_${process.env.BOUTIQUE === "auto" ? "site" : process.env.BOUTIQUE}`
49
50
  const newCacheId = request.cookies.has(cacheCookie) ? null : crypto.randomUUID()
50
51
  if (newCacheId) {
51
52
  // Visible dès cette requête par les server components…
@@ -46,7 +46,8 @@
46
46
  :is(.ub-hero, .ub-hv-hero, .ub-gl-hero, .ub-cn-hero, .ub-al-hero, .ub-tc-hero, .ub-gr-hero) :is(a, button) { animation: ub-hero-in 0.8s 0.3s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
47
47
  @keyframes ub-hero-in { from { opacity: 0; transform: translate3d(0, 18px, 0); } }
48
48
  /* Fond du hero : très lente respiration */
49
- :is(.ub-hv-hero__bg, .ub-tc-hero__cover img, .ub-gr-hero__bg) { animation: ub-drift 22s ease-in-out infinite alternate; }
49
+ /* Seulement les fonds qui couvrent tout le hero (un visuel posé sur une partie, fondu par un dégradé, laisserait voir son bord) */
50
+ :is(.ub-hv-hero__bg, .ub-gr-hero__bg) { animation: ub-drift 22s ease-in-out infinite alternate; }
50
51
  @keyframes ub-drift { to { transform: scale(1.05) translate3d(-1%, -1%, 0); } }
51
52
 
52
53
  /* ---------- Cartes produits : survol ---------- */
@@ -37,10 +37,10 @@
37
37
  }
38
38
 
39
39
  /* ---------- En-tête et pied de page ---------- */
40
- [data-theme="cyber-monday"]:not([data-variant]) .ub-nav { background: rgb(11 7 22 / 0.82); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
40
+ [data-theme="cyber-monday"]:not([data-variant]) .ub-nav { background: #0b0716; border-bottom: 1px solid var(--border); }
41
41
  [data-theme="cyber-monday"]:not([data-variant]) .ub-search { background: var(--surface); border-color: var(--border); color: var(--ink); }
42
42
  [data-theme="cyber-monday"]:not([data-variant]) .ub-nav__cta { background: var(--primary); color: #fff; box-shadow: var(--tc-glow); }
43
- [data-variant="cyber-monday-2"] .ub-nav { background: rgb(255 255 255 / 0.9); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); }
43
+ [data-variant="cyber-monday-2"] .ub-nav { background: #ffffff; border-bottom: 1px solid var(--border); }
44
44
  [data-variant="cyber-monday-2"] .ub-nav__cta { background: var(--ink); color: #fff; }
45
45
  [data-theme="cyber-monday"]:not([data-variant]) .ub-footwrap { --foot-bg: #120b24; --foot-ink: var(--ink); --cta-bg: var(--primary); --cta-ink: #ffffff; }
46
46
  [data-theme="cyber-monday"]:not([data-variant]) .ub-footwrap .ub-footer { border: 1px solid var(--border); }
@@ -93,7 +93,7 @@
93
93
  /* Cyber Monday 1 : le visuel fait partie du fond (comme Black Friday) : posé à droite sur toute la hauteur,
94
94
  fondu dans le noir à gauche, en haut et en bas. Pas de cadre, pas d'ombre. */
95
95
  .ub-tc-hero[data-has-image] .ub-tc-hero__beam { display: none; }
96
- .ub-tc-hero__cover { position: absolute; inset: 0 0 0 34%; z-index: -1; }
96
+ .ub-tc-hero__cover { position: absolute; inset: 0 0 0 34%; z-index: -1; overflow: hidden; }
97
97
  .ub-tc-hero__cover img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 62%; animation: ub-tc-fadein 1.2s ease both; }
98
98
  .ub-tc-hero__cover::after {
99
99
  content: "";