@salifbiaye/create-boutique 0.7.1 → 0.7.3
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 +83 -3
- package/cli/dist/generate.js +2 -0
- package/cli/dist/index.js +11 -5
- package/docs/guide-complet.fr.html +29 -1
- package/docs/guide-images/personnaliser-style.jpg +0 -0
- package/docs/guide-images/personnaliser-textes.jpg +0 -0
- package/package.json +4 -2
- package/templates/backend/medusa-config.ts +4 -0
- package/templates/backend/src/admin/i18n/json/en.json +64 -0
- package/templates/backend/src/admin/i18n/json/fr.json +64 -0
- package/templates/backend/src/admin/routes/personnaliser/page.tsx +407 -0
- package/templates/backend/src/api/admin/customization/css/restore/route.ts +21 -0
- package/templates/backend/src/api/admin/customization/css/route.ts +37 -0
- package/templates/backend/src/api/admin/customization/texts/route.ts +38 -0
- package/templates/backend/src/api/middlewares.ts +4 -0
- package/templates/backend/src/api/store/customization/route.ts +21 -0
- package/templates/backend/src/lib/customization.ts +43 -0
- package/templates/backend/src/modules/customization/index.ts +9 -0
- package/templates/backend/src/modules/customization/migrations/Migration20261001080000.ts +21 -0
- package/templates/backend/src/modules/customization/models/css-version.ts +16 -0
- package/templates/backend/src/modules/customization/models/site-text.ts +17 -0
- package/templates/backend/src/modules/customization/service.ts +7 -0
- package/templates/backend/src/modules/customization/validators.ts +49 -0
- package/templates/backend/src/workflows/customization.ts +15 -0
- package/templates/backend/src/workflows/steps/save-css-version.ts +20 -0
- package/templates/backend/src/workflows/steps/save-site-texts.ts +56 -0
- package/templates/storefront/Dockerfile +2 -0
- package/templates/storefront/PERSONNALISER.md +15 -4
- package/templates/storefront/src/app/layout.tsx +17 -2
- package/templates/storefront/src/lib/boutique/customization.ts +76 -0
- package/templates/storefront/src/lib/boutique/i18n.ts +4 -2
- package/templates/storefront/src/lib/boutique/schema.ts +3 -0
- package/templates/storefront/src/styles/custom.css +2 -0
- package/templates/storefront/themes/black-friday/preview.jpg +0 -0
- package/templates/storefront/themes/black-friday/theme.json +2 -0
- package/templates/storefront/themes/black-friday-2/preview.jpg +0 -0
- package/templates/storefront/themes/black-friday-2/theme.json +2 -0
- package/templates/storefront/themes/cyber-monday/preview.jpg +0 -0
- package/templates/storefront/themes/cyber-monday/theme.json +2 -0
- package/templates/storefront/themes/cyber-monday-2/preview.jpg +0 -0
- package/templates/storefront/themes/cyber-monday-2/theme.json +2 -0
- package/templates/storefront/themes/halloween/preview.jpg +0 -0
- package/templates/storefront/themes/halloween/theme.json +2 -0
- package/templates/storefront/themes/halloween-2/preview.jpg +0 -0
- package/templates/storefront/themes/halloween-2/theme.json +2 -0
- package/templates/storefront/themes/jeux/preview.jpg +0 -0
- package/templates/storefront/themes/jeux/theme.json +2 -0
- package/templates/storefront/themes/jeux-2/preview.jpg +0 -0
- package/templates/storefront/themes/jeux-2/theme.json +2 -0
- package/templates/storefront/themes/mubarak/preview.jpg +0 -0
- package/templates/storefront/themes/mubarak/theme.json +2 -0
- package/templates/storefront/themes/mubarak-2/preview.jpg +0 -0
- package/templates/storefront/themes/mubarak-2/theme.json +2 -0
- package/templates/storefront/themes/noel/preview.jpg +0 -0
- package/templates/storefront/themes/noel/theme.json +2 -0
- package/templates/storefront/themes/noel-2/preview.jpg +0 -0
- package/templates/storefront/themes/noel-2/theme.json +2 -0
- package/templates/storefront/themes/nouvel-an/preview.jpg +0 -0
- package/templates/storefront/themes/nouvel-an/theme.json +2 -0
- package/templates/storefront/themes/nouvel-an-2/preview.jpg +0 -0
- package/templates/storefront/themes/nouvel-an-2/theme.json +2 -0
- package/templates/storefront/themes/sport/preview.jpg +0 -0
- package/templates/storefront/themes/sport/theme.json +2 -0
- package/templates/storefront/themes/sport-2/preview.jpg +0 -0
- package/templates/storefront/themes/sport-2/theme.json +2 -0
- package/templates/storefront/themes/summer/preview.jpg +0 -0
- package/templates/storefront/themes/summer/theme.json +2 -0
- package/templates/storefront/themes/summer-2/preview.jpg +0 -0
- package/templates/storefront/themes/summer-2/theme.json +2 -0
- package/templates/storefront/themes/thanksgiving/preview.jpg +0 -0
- package/templates/storefront/themes/thanksgiving/theme.json +2 -0
- package/templates/storefront/themes/thanksgiving-2/preview.jpg +0 -0
- package/templates/storefront/themes/thanksgiving-2/theme.json +2 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { model } from "@medusajs/framework/utils"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Texte d'interface remplacé (admin → Personnaliser → Textes du site).
|
|
5
|
+
* scope = « all » (toutes les boutiques du projet) ou l'id du sales channel d'une boutique (prioritaire).
|
|
6
|
+
* key = clé de storefront/locales/<langue>.json (ex. « cart.title ») ; texte simple uniquement.
|
|
7
|
+
*/
|
|
8
|
+
const SiteText = model
|
|
9
|
+
.define("site_text", {
|
|
10
|
+
id: model.id({ prefix: "stxt" }).primaryKey(),
|
|
11
|
+
scope: model.text(),
|
|
12
|
+
key: model.text(),
|
|
13
|
+
value: model.text(),
|
|
14
|
+
})
|
|
15
|
+
.indexes([{ on: ["scope", "key"], unique: true }])
|
|
16
|
+
|
|
17
|
+
export default SiteText
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MedusaService } from "@medusajs/framework/utils"
|
|
2
|
+
import CssVersion from "./models/css-version"
|
|
3
|
+
import SiteText from "./models/site-text"
|
|
4
|
+
|
|
5
|
+
class CustomizationModuleService extends MedusaService({ SiteText, CssVersion }) {}
|
|
6
|
+
|
|
7
|
+
export default CustomizationModuleService
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "@medusajs/framework/zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Personnalisation : textes d'interface (texte simple, pas de HTML) et style CSS (taille bornée, rien qui charge
|
|
5
|
+
* du contenu externe ou sorte de la balise <style>).
|
|
6
|
+
*/
|
|
7
|
+
export const KEY_RE = /^[a-zA-Z0-9_.-]{1,120}$/
|
|
8
|
+
export const SCOPE_RE = /^(all|sc_[A-Za-z0-9]+)$/
|
|
9
|
+
|
|
10
|
+
export const TextsInput = z.object({
|
|
11
|
+
scope: z.string().regex(SCOPE_RE, "boutique invalide"),
|
|
12
|
+
/** clé → nouveau texte ; null = revenir au texte d'origine */
|
|
13
|
+
values: z.record(
|
|
14
|
+
z.string().regex(KEY_RE, "clé invalide"),
|
|
15
|
+
z
|
|
16
|
+
.string()
|
|
17
|
+
.max(1000, "1000 caractères maximum")
|
|
18
|
+
.refine((v) => !/[<>]/.test(v), "Texte simple uniquement (pas de HTML)")
|
|
19
|
+
.nullable()
|
|
20
|
+
),
|
|
21
|
+
})
|
|
22
|
+
export type TextsInput = z.infer<typeof TextsInput>
|
|
23
|
+
|
|
24
|
+
export const MAX_CSS = 200_000
|
|
25
|
+
|
|
26
|
+
/** Interdits : sortie de la balise <style>, @import et url() externes (chargement depuis un autre site), anciens vecteurs de script */
|
|
27
|
+
export function cssProblem(css: string): string | null {
|
|
28
|
+
if (css.length > MAX_CSS) return `CSS trop long (${Math.round(MAX_CSS / 1000)} Ko maximum)`
|
|
29
|
+
if (/<\/?\s*style|<!--|<script/i.test(css)) return "Balises HTML interdites dans le CSS"
|
|
30
|
+
if (/@import/i.test(css)) return "@import interdit : mettez les polices dans public/custom/ ou utilisez celles du thème"
|
|
31
|
+
if (/expression\s*\(|javascript:|behavior\s*:|-moz-binding/i.test(css)) return "Instruction interdite dans le CSS"
|
|
32
|
+
const urls = [...css.matchAll(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi)].map((m) => m[2].trim())
|
|
33
|
+
const bad = urls.find((u) => !(u.startsWith("/") || u.startsWith("data:image/") || u.startsWith("#")))
|
|
34
|
+
if (bad) return `url(${bad}) interdite : seulement des fichiers du site (/custom/…) ou des images data:`
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const CssInput = z.object({
|
|
39
|
+
css: z.string().superRefine((v, ctx) => {
|
|
40
|
+
const p = cssProblem(v)
|
|
41
|
+
if (p) ctx.addIssue({ code: "custom", message: p })
|
|
42
|
+
}),
|
|
43
|
+
use_file: z.boolean(),
|
|
44
|
+
note: z.string().trim().max(120).optional(),
|
|
45
|
+
})
|
|
46
|
+
export type CssInput = z.infer<typeof CssInput>
|
|
47
|
+
|
|
48
|
+
export const CssRestoreInput = z.object({ id: z.string().regex(/^scss_[A-Za-z0-9]+$/) })
|
|
49
|
+
export type CssRestoreInput = z.infer<typeof CssRestoreInput>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
|
2
|
+
import { type SaveCssData, saveCssVersionStep } from "./steps/save-css-version"
|
|
3
|
+
import { type SaveSiteTextsData, saveSiteTextsStep } from "./steps/save-site-texts"
|
|
4
|
+
|
|
5
|
+
/** Admin → Personnaliser → Textes du site */
|
|
6
|
+
export const saveSiteTextsWorkflow = createWorkflow("save-site-texts", function (input: SaveSiteTextsData) {
|
|
7
|
+
const result = saveSiteTextsStep(input)
|
|
8
|
+
return new WorkflowResponse(result)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
/** Admin → Personnaliser → Style : enregistrer, restaurer une version, revenir au thème */
|
|
12
|
+
export const saveCssWorkflow = createWorkflow("save-css-version", function (input: SaveCssData) {
|
|
13
|
+
const version = saveCssVersionStep(input)
|
|
14
|
+
return new WorkflowResponse(version)
|
|
15
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
|
2
|
+
import { CUSTOMIZATION_MODULE } from "../../modules/customization"
|
|
3
|
+
import type CustomizationModuleService from "../../modules/customization/service"
|
|
4
|
+
|
|
5
|
+
export type SaveCssData = { css: string; use_file: boolean; note: string | null; author: string | null }
|
|
6
|
+
|
|
7
|
+
/** Nouvelle version du style (la plus récente est appliquée) ; rollback : supprime la version créée. */
|
|
8
|
+
export const saveCssVersionStep = createStep(
|
|
9
|
+
"save-css-version",
|
|
10
|
+
async (input: SaveCssData, { container }) => {
|
|
11
|
+
const service: CustomizationModuleService = container.resolve(CUSTOMIZATION_MODULE)
|
|
12
|
+
const version = await service.createCssVersions(input)
|
|
13
|
+
return new StepResponse(version, version.id)
|
|
14
|
+
},
|
|
15
|
+
async (id, { container }) => {
|
|
16
|
+
if (!id) return
|
|
17
|
+
const service: CustomizationModuleService = container.resolve(CUSTOMIZATION_MODULE)
|
|
18
|
+
await service.deleteCssVersions(id)
|
|
19
|
+
}
|
|
20
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
|
2
|
+
import { CUSTOMIZATION_MODULE } from "../../modules/customization"
|
|
3
|
+
import type CustomizationModuleService from "../../modules/customization/service"
|
|
4
|
+
|
|
5
|
+
export type SaveSiteTextsData = { scope: string; values: Record<string, string | null> }
|
|
6
|
+
type Previous = { scope: string; key: string; value: string | null }[]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Enregistre des textes remplacés pour une portée (projet entier ou une boutique) : valeur = remplacer,
|
|
10
|
+
* null ou vide = revenir au texte d'origine. Rollback : remet les valeurs d'avant.
|
|
11
|
+
*/
|
|
12
|
+
export const saveSiteTextsStep = createStep(
|
|
13
|
+
"save-site-texts",
|
|
14
|
+
async (input: SaveSiteTextsData, { container }) => {
|
|
15
|
+
const service: CustomizationModuleService = container.resolve(CUSTOMIZATION_MODULE)
|
|
16
|
+
const keys = Object.keys(input.values)
|
|
17
|
+
if (!keys.length) return new StepResponse({ changed: 0 }, [] as Previous)
|
|
18
|
+
const existing = await service.listSiteTexts({ scope: input.scope, key: keys })
|
|
19
|
+
const byKey = new Map(existing.map((e) => [e.key, e]))
|
|
20
|
+
const previous: Previous = keys.map((key) => ({ scope: input.scope, key, value: byKey.get(key)?.value ?? null }))
|
|
21
|
+
|
|
22
|
+
let changed = 0
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
const value = input.values[key]?.trim() ? input.values[key] : null
|
|
25
|
+
const row = byKey.get(key)
|
|
26
|
+
if (value === null) {
|
|
27
|
+
if (row) {
|
|
28
|
+
await service.deleteSiteTexts(row.id)
|
|
29
|
+
changed++
|
|
30
|
+
}
|
|
31
|
+
} else if (row) {
|
|
32
|
+
if (row.value !== value) {
|
|
33
|
+
await service.updateSiteTexts({ id: row.id, value })
|
|
34
|
+
changed++
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
await service.createSiteTexts({ scope: input.scope, key, value })
|
|
38
|
+
changed++
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return new StepResponse({ changed }, previous)
|
|
42
|
+
},
|
|
43
|
+
async (previous, { container }) => {
|
|
44
|
+
if (!previous?.length) return
|
|
45
|
+
const service: CustomizationModuleService = container.resolve(CUSTOMIZATION_MODULE)
|
|
46
|
+
const scope = previous[0].scope
|
|
47
|
+
const current = await service.listSiteTexts({ scope, key: previous.map((p) => p.key) })
|
|
48
|
+
const byKey = new Map(current.map((c) => [c.key, c]))
|
|
49
|
+
for (const p of previous) {
|
|
50
|
+
const row = byKey.get(p.key)
|
|
51
|
+
if (p.value === null && row) await service.deleteSiteTexts(row.id)
|
|
52
|
+
else if (p.value !== null && row) await service.updateSiteTexts({ id: row.id, value: p.value })
|
|
53
|
+
else if (p.value !== null) await service.createSiteTexts({ scope, key: p.key, value: p.value })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
)
|
|
@@ -29,6 +29,8 @@ COPY --from=build --chown=app:app /app/.next/static ./.next/static
|
|
|
29
29
|
COPY --from=build --chown=app:app /app/public ./public
|
|
30
30
|
COPY --from=build --chown=app:app /app/themes ./themes
|
|
31
31
|
COPY --from=build --chown=app:app /app/locales ./locales
|
|
32
|
+
# Retouches du projet : lues à l'exécution (désactivables depuis l'admin → Personnaliser)
|
|
33
|
+
COPY --from=build --chown=app:app /app/src/styles/custom.css ./custom/custom.css
|
|
32
34
|
|
|
33
35
|
USER app
|
|
34
36
|
EXPOSE 8000
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
# Personnaliser l'apparence d'un projet
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Deux endroits, qui se cumulent (le second passe après le premier) :
|
|
4
|
+
|
|
5
|
+
1. **Admin → Personnaliser → Style (CSS)** : éditeur dans le navigateur, **sans reconstruire** (visible en ~1 minute),
|
|
6
|
+
avec historique des versions, « Annuler les modifications », « Restaurer » une version, « Revenir au CSS du thème ».
|
|
7
|
+
Idéal pour les retouches du client ou les essais.
|
|
8
|
+
2. **`storefront/src/styles/custom.css`** (+ images et polices dans `storefront/public/custom/`) : retouches dans le code,
|
|
9
|
+
conservées par `create-boutique update` ; après une modification : `docker compose up -d --build`.
|
|
10
|
+
Affiché en lecture seule dans l'admin ; un interrupteur l'applique ou non (« Revenir au CSS du thème » le désactive aussi).
|
|
11
|
+
|
|
12
|
+
Les deux sont injectés à l'exécution après les feuilles du thème : pas besoin de `!important`. Refusé dans l'éditeur de
|
|
13
|
+
l'admin : `@import`, `url()` vers un autre site, balises HTML (sécurité) ; images : `/custom/…` ou `data:image/…`.
|
|
14
|
+
|
|
15
|
+
**Textes de l'interface** (boutons, panier, paiement, compte…) : **admin → Personnaliser → Textes du site**, pour toutes les
|
|
16
|
+
boutiques ou une seule, sans reconstruire. Les textes de campagne (titre du hero, bandeau, dates, visuels) : **admin → Mes boutiques**.
|
|
6
17
|
|
|
7
18
|
Ce fichier-ci (`PERSONNALISER.md`) est remis à jour par `update` : il décrit toujours la version installée.
|
|
8
19
|
|
|
@@ -16,7 +27,7 @@ Ce fichier-ci (`PERSONNALISER.md`) est remis à jour par `update` : il décrit t
|
|
|
16
27
|
| Image de fond, motif, décoration (`::before`, `::after`, `url("/custom/…")`) | Modifier un e-mail, une règle de prix, de livraison |
|
|
17
28
|
| Cibler une seule boutique, un thème, une page, l'ordinateur ou le mobile | |
|
|
18
29
|
|
|
19
|
-
Les **textes**
|
|
30
|
+
Les **textes** se changent dans l'admin, pas ici : textes de campagne dans **Mes boutiques**, textes de l'interface dans **Personnaliser**.
|
|
20
31
|
Un besoin de la colonne de droite : on le fait dans le modèle (dépôt create-boutique), puis `create-boutique update` sur les projets.
|
|
21
32
|
Modifier directement un autre fichier du projet reste possible, mais `update` le remplacera (il le liste et le sauvegarde
|
|
22
33
|
dans `backups/` avant).
|
|
@@ -36,7 +36,9 @@ import "styles/ub/responsive.css"
|
|
|
36
36
|
import "styles/ub/fonts.css"
|
|
37
37
|
import "styles/ub/medusa-ui.css"
|
|
38
38
|
import "styles/ub/overrides.css"
|
|
39
|
-
|
|
39
|
+
// Retouches du projet : styles/custom.css (code) et le style de l'admin (Personnaliser), injectés à l'exécution
|
|
40
|
+
// en dernier dans <head> (voir lib/boutique/customization.ts) : modifiables ou désactivables sans reconstruire.
|
|
41
|
+
import { customCssFile, getCustomization, safeCss, shortHash } from "@lib/boutique/customization"
|
|
40
42
|
|
|
41
43
|
// Tout dépend de la boutique de l'instance (lue à l'exécution) : rien n'est pré-rendu au build.
|
|
42
44
|
export const dynamic = "force-dynamic"
|
|
@@ -70,12 +72,25 @@ export async function generateMetadata(): Promise<Metadata> {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export default function RootLayout(props: { children: React.ReactNode }) {
|
|
75
|
+
export default async function RootLayout(props: { children: React.ReactNode }) {
|
|
74
76
|
const b = getBoutique()
|
|
77
|
+
// Avant le rendu : textes remplacés disponibles pour t() et messages()
|
|
78
|
+
const custom = await getCustomization()
|
|
75
79
|
|
|
76
80
|
return (
|
|
77
81
|
<html lang={b.locale} data-theme={b.theme.layout ?? b.theme.id} data-variant={b.theme.layout ? b.theme.id : undefined} data-event={b.theme.id.replace(/-2$/, "")} data-boutique={b.slug} data-mode="light" className={themeFontClasses(b.theme)}>
|
|
78
82
|
<body className="ub">
|
|
83
|
+
{/* React place ces styles dans <head>, après les feuilles du thème (precedence découverte en dernier) */}
|
|
84
|
+
{custom.useFile && customCssFile() && (
|
|
85
|
+
<style href="ub-custom-file" precedence="ub-custom">
|
|
86
|
+
{safeCss(customCssFile())}
|
|
87
|
+
</style>
|
|
88
|
+
)}
|
|
89
|
+
{custom.css && (
|
|
90
|
+
<style href={`ub-custom-admin-${shortHash(custom.css)}`} precedence="ub-custom">
|
|
91
|
+
{safeCss(custom.css)}
|
|
92
|
+
</style>
|
|
93
|
+
)}
|
|
79
94
|
<I18nProvider messages={messages("")} locale={b.locale}>
|
|
80
95
|
<main className="relative">{props.children}</main>
|
|
81
96
|
<Motion event={b.theme.id.replace(/-2$/, "")} />
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import "server-only"
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { sdk } from "@lib/config"
|
|
6
|
+
import { getBoutique } from "./config"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Personnalisation faite dans l'admin (Personnaliser) : textes d'interface remplacés et style CSS, lus sur
|
|
10
|
+
* GET /store/customization (la boutique est déterminée par sa publishable key). Gardés 60 s en mémoire par boutique :
|
|
11
|
+
* une modification apparaît en une minute, sans reconstruire ni redémarrer.
|
|
12
|
+
* Le backend injoignable ne casse jamais le site : textes d'origine et fichier custom.css seulement.
|
|
13
|
+
*/
|
|
14
|
+
export type Customization = { texts: Record<string, string>; css: string; useFile: boolean }
|
|
15
|
+
|
|
16
|
+
const TTL = 60_000
|
|
17
|
+
const EMPTY: Customization = { texts: {}, css: "", useFile: true }
|
|
18
|
+
const g = globalThis as unknown as { __ubCustom?: Map<string, { at: number; value: Customization; loading?: Promise<Customization> }> }
|
|
19
|
+
const store = (g.__ubCustom ??= new Map())
|
|
20
|
+
|
|
21
|
+
async function load(): Promise<Customization> {
|
|
22
|
+
try {
|
|
23
|
+
const r = await sdk.client.fetch<{ texts?: Record<string, string>; css?: string; use_file?: boolean }>("/store/customization", { method: "GET", cache: "no-store" })
|
|
24
|
+
return { texts: r.texts ?? {}, css: r.css ?? "", useFile: r.use_file !== false }
|
|
25
|
+
} catch {
|
|
26
|
+
return EMPTY
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Personnalisation de la boutique affichée (rechargée au plus toutes les 60 s) */
|
|
31
|
+
export async function getCustomization(): Promise<Customization> {
|
|
32
|
+
const slug = getBoutique().slug
|
|
33
|
+
const entry = store.get(slug)
|
|
34
|
+
if (entry && Date.now() - entry.at < TTL) return entry.value
|
|
35
|
+
if (entry?.loading) return entry.loading
|
|
36
|
+
const loading = load().then((value) => {
|
|
37
|
+
store.set(slug, { at: Date.now(), value })
|
|
38
|
+
return value
|
|
39
|
+
})
|
|
40
|
+
store.set(slug, { at: entry?.at ?? 0, value: entry?.value ?? EMPTY, loading })
|
|
41
|
+
return loading
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Textes remplacés connus pour la boutique affichée (synchrone : utilisé par t()) */
|
|
45
|
+
export function textOverrides(): Record<string, string> {
|
|
46
|
+
return store.get(getBoutique().slug)?.value.texts ?? {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fichier custom.css du projet (storefront/src/styles/custom.css) : copié dans l'image (custom/custom.css),
|
|
51
|
+
* lu à l'exécution pour pouvoir être désactivé depuis l'admin (« Revenir au CSS du thème »).
|
|
52
|
+
*/
|
|
53
|
+
let fileCss: string | null = null
|
|
54
|
+
export function customCssFile(): string {
|
|
55
|
+
if (fileCss !== null) return fileCss
|
|
56
|
+
for (const file of [path.join(/*turbopackIgnore: true*/ process.cwd(), "custom", "custom.css"), path.join(/*turbopackIgnore: true*/ process.cwd(), "src", "styles", "custom.css")]) {
|
|
57
|
+
try {
|
|
58
|
+
fileCss = fs.readFileSync(file, "utf8")
|
|
59
|
+
return fileCss
|
|
60
|
+
} catch {
|
|
61
|
+
// fichier suivant
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
fileCss = ""
|
|
65
|
+
return fileCss
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Texte CSS sûr dans une balise <style> : impossible d'en sortir (le backend refuse déjà ces séquences) */
|
|
69
|
+
export const safeCss = (css: string) => css.replace(/<\/?(style|script)/gi, "").replace(/<!--|-->/g, "")
|
|
70
|
+
|
|
71
|
+
/** Empreinte courte d'un texte (identifiant du <style> : un style modifié remplace l'ancien même en navigation) */
|
|
72
|
+
export function shortHash(text: string): string {
|
|
73
|
+
let h = 5381
|
|
74
|
+
for (let i = 0; i < text.length; i++) h = ((h << 5) + h + text.charCodeAt(i)) | 0
|
|
75
|
+
return (h >>> 0).toString(36)
|
|
76
|
+
}
|
|
@@ -3,6 +3,7 @@ import "server-only"
|
|
|
3
3
|
import fs from "node:fs"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { getBoutique } from "./config"
|
|
6
|
+
import { textOverrides } from "./customization"
|
|
6
7
|
|
|
7
8
|
type Dict = Record<string, string>
|
|
8
9
|
|
|
@@ -24,7 +25,8 @@ function dict(lang: string): Dict {
|
|
|
24
25
|
*/
|
|
25
26
|
export function t(key: string, vars?: Record<string, string | number>): string {
|
|
26
27
|
const { language } = getBoutique()
|
|
27
|
-
|
|
28
|
+
// Texte remplacé dans l'admin (Personnaliser → Textes du site), sinon celui de la langue, sinon l'anglais
|
|
29
|
+
const text = textOverrides()[key] ?? dict(language)[key] ?? dict("en")[key] ?? key
|
|
28
30
|
return vars ? text.replace(/\{(\w+)\}/g, (m, k: string) => (k in vars ? String(vars[k]) : m)) : text
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -32,7 +34,7 @@ export function t(key: string, vars?: Record<string, string | number>): string {
|
|
|
32
34
|
export function messages(prefix: string): Dict {
|
|
33
35
|
const { language } = getBoutique()
|
|
34
36
|
const out: Dict = {}
|
|
35
|
-
for (const d of [dict("en"), dict(language)]) {
|
|
37
|
+
for (const d of [dict("en"), dict(language), textOverrides()]) {
|
|
36
38
|
for (const [k, v] of Object.entries(d)) if (k.startsWith(prefix)) out[k] = v
|
|
37
39
|
}
|
|
38
40
|
return out
|
|
@@ -70,6 +70,9 @@ export type Project = z.infer<typeof projectSchema>
|
|
|
70
70
|
export const themeSchema = z.object({
|
|
71
71
|
id: z.string().regex(SLUG),
|
|
72
72
|
name: z.string().min(1),
|
|
73
|
+
/** Événement (regroupement dans la plateforme) : id et libellé, ex. « halloween » / « Halloween » */
|
|
74
|
+
event: z.string().regex(SLUG).optional(),
|
|
75
|
+
eventLabel: z.string().optional(),
|
|
73
76
|
reference: z.string().optional(),
|
|
74
77
|
/** Variante : reprend la mise en page (règles CSS) de ce thème, avec la palette de styles/ub/variants.css. */
|
|
75
78
|
layout: z.string().regex(SLUG).optional(),
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* - Jamais remplacé par `create-boutique update` (le reste du code, si).
|
|
9
9
|
* - Images ou polices utilisées ici : dans public/custom/ (conservé aussi), puis url("/custom/mon-image.png").
|
|
10
10
|
* - Après modification : docker compose up -d --build
|
|
11
|
+
* - Sans reconstruire : admin → Personnaliser → Style (CSS), appliqué après ce fichier (historique, retour au thème).
|
|
12
|
+
* Ce fichier y est affiché en lecture seule ; « Revenir au CSS du thème » le désactive.
|
|
11
13
|
*
|
|
12
14
|
* CIBLER :
|
|
13
15
|
* tout le projet ...................... directement (ex. .ub-tc-ticker { display: none; })
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|