@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.
- package/cli/dist/commands.js +98 -7
- package/cli/dist/create.js +56 -10
- package/cli/dist/generate.js +43 -35
- package/cli/dist/guide.js +2 -2
- package/cli/dist/index.js +20 -2
- package/cli/dist/project.js +42 -3
- package/package.json +1 -1
- package/templates/backend/src/admin/i18n/json/en.json +12 -2
- package/templates/backend/src/admin/i18n/json/fr.json +12 -2
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +27 -17
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +49 -6
- package/templates/backend/src/api/admin/active-boutique/route.ts +25 -0
- package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +7 -1
- package/templates/backend/src/api/admin/boutique-campaigns/route.ts +6 -0
- package/templates/backend/src/api/middlewares.ts +2 -0
- package/templates/backend/src/lib/active.ts +41 -0
- package/templates/backend/src/lib/project.ts +26 -1
- package/templates/backend/src/scripts/rewrite-file-urls.ts +56 -0
- package/templates/backend/src/scripts/seed.ts +8 -0
- package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +4 -1
- package/templates/storefront/src/lib/boutique/config.ts +71 -23
- package/templates/storefront/src/lib/boutique/schema.ts +4 -1
- package/templates/storefront/src/lib/config.ts +37 -8
- package/templates/storefront/src/lib/data/cart.ts +4 -1
- package/templates/storefront/src/lib/data/cookies.ts +14 -2
- package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +6 -1
- package/templates/storefront/src/proxy.ts +2 -1
- package/templates/storefront/src/styles/ub/motion.css +2 -1
- 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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
34
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: "";
|