@salifbiaye/create-boutique 0.5.0 → 0.7.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/code.js +110 -0
- package/cli/dist/commands.js +241 -17
- package/cli/dist/create.js +108 -15
- package/cli/dist/generate.js +108 -39
- package/cli/dist/guide.js +12 -4
- package/cli/dist/index.js +38 -7
- package/cli/dist/payments.js +150 -0
- package/cli/dist/project.js +42 -3
- package/docs/fiche-client.en.html +2 -2
- package/docs/fiche-client.html +2 -2
- package/docs/guide-complet.fr.html +681 -256
- package/docs/guide-images/categories.jpg +0 -0
- package/docs/guide-images/clients.jpg +0 -0
- package/docs/guide-images/collections.jpg +0 -0
- package/docs/guide-images/commande-detail.jpg +0 -0
- package/docs/guide-images/commande-expedier.jpg +0 -0
- package/docs/guide-images/commande-menu.jpg +0 -0
- package/docs/guide-images/commande-paiement.jpg +0 -0
- package/docs/guide-images/commandes-liste.jpg +0 -0
- package/docs/guide-images/connexion.jpg +0 -0
- package/docs/guide-images/groupes-clients.jpg +0 -0
- package/docs/guide-images/inventaire.jpg +0 -0
- package/docs/guide-images/listes-prix.jpg +0 -0
- package/docs/guide-images/menu.jpg +0 -0
- package/docs/guide-images/mes-boutiques-fermer.jpg +0 -0
- package/docs/guide-images/mes-boutiques-panneau.jpg +0 -0
- package/docs/guide-images/mes-boutiques.jpg +0 -0
- package/docs/guide-images/parametres-boutique.jpg +0 -0
- package/docs/guide-images/parametres-canaux.jpg +0 -0
- package/docs/guide-images/parametres-emplacements.jpg +0 -0
- package/docs/guide-images/parametres-profil.jpg +0 -0
- package/docs/guide-images/parametres-regions.jpg +0 -0
- package/docs/guide-images/parametres-utilisateurs.jpg +0 -0
- package/docs/guide-images/produit-creer-details.jpg +0 -0
- package/docs/guide-images/produit-detail.jpg +0 -0
- package/docs/guide-images/produit-variantes-menu.jpg +0 -0
- package/docs/guide-images/produit-variantes.jpg +0 -0
- package/docs/guide-images/produits-liste.jpg +0 -0
- package/docs/guide-images/promotion-details.jpg +0 -0
- package/docs/guide-images/promotion-type.jpg +0 -0
- package/docs/guide-images/promotions-liste.jpg +0 -0
- package/docs/guide-images/site-boutique.jpg +0 -0
- package/docs/guide-images/site-fermee.png +0 -0
- package/docs/guide-images/vue-ensemble.jpg +0 -0
- package/package.json +3 -2
- package/templates/backend/medusa-config.ts +31 -25
- package/templates/backend/package-lock.json +21 -0
- package/templates/backend/package.json +2 -0
- package/templates/backend/src/admin/i18n/json/en.json +63 -5
- package/templates/backend/src/admin/i18n/json/fr.json +63 -5
- package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +29 -17
- package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +84 -6
- package/templates/backend/src/admin/routes/reviews/page.tsx +259 -0
- 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 +9 -1
- package/templates/backend/src/api/admin/boutique-online/[salesChannelId]/route.ts +22 -0
- package/templates/backend/src/api/admin/reviews/[id]/route.ts +15 -0
- package/templates/backend/src/api/admin/reviews/route.ts +47 -0
- package/templates/backend/src/api/middlewares.ts +9 -0
- package/templates/backend/src/api/store/campaign/route.ts +5 -1
- package/templates/backend/src/api/store/reviews/latest/route.ts +39 -0
- package/templates/backend/src/api/store/reviews/order/route.ts +36 -0
- package/templates/backend/src/api/store/reviews/route.ts +43 -0
- package/templates/backend/src/api/store/reviews/summary/route.ts +28 -0
- package/templates/backend/src/emails/en.json +3 -1
- package/templates/backend/src/emails/fr.json +3 -1
- package/templates/backend/src/lib/active.ts +41 -0
- package/templates/backend/src/lib/email-config.ts +14 -0
- package/templates/backend/src/lib/emails.ts +9 -0
- package/templates/backend/src/lib/notify.ts +14 -8
- package/templates/backend/src/lib/payment-config.ts +66 -0
- package/templates/backend/src/lib/project.ts +38 -1
- package/templates/backend/src/lib/redirect-payment.ts +264 -0
- package/templates/backend/src/lib/review-token.ts +40 -0
- package/templates/backend/src/lib/reviews.ts +25 -0
- package/templates/backend/src/modules/naboopay/index.ts +6 -0
- package/templates/backend/src/modules/naboopay/service.ts +92 -0
- package/templates/backend/src/modules/paydunya/index.ts +6 -0
- package/templates/backend/src/modules/paydunya/service.ts +89 -0
- package/templates/backend/src/modules/paytech/index.ts +6 -0
- package/templates/backend/src/modules/paytech/service.ts +114 -0
- package/templates/backend/src/modules/product-review/index.ts +8 -0
- package/templates/backend/src/modules/product-review/migrations/Migration20261001090000.ts +18 -0
- package/templates/backend/src/modules/product-review/models/review.ts +25 -0
- package/templates/backend/src/modules/product-review/service.ts +6 -0
- package/templates/backend/src/modules/product-review/validators.ts +27 -0
- package/templates/backend/src/modules/smtp/index.ts +6 -0
- package/templates/backend/src/modules/smtp/service.ts +73 -0
- package/templates/backend/src/scripts/rewrite-file-urls.ts +56 -0
- package/templates/backend/src/scripts/seed.ts +60 -36
- package/templates/backend/src/subscribers/fulfillment-emails.ts +1 -1
- package/templates/backend/src/workflows/reviews.ts +21 -0
- package/templates/backend/src/workflows/steps/create-review.ts +52 -0
- package/templates/backend/src/workflows/steps/update-review.ts +36 -0
- package/templates/storefront/PERSONNALISER.md +134 -0
- package/templates/storefront/locales/en.json +198 -11
- package/templates/storefront/locales/fr.json +198 -11
- package/templates/storefront/public/custom/LISEZMOI.txt +2 -0
- package/templates/storefront/src/app/(checkout)/layout.tsx +1 -1
- package/templates/storefront/src/app/(main)/layout.tsx +8 -1
- package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -7
- package/templates/storefront/src/app/(main)/page.tsx +149 -4
- package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +4 -1
- package/templates/storefront/src/app/(main)/review/page.tsx +60 -0
- package/templates/storefront/src/app/(main)/store/page.tsx +3 -1
- package/templates/storefront/src/app/(main)/template.tsx +17 -2
- package/templates/storefront/src/app/layout.tsx +13 -1
- package/templates/storefront/src/lib/boutique/campaign.ts +5 -1
- package/templates/storefront/src/lib/boutique/config.ts +71 -23
- package/templates/storefront/src/lib/boutique/page-name.ts +25 -0
- package/templates/storefront/src/lib/boutique/schema.ts +7 -2
- package/templates/storefront/src/lib/config.ts +37 -8
- package/templates/storefront/src/lib/constants.tsx +17 -0
- package/templates/storefront/src/lib/data/cart.ts +4 -1
- package/templates/storefront/src/lib/data/cookies.ts +14 -2
- package/templates/storefront/src/lib/data/review-actions.ts +28 -0
- package/templates/storefront/src/lib/data/reviews.ts +64 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +26 -4
- package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +85 -2
- package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +6 -1
- package/templates/storefront/src/modules/products/templates/index.tsx +5 -0
- package/templates/storefront/src/modules/store/components/shop-search.tsx +85 -0
- package/templates/storefront/src/modules/store/templates/index.tsx +16 -8
- package/templates/storefront/src/modules/store/templates/paginated-products.tsx +15 -3
- package/templates/storefront/src/modules/ub/album.tsx +2 -0
- package/templates/storefront/src/modules/ub/arena.tsx +182 -0
- package/templates/storefront/src/modules/ub/campaign-blocks.tsx +3 -2
- package/templates/storefront/src/modules/ub/campaign-ended.tsx +27 -13
- package/templates/storefront/src/modules/ub/cinema.tsx +2 -0
- package/templates/storefront/src/modules/ub/cookie-banner.tsx +62 -0
- package/templates/storefront/src/modules/ub/gala.tsx +2 -0
- package/templates/storefront/src/modules/ub/grimoire.tsx +35 -8
- package/templates/storefront/src/modules/ub/harvest.tsx +2 -1
- package/templates/storefront/src/modules/ub/hero-image.tsx +14 -0
- package/templates/storefront/src/modules/ub/home-kit.ts +45 -0
- package/templates/storefront/src/modules/ub/lantern.tsx +244 -0
- package/templates/storefront/src/modules/ub/locker.tsx +180 -0
- package/templates/storefront/src/modules/ub/mosaic.tsx +189 -0
- package/templates/storefront/src/modules/ub/nav.tsx +8 -5
- package/templates/storefront/src/modules/ub/review-form.tsx +62 -0
- package/templates/storefront/src/modules/ub/reviews.tsx +131 -0
- package/templates/storefront/src/modules/ub/riviera.tsx +175 -0
- package/templates/storefront/src/modules/ub/souk.tsx +214 -0
- package/templates/storefront/src/modules/ub/sunset-checklist.tsx +95 -0
- package/templates/storefront/src/modules/ub/sunset.tsx +164 -0
- package/templates/storefront/src/modules/ub/tech.tsx +2 -2
- package/templates/storefront/src/modules/ub/training.tsx +196 -0
- package/templates/storefront/src/proxy.ts +4 -1
- package/templates/storefront/src/styles/custom.css +26 -0
- package/templates/storefront/src/styles/ub/album.css +1 -1
- package/templates/storefront/src/styles/ub/arena.css +120 -0
- package/templates/storefront/src/styles/ub/cinema.css +1 -1
- package/templates/storefront/src/styles/ub/gala.css +1 -1
- package/templates/storefront/src/styles/ub/grimoire.css +23 -7
- package/templates/storefront/src/styles/ub/harvest.css +7 -6
- package/templates/storefront/src/styles/ub/lantern.css +157 -0
- package/templates/storefront/src/styles/ub/locker.css +115 -0
- package/templates/storefront/src/styles/ub/media.css +10 -0
- package/templates/storefront/src/styles/ub/mosaic.css +118 -0
- package/templates/storefront/src/styles/ub/motion.css +0 -2
- package/templates/storefront/src/styles/ub/overrides.css +10 -0
- package/templates/storefront/src/styles/ub/pages.css +176 -0
- package/templates/storefront/src/styles/ub/reviews.css +85 -0
- package/templates/storefront/src/styles/ub/riviera.css +103 -0
- package/templates/storefront/src/styles/ub/souk.css +144 -0
- package/templates/storefront/src/styles/ub/sunset.css +123 -0
- package/templates/storefront/src/styles/ub/tech.css +5 -8
- package/templates/storefront/src/styles/ub/training.css +120 -0
- package/templates/storefront/themes/jeux/campaign.en.json +31 -0
- package/templates/storefront/themes/jeux/campaign.fr.json +31 -0
- package/templates/storefront/themes/jeux/theme.json +11 -0
- package/templates/storefront/themes/jeux-2/campaign.en.json +31 -0
- package/templates/storefront/themes/jeux-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/jeux-2/theme.json +11 -0
- package/templates/storefront/themes/mubarak/campaign.en.json +31 -0
- package/templates/storefront/themes/mubarak/campaign.fr.json +31 -0
- package/templates/storefront/themes/mubarak/theme.json +11 -0
- package/templates/storefront/themes/mubarak-2/campaign.en.json +31 -0
- package/templates/storefront/themes/mubarak-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/mubarak-2/theme.json +11 -0
- package/templates/storefront/themes/sport/campaign.en.json +31 -0
- package/templates/storefront/themes/sport/campaign.fr.json +31 -0
- package/templates/storefront/themes/sport/theme.json +11 -0
- package/templates/storefront/themes/sport-2/campaign.en.json +31 -0
- package/templates/storefront/themes/sport-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/sport-2/theme.json +11 -0
- package/templates/storefront/themes/summer/campaign.en.json +31 -0
- package/templates/storefront/themes/summer/campaign.fr.json +31 -0
- package/templates/storefront/themes/summer/theme.json +11 -0
- package/templates/storefront/themes/summer-2/campaign.en.json +31 -0
- package/templates/storefront/themes/summer-2/campaign.fr.json +31 -0
- package/templates/storefront/themes/summer-2/theme.json +11 -0
package/cli/dist/create.js
CHANGED
|
@@ -6,9 +6,11 @@ import { z } from "zod";
|
|
|
6
6
|
import { writeCampaignFiles, writeCompose, writeEnv, writeReadme } from "./generate.js";
|
|
7
7
|
import { writeGuides } from "./guide.js";
|
|
8
8
|
import { copyDir, copyTemplate, templateVersion } from "./paths.js";
|
|
9
|
+
import { syncCustomCss, writeFingerprint } from "./code.js";
|
|
9
10
|
import { nextFreePort, reservedByOthers, reservePorts } from "./ports.js";
|
|
10
|
-
import { validateProject, writeJsonSchema, writeProject, ProjectError } from "./project.js";
|
|
11
|
+
import { boutiqueUrl, normalizeUrl, storePorts, validateProject, writeJsonSchema, writeProject, ProjectError } from "./project.js";
|
|
11
12
|
import { listThemes, themeIds } from "./themes.js";
|
|
13
|
+
import { askMail, askMobile, mobileSummary } from "./payments.js";
|
|
12
14
|
/** Réponses du CLI : mêmes clés pour les options, le fichier --config et les questions. */
|
|
13
15
|
export const answersSchema = z.object({
|
|
14
16
|
name: z.string().min(1).max(60).optional(),
|
|
@@ -18,11 +20,34 @@ export const answersSchema = z.object({
|
|
|
18
20
|
language: z.string().regex(/^[a-z]{2}$/).optional(),
|
|
19
21
|
themes: z.union([z.literal("all"), z.array(z.string())]).optional(),
|
|
20
22
|
boutiques: z.array(z.string().regex(/^[a-z0-9-]+:[a-z0-9-]+$/, "format slug:thème")).optional(),
|
|
23
|
+
/** multi : un site par boutique ; single : un seul site, la boutique active se choisit dans l'admin */
|
|
24
|
+
mode: z.enum(["multi", "single"]).optional(),
|
|
25
|
+
/** Production : domaines publics de l'admin/API et des images (le CLI ajoute /medusa pour les images) */
|
|
26
|
+
apiUrl: z.string().optional(),
|
|
27
|
+
filesUrl: z.string().optional(),
|
|
28
|
+
/** Mode single : domaine du site */
|
|
29
|
+
siteUrl: z.string().optional(),
|
|
21
30
|
stripePk: z.string().optional(),
|
|
22
31
|
stripeSk: z.string().optional(),
|
|
23
32
|
stripeWhsec: z.string().optional(),
|
|
24
33
|
resendKey: z.string().optional(),
|
|
25
34
|
emailFrom: z.email().optional(),
|
|
35
|
+
/** E-mails par SMTP (ex. Gmail + mot de passe d'application) si pas de Resend */
|
|
36
|
+
smtpHost: z.string().optional(),
|
|
37
|
+
smtpPort: z.number().int().optional(),
|
|
38
|
+
smtpUser: z.string().optional(),
|
|
39
|
+
smtpPass: z.string().optional(),
|
|
40
|
+
/** Paiement mobile (Wave, Orange Money…) : agrégateurs et leurs clés (voir cli/src/payments.ts) */
|
|
41
|
+
mobile: z.array(z.enum(["paytech", "paydunya", "naboopay"])).optional(),
|
|
42
|
+
paytechKey: z.string().optional(),
|
|
43
|
+
paytechSecret: z.string().optional(),
|
|
44
|
+
paytechEnv: z.enum(["test", "prod"]).optional(),
|
|
45
|
+
paydunyaMaster: z.string().optional(),
|
|
46
|
+
paydunyaPrivate: z.string().optional(),
|
|
47
|
+
paydunyaToken: z.string().optional(),
|
|
48
|
+
paydunyaEnv: z.enum(["test", "prod"]).optional(),
|
|
49
|
+
naboopayKey: z.string().optional(),
|
|
50
|
+
naboopaySecret: z.string().optional(),
|
|
26
51
|
products: z.string().optional(),
|
|
27
52
|
images: z.string().optional(),
|
|
28
53
|
storePort: z.number().int().optional(),
|
|
@@ -88,12 +113,32 @@ async function ask(a, dirName, yes) {
|
|
|
88
113
|
extra.push(`${slug}:${theme}`);
|
|
89
114
|
}
|
|
90
115
|
}
|
|
116
|
+
const mode = a.mode ??
|
|
117
|
+
cancelled(await p.select({
|
|
118
|
+
message: "Sites",
|
|
119
|
+
options: [
|
|
120
|
+
{ value: "multi", label: "Un site par boutique", hint: "toutes en ligne, chacune sur son port / domaine" },
|
|
121
|
+
{ value: "single", label: "Un seul site", hint: "une boutique en ligne à la fois, choisie dans l'admin → Campagnes" },
|
|
122
|
+
],
|
|
123
|
+
initialValue: "multi",
|
|
124
|
+
}));
|
|
125
|
+
// Adresses publiques (production) : inscrites dans les images importées, donc à donner avant le premier démarrage
|
|
126
|
+
const apiUrl = a.apiUrl ??
|
|
127
|
+
(cancelled(await p.text({ message: "Domaine de l'admin / API en production (vide = test en local)", placeholder: "https://admin.marque.com", defaultValue: "" })).trim() ||
|
|
128
|
+
undefined);
|
|
129
|
+
const filesUrl = apiUrl
|
|
130
|
+
? (a.filesUrl ?? cancelled(await p.text({ message: "Domaine des images (juste le domaine, /medusa est ajouté)", placeholder: "https://images.marque.com", validate: (v) => (v?.trim() ? undefined : "obligatoire avec le domaine de l'admin") })).trim())
|
|
131
|
+
: a.filesUrl;
|
|
91
132
|
const stripePk = a.stripePk ?? (cancelled(await p.text({ message: "Clé Stripe publique (vide = paiement manuel)", placeholder: "pk_test_…", defaultValue: "" })) || undefined);
|
|
92
133
|
const stripeSk = stripePk ? (a.stripeSk ?? (cancelled(await p.password({ message: "Clé Stripe secrète" })) || undefined)) : a.stripeSk;
|
|
93
|
-
//
|
|
134
|
+
// Paiement mobile (Wave, Orange Money…) : critères de chaque agrégateur expliqués avant le choix
|
|
135
|
+
const mobile = await askMobile(a, [currency]);
|
|
136
|
+
// E-mails clients : Resend, SMTP (Gmail…) ou aucun. Déjà donnés en option / --config : pas de question.
|
|
137
|
+
const mail = a.resendKey || a.smtpHost ? { kind: a.resendKey ? "resend" : "smtp" } : await askMail();
|
|
94
138
|
const resendKey = a.resendKey ??
|
|
95
|
-
(cancelled(await p.password({ message: "Clé API Resend (vide = aucun e-mail envoyé)", validate: (v) => (!v || v.startsWith("re_") ? undefined : "une clé Resend commence par re_") })) ||
|
|
139
|
+
(mail.kind !== "resend" ? undefined : cancelled(await p.password({ message: "Clé API Resend (vide = aucun e-mail envoyé)", validate: (v) => (!v || v.startsWith("re_") ? undefined : "une clé Resend commence par re_") })) ||
|
|
96
140
|
undefined);
|
|
141
|
+
const smtp = "smtp" in mail ? mail.smtp : undefined;
|
|
97
142
|
const emailFrom = resendKey
|
|
98
143
|
? (a.emailFrom ??
|
|
99
144
|
cancelled(await p.text({
|
|
@@ -112,7 +157,7 @@ async function ask(a, dirName, yes) {
|
|
|
112
157
|
};
|
|
113
158
|
const products = a.products ?? (cancelled(await p.text({ message: "Fichier produits (CSV, optionnel)", defaultValue: "", validate: exists("file") })).trim() || undefined);
|
|
114
159
|
const images = a.images ?? (cancelled(await p.text({ message: "Dossier images (optionnel)", defaultValue: "", validate: exists("dir") })).trim() || undefined);
|
|
115
|
-
return { ...a, name, email, currency, country, language, themes: chosen, boutiques: extra, stripePk, stripeSk, resendKey, emailFrom, products, images };
|
|
160
|
+
return { ...a, ...mobile, ...smtp, name, email, currency, country, language, mode, apiUrl, filesUrl, themes: chosen, boutiques: extra, stripePk, stripeSk, resendKey, emailFrom, products, images };
|
|
116
161
|
}
|
|
117
162
|
export async function create(dir, input, opts) {
|
|
118
163
|
const target = path.resolve(dir);
|
|
@@ -144,19 +189,39 @@ export async function create(dir, input, opts) {
|
|
|
144
189
|
taken.add(apiPort);
|
|
145
190
|
const storagePort = await nextFreePort(a.storagePort ?? 9100, taken);
|
|
146
191
|
taken.add(storagePort);
|
|
192
|
+
const mode = a.mode ?? "multi";
|
|
147
193
|
let nextStore = a.storePort ?? 8001;
|
|
148
194
|
const boutiques = [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
195
|
+
let site;
|
|
196
|
+
if (mode === "single") {
|
|
197
|
+
// Un seul site : un port pour tout le projet, les boutiques n'en ont pas
|
|
198
|
+
site = { port: await nextFreePort(nextStore, taken) };
|
|
199
|
+
taken.add(site.port);
|
|
200
|
+
for (const w of wanted)
|
|
201
|
+
boutiques.push({ slug: w.slug, theme: w.theme });
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
for (const w of wanted) {
|
|
205
|
+
const port = await nextFreePort(nextStore, taken);
|
|
206
|
+
taken.add(port);
|
|
207
|
+
nextStore = port + 1;
|
|
208
|
+
boutiques.push({ slug: w.slug, theme: w.theme, port });
|
|
209
|
+
}
|
|
154
210
|
}
|
|
211
|
+
// Adresses publiques : les deux ensemble (les images importées gardent l'adresse du stockage)
|
|
212
|
+
if (!!a.apiUrl !== !!a.filesUrl)
|
|
213
|
+
throw new ProjectError("--api-url et --files-url vont ensemble (domaine de l'admin et domaine des images)");
|
|
214
|
+
const urls = a.apiUrl && a.filesUrl ? { api: normalizeUrl(a.apiUrl, "--api-url"), files: normalizeUrl(a.filesUrl, "--files-url") } : undefined;
|
|
215
|
+
if (site && a.siteUrl)
|
|
216
|
+
Object.assign(site, { url: normalizeUrl(a.siteUrl, "--site-url") });
|
|
155
217
|
const proj = validateProject({
|
|
156
218
|
project,
|
|
157
219
|
templateVersion: templateVersion(),
|
|
158
220
|
apiPort,
|
|
159
221
|
storagePort,
|
|
222
|
+
mode,
|
|
223
|
+
...(site ? { site } : {}),
|
|
224
|
+
...(urls ? { urls } : {}),
|
|
160
225
|
defaults: {
|
|
161
226
|
storeName: a.name,
|
|
162
227
|
currency: a.currency,
|
|
@@ -171,11 +236,15 @@ export async function create(dir, input, opts) {
|
|
|
171
236
|
// Récapitulatif
|
|
172
237
|
const width = Math.max(...proj.boutiques.map((b) => b.slug.length)) + 2;
|
|
173
238
|
const lines = [
|
|
174
|
-
...proj.
|
|
175
|
-
|
|
239
|
+
...(proj.mode === "single"
|
|
240
|
+
? [`Site unique → http://localhost:${proj.site.port} (boutique en ligne choisie dans admin → Campagnes ; au départ : ${proj.boutiques[0].slug})`, ...proj.boutiques.map((b) => ` ${b.slug.padEnd(width)}${b.theme}`)]
|
|
241
|
+
: proj.boutiques.map((b) => `${b.slug.padEnd(width)}${b.theme.padEnd(16)}→ http://localhost:${b.port}`)),
|
|
242
|
+
`${"Backend / admin".padEnd(width + 16)}→ ${proj.urls ? `${proj.urls.api}/app` : `http://localhost:${proj.apiPort}/app`}`,
|
|
243
|
+
`Images : ${proj.urls ? `${proj.urls.files}/medusa` : `http://localhost:${proj.storagePort}/medusa (local : à changer avant la production avec create-boutique urls)`}`,
|
|
176
244
|
`Devise ${a.currency.toUpperCase()} · pays ${a.country.toUpperCase()} · langue ${a.language}`,
|
|
177
245
|
`Paiement : ${a.stripeSk ? "Stripe + manuel" : "manuel (Stripe : non configuré)"}`,
|
|
178
|
-
`E-mails : ${a.resendKey ? `Resend, depuis ${a.emailFrom ?? "(adresse manquante)"}` : "
|
|
246
|
+
`E-mails : ${a.resendKey ? `Resend, depuis ${a.emailFrom ?? "(adresse manquante)"}` : a.smtpHost ? `SMTP ${a.smtpHost}${a.smtpUser ? `, depuis ${a.smtpUser}` : ""}${a.smtpPass ? "" : " (mot de passe à mettre dans .env)"}` : "désactivés (Resend ou SMTP à renseigner dans .env)"}`,
|
|
247
|
+
mobileSummary(a),
|
|
179
248
|
`Catalogue : ${a.products ?? "aucun (à ajouter dans l'admin)"}`,
|
|
180
249
|
];
|
|
181
250
|
if (opts.yes)
|
|
@@ -190,6 +259,8 @@ export async function create(dir, input, opts) {
|
|
|
190
259
|
// Écriture du projet
|
|
191
260
|
copyTemplate("storefront", target);
|
|
192
261
|
copyTemplate("backend", target);
|
|
262
|
+
syncCustomCss(target, proj.boutiques.map((b) => ({ slug: b.slug, theme: b.theme, url: boutiqueUrl(proj, b) })));
|
|
263
|
+
writeFingerprint(target, templateVersion()); // empreinte du code généré : update saura dire ce qui a été modifié à la main
|
|
193
264
|
writeProject(target, proj);
|
|
194
265
|
writeJsonSchema(target);
|
|
195
266
|
fs.mkdirSync(path.join(target, "seed", "images"), { recursive: true });
|
|
@@ -205,18 +276,40 @@ export async function create(dir, input, opts) {
|
|
|
205
276
|
stripeWhsec: a.stripeWhsec,
|
|
206
277
|
resendKey: a.resendKey,
|
|
207
278
|
emailFrom: a.emailFrom,
|
|
279
|
+
smtpHost: a.smtpHost,
|
|
280
|
+
smtpPort: a.smtpPort,
|
|
281
|
+
smtpUser: a.smtpUser,
|
|
282
|
+
smtpPass: a.smtpPass,
|
|
283
|
+
paytechKey: a.paytechKey,
|
|
284
|
+
paytechSecret: a.paytechSecret,
|
|
285
|
+
paytechEnv: a.paytechEnv,
|
|
286
|
+
paydunyaMaster: a.paydunyaMaster,
|
|
287
|
+
paydunyaPrivate: a.paydunyaPrivate,
|
|
288
|
+
paydunyaToken: a.paydunyaToken,
|
|
289
|
+
paydunyaEnv: a.paydunyaEnv,
|
|
290
|
+
naboopayKey: a.naboopayKey,
|
|
291
|
+
naboopaySecret: a.naboopaySecret,
|
|
208
292
|
});
|
|
209
293
|
writeCompose(target, proj);
|
|
210
294
|
writeReadme(target, proj);
|
|
211
295
|
fs.writeFileSync(path.join(target, ".gitignore"), ".env\nbackups/\nnode_modules/\n");
|
|
212
|
-
reservePorts(target, [proj.apiPort, proj.storagePort, ...proj
|
|
296
|
+
reservePorts(target, [proj.apiPort, proj.storagePort, ...storePorts(proj)]);
|
|
213
297
|
for (const g of writeGuides(target, proj, a.email)) {
|
|
214
298
|
if (!g.pdf)
|
|
215
299
|
console.log(pc.yellow(`Chrome/Edge introuvable : ${path.basename(g.file)} à imprimer en PDF.`));
|
|
216
300
|
}
|
|
217
301
|
const next = `cd ${path.relative(process.cwd(), target) || "."}\ndocker compose up -d`;
|
|
302
|
+
// Personnaliser : ce qui est conservé par les mises à jour, et ce qui ne l'est pas
|
|
303
|
+
const custom = [
|
|
304
|
+
"Personnaliser l'apparence (conservé par create-boutique update) :",
|
|
305
|
+
' storefront/src/styles/custom.css : une section par boutique ([data-boutique="<slug>"]) + une commune',
|
|
306
|
+
" storefront/PERSONNALISER.md : mode d'emploi (plan du site page par page, classes, exemples)",
|
|
307
|
+
" storefront/public/custom/ : images et polices utilisées par custom.css",
|
|
308
|
+
" puis : docker compose up -d --build",
|
|
309
|
+
"Modifier d'autres fichiers du code reste possible, mais update les remplace (il les liste et les copie dans backups/ avant).",
|
|
310
|
+
].join("\n ");
|
|
218
311
|
if (opts.yes)
|
|
219
|
-
console.log(`Projet créé : ${target}\nEnsuite :\n ${next.replace("\n", "\n ")}\n`);
|
|
312
|
+
console.log(`Projet créé : ${target}\nEnsuite :\n ${next.replace("\n", "\n ")}\n\n ${custom}\n`);
|
|
220
313
|
else
|
|
221
|
-
p.outro(`Projet créé. Ensuite :\n\n ${pc.cyan(next.replace("\n", "\n "))}\n\n Mot de passe admin : ADMIN_PASSWORD dans .env (à transmettre à part)
|
|
314
|
+
p.outro(`Projet créé. Ensuite :\n\n ${pc.cyan(next.replace("\n", "\n "))}\n\n Mot de passe admin : ADMIN_PASSWORD dans .env (à transmettre à part).\n\n ${custom}`);
|
|
222
315
|
}
|
package/cli/dist/generate.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { currencies, effective } from "./project.js";
|
|
4
|
+
import { boutiqueUrl, currencies, effective, fronts } from "./project.js";
|
|
5
5
|
const secret = () => crypto.randomBytes(32).toString("base64url");
|
|
6
6
|
// ─────────────────────────── docker-compose.yml ───────────────────────────
|
|
7
|
-
/** Production (urls présent) : ports
|
|
7
|
+
/** Production (urls présent) : ports seulement sur 127.0.0.1 ; ton reverse proxy (Caddy, Nginx…) sur la machine les expose en https. */
|
|
8
8
|
const bind = (p, port) => (p.urls ? `127.0.0.1:${port}` : String(port));
|
|
9
9
|
const host = (url) => new URL(url).host;
|
|
10
10
|
export function composeYaml(p) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Mode single : un seul service, BOUTIQUE=auto (il affiche la boutique active, choisie dans l'admin)
|
|
12
|
+
const services = fronts(p)
|
|
13
|
+
.map((f) => ` ${f.service}:
|
|
13
14
|
<<: *storefront
|
|
14
|
-
ports: ["${bind(p,
|
|
15
|
-
environment: { <<: *storefront-env, BOUTIQUE: ${
|
|
15
|
+
ports: ["${bind(p, f.port)}:8000"]
|
|
16
|
+
environment: { <<: *storefront-env, BOUTIQUE: ${f.boutique} }
|
|
16
17
|
`)
|
|
17
18
|
.join("\n");
|
|
18
19
|
return `# GÉNÉRÉ par create-boutique à partir de config/project.json — ne pas modifier à la main.
|
|
@@ -51,6 +52,7 @@ ${services}
|
|
|
51
52
|
volumes:
|
|
52
53
|
- ./config:/app/config:ro # boutiques, devises, pays → régions, sales channels, clés
|
|
53
54
|
- ./seed:/app/seed:ro # products.csv + images à importer
|
|
55
|
+
- ./storefront/themes:/app/themes:ro # textes par défaut des thèmes (pré-remplis dans admin → Campagnes)
|
|
54
56
|
- keys:/app/keys # y écrit la publishable key de chaque boutique
|
|
55
57
|
depends_on:
|
|
56
58
|
postgres: { condition: service_healthy }
|
|
@@ -94,53 +96,59 @@ ${services}
|
|
|
94
96
|
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9000/health >/dev/null || exit 1"]
|
|
95
97
|
interval: 5s
|
|
96
98
|
retries: 20
|
|
97
|
-
${p.urls ? caddyService : ""}
|
|
98
99
|
volumes:
|
|
99
100
|
pg_data:
|
|
100
101
|
redis_data:
|
|
101
102
|
storage_data:
|
|
102
|
-
keys
|
|
103
|
+
keys:
|
|
103
104
|
`;
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
restart: unless-stopped
|
|
110
|
-
ports: ["80:80", "443:443"]
|
|
111
|
-
volumes:
|
|
112
|
-
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
113
|
-
- caddy_data:/data
|
|
114
|
-
- caddy_config:/config
|
|
115
|
-
depends_on:
|
|
116
|
-
backend: { condition: service_healthy }
|
|
117
|
-
`;
|
|
118
|
-
/** Caddyfile (production) : chaque domaine vers son container ; certificats https obtenus et renouvelés par Caddy. */
|
|
106
|
+
/**
|
|
107
|
+
* Exemple de Caddyfile (production) : le CLI ne lance aucun reverse proxy, c'est toi qui le gères sur la machine.
|
|
108
|
+
* Ce fichier n'est utilisé par rien : il montre quel domaine envoyer vers quel port (127.0.0.1).
|
|
109
|
+
*/
|
|
119
110
|
export function caddyfile(p) {
|
|
120
|
-
const site = (url,
|
|
111
|
+
const site = (url, port) => `${host(url)} {
|
|
121
112
|
encode zstd gzip
|
|
122
|
-
reverse_proxy
|
|
113
|
+
reverse_proxy 127.0.0.1:${port}
|
|
123
114
|
}
|
|
124
115
|
`;
|
|
125
116
|
return [
|
|
126
|
-
"#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
site(p.urls.
|
|
117
|
+
"# EXEMPLE généré par create-boutique (non utilisé) : à recopier dans ton propre Caddyfile, puis recharger Caddy.",
|
|
118
|
+
"# Régénéré à chaque apply / urls à partir de config/project.json.",
|
|
119
|
+
...fronts(p).filter((f) => f.url).map((f) => site(f.url, f.port)),
|
|
120
|
+
site(p.urls.api, p.apiPort),
|
|
121
|
+
// Images : lecture seule des fichiers du bucket (GET /medusa/…), tout le reste 404 (ni liste, ni écriture, ni console)
|
|
122
|
+
`${host(p.urls.files)} {
|
|
123
|
+
encode zstd gzip
|
|
124
|
+
@read {
|
|
125
|
+
method GET HEAD
|
|
126
|
+
path /medusa/*
|
|
127
|
+
}
|
|
128
|
+
handle @read {
|
|
129
|
+
reverse_proxy 127.0.0.1:${p.storagePort}
|
|
130
|
+
}
|
|
131
|
+
respond 404
|
|
132
|
+
}
|
|
133
|
+
`,
|
|
130
134
|
].join("\n");
|
|
131
135
|
}
|
|
132
|
-
/** Écrit docker-compose.yml (+ Caddyfile en production
|
|
136
|
+
/** Écrit docker-compose.yml (+ Caddyfile.exemple en production). */
|
|
133
137
|
export function writeCompose(dir, p) {
|
|
134
138
|
fs.writeFileSync(path.join(dir, "docker-compose.yml"), composeYaml(p));
|
|
135
|
-
|
|
139
|
+
// Ancien Caddyfile généré par une version précédente (service caddy retiré) : supprimé ; un Caddyfile à toi n'est jamais touché
|
|
140
|
+
const old = path.join(dir, "Caddyfile");
|
|
141
|
+
if (fs.existsSync(old) && fs.readFileSync(old, "utf8").startsWith("# GÉNÉRÉ par create-boutique"))
|
|
142
|
+
fs.rmSync(old);
|
|
143
|
+
const example = path.join(dir, "Caddyfile.exemple");
|
|
136
144
|
if (p.urls)
|
|
137
|
-
fs.writeFileSync(
|
|
138
|
-
else if (fs.existsSync(
|
|
139
|
-
fs.rmSync(
|
|
145
|
+
fs.writeFileSync(example, caddyfile(p));
|
|
146
|
+
else if (fs.existsSync(example))
|
|
147
|
+
fs.rmSync(example);
|
|
140
148
|
}
|
|
141
149
|
/** Variables qui dépendent de project.json (réécrites à chaque apply). Les secrets ne sont jamais régénérés. */
|
|
142
150
|
function derivedEnv(p) {
|
|
143
|
-
const origins = p.
|
|
151
|
+
const origins = fronts(p).map((f) => f.url ?? `http://localhost:${f.port}`);
|
|
144
152
|
const trim = (u) => u.replace(/\/+$/, "");
|
|
145
153
|
const admin = trim(p.urls?.api ?? `http://localhost:${p.apiPort}`);
|
|
146
154
|
const files = trim(p.urls?.files ?? `http://localhost:${p.storagePort}`);
|
|
@@ -177,9 +185,27 @@ export function writeEnv(dir, p, s) {
|
|
|
177
185
|
`STRIPE_WEBHOOK_SECRET=${s.stripeWhsec ?? ""}`,
|
|
178
186
|
`STRIPE_PUBLISHABLE_KEY=${s.stripePk ?? ""}`,
|
|
179
187
|
"",
|
|
180
|
-
"# E-mails
|
|
188
|
+
"# E-mails : Resend (prioritaire) OU un serveur SMTP ; les deux vides = aucun e-mail envoyé",
|
|
181
189
|
`RESEND_API_KEY=${s.resendKey ?? ""}`,
|
|
182
190
|
`EMAIL_FROM=${s.emailFrom ?? ""}`,
|
|
191
|
+
"# SMTP, ex. Gmail : SMTP_HOST=smtp.gmail.com, SMTP_PORT=465, SMTP_USER=adresse gmail, SMTP_PASS=mot de passe d'application",
|
|
192
|
+
`SMTP_HOST=${s.smtpHost ?? ""}`,
|
|
193
|
+
`SMTP_PORT=${s.smtpPort ?? 465}`,
|
|
194
|
+
`SMTP_USER=${s.smtpUser ?? ""}`,
|
|
195
|
+
`SMTP_PASS=${s.smtpPass ?? ""}`,
|
|
196
|
+
"",
|
|
197
|
+
"# Paiement Wave / Orange Money / carte (Afrique de l'Ouest, devise XOF). Vide = désactivé. Après modification :",
|
|
198
|
+
"# docker compose restart backend. Webhooks : voir README (section Paiement mobile).",
|
|
199
|
+
`PAYTECH_API_KEY=${s.paytechKey ?? ""}`,
|
|
200
|
+
`PAYTECH_API_SECRET=${s.paytechSecret ?? ""}`,
|
|
201
|
+
`PAYTECH_ENV=${s.paytechEnv ?? "test"}`,
|
|
202
|
+
`PAYDUNYA_MASTER_KEY=${s.paydunyaMaster ?? ""}`,
|
|
203
|
+
`PAYDUNYA_PRIVATE_KEY=${s.paydunyaPrivate ?? ""}`,
|
|
204
|
+
`PAYDUNYA_TOKEN=${s.paydunyaToken ?? ""}`,
|
|
205
|
+
`PAYDUNYA_ENV=${s.paydunyaEnv ?? "test"}`,
|
|
206
|
+
`NABOOPAY_API_KEY=${s.naboopayKey ?? ""}`,
|
|
207
|
+
`NABOOPAY_WEBHOOK_SECRET=${s.naboopaySecret ?? ""}`,
|
|
208
|
+
"NABOOPAY_METHODS=wave,orange_money",
|
|
183
209
|
"",
|
|
184
210
|
"# Dérivé de config/project.json (réécrit par `create-boutique apply`)",
|
|
185
211
|
...Object.entries(derivedEnv(p)).map(([k, v]) => `${k}=${v}`),
|
|
@@ -261,13 +287,28 @@ export function campaignDates(theme, now = new Date()) {
|
|
|
261
287
|
return [new Date(Date.UTC(y, 11, 1)), day(new Date(Date.UTC(y, 11, 24)), 0, true)];
|
|
262
288
|
case "nouvel-an":
|
|
263
289
|
return [new Date(Date.UTC(y, 11, 26)), day(new Date(Date.UTC(y + 1, 0, 2)), 0, true)];
|
|
290
|
+
// Saison d'été (solstice → fin août) ; Jeux et sport : l'été sportif (mi-juillet → fin août)
|
|
291
|
+
case "summer":
|
|
292
|
+
return [new Date(Date.UTC(y, 5, 21)), day(new Date(Date.UTC(y, 7, 31)), 0, true)];
|
|
293
|
+
case "jeux":
|
|
294
|
+
case "sport":
|
|
295
|
+
return [new Date(Date.UTC(y, 6, 15)), day(new Date(Date.UTC(y, 7, 31)), 0, true)];
|
|
296
|
+
// Ramadan → 2 jours après l'Aïd el-Fitr (Korité). Calendrier lunaire : dates approximatives (± 1 jour selon
|
|
297
|
+
// l'observation de la lune), à ajuster dans l'admin ; pour la Tabaski, le client change simplement les dates.
|
|
298
|
+
case "mubarak": {
|
|
299
|
+
const ramadan = {
|
|
300
|
+
2027: [1, 8, 2, 11], 2028: [0, 28, 2, 1], 2029: [0, 16, 1, 17], 2030: [0, 6, 1, 7],
|
|
301
|
+
};
|
|
302
|
+
const r = ramadan[y];
|
|
303
|
+
return r ? [new Date(Date.UTC(y, r[0], r[1])), day(new Date(Date.UTC(y, r[2], r[3])), 0, true)] : null;
|
|
304
|
+
}
|
|
264
305
|
default:
|
|
265
306
|
return null;
|
|
266
307
|
}
|
|
267
308
|
};
|
|
268
309
|
let range = build(now.getUTCFullYear());
|
|
269
|
-
if (range
|
|
270
|
-
range = build(now.getUTCFullYear() + 1);
|
|
310
|
+
if (!range || range[1] < now)
|
|
311
|
+
range = build(now.getUTCFullYear() + 1) ?? range;
|
|
271
312
|
if (!range)
|
|
272
313
|
range = [day(now), day(now, 30, true)];
|
|
273
314
|
return { startsAt: range[0].toISOString(), endsAt: range[1].toISOString() };
|
|
@@ -309,7 +350,7 @@ export function writeReadme(dir, p) {
|
|
|
309
350
|
const rows = p.boutiques
|
|
310
351
|
.map((b) => {
|
|
311
352
|
const e = effective(p, b);
|
|
312
|
-
return `| ${b.slug} | ${b.theme} | ${e.currency.toUpperCase()} |
|
|
353
|
+
return `| ${b.slug} | ${b.theme} | ${e.currency.toUpperCase()} | ${boutiqueUrl(p, b)} |`;
|
|
313
354
|
})
|
|
314
355
|
.join("\n");
|
|
315
356
|
const text = `# ${p.defaults.storeName} — projet \`${p.project}\`
|
|
@@ -365,13 +406,41 @@ create-boutique backup . # sauvegarde base + images dans backups/<date>/
|
|
|
365
406
|
puis copier le secret \`whsec_…\` dans \`STRIPE_WEBHOOK_SECRET\` et \`docker compose restart backend\`.
|
|
366
407
|
4. Apple Pay : déclarer les domaines des boutiques dans Stripe → Paramètres → Moyens de paiement.
|
|
367
408
|
|
|
409
|
+
## Paiement mobile : Wave, Orange Money, carte (PayTech, PayDunya, NabooPay)
|
|
410
|
+
|
|
411
|
+
Pour les boutiques en **XOF** (FCFA ; PayTech accepte aussi EUR et USD). Le client choisit « Wave, Orange Money… »,
|
|
412
|
+
paie sur la page sécurisée de l'agrégateur, puis revient sur la boutique. Le montant est toujours calculé par le backend.
|
|
413
|
+
|
|
414
|
+
1. Créer un compte chez l'agrégateur choisi et récupérer ses clés (de test d'abord) :
|
|
415
|
+
- **PayTech** (paytech.sn) : \`PAYTECH_API_KEY\`, \`PAYTECH_API_SECRET\` ; \`PAYTECH_ENV=test\` (débite 100 à 150 FCFA) puis \`prod\`.
|
|
416
|
+
- **PayDunya** (paydunya.com) : \`PAYDUNYA_MASTER_KEY\`, \`PAYDUNYA_PRIVATE_KEY\`, \`PAYDUNYA_TOKEN\` ; \`PAYDUNYA_ENV=test\` (sandbox) puis \`prod\`.
|
|
417
|
+
- **NabooPay** (naboopay.com) : \`NABOOPAY_API_KEY\` ; \`NABOOPAY_METHODS=wave,orange_money\`.
|
|
418
|
+
2. Les écrire dans \`.env\` (jamais ailleurs), puis \`docker compose restart backend\` : le moyen apparaît au paiement.
|
|
419
|
+
3. Notifications (le paiement est confirmé même si le client ferme la page) : le backend doit être en ligne en HTTPS.
|
|
420
|
+
- PayTech et PayDunya : rien à faire, l'adresse est envoyée à chaque paiement
|
|
421
|
+
(\`<adresse du backend>/hooks/payment/paytech_paytech\`, \`…/paydunya_paydunya\`).
|
|
422
|
+
- NabooPay : tableau de bord → Paramètres → Intégration, webhook \`<adresse du backend>/hooks/payment/naboopay_naboopay\`,
|
|
423
|
+
puis copier son secret dans \`NABOOPAY_WEBHOOK_SECRET\` et \`docker compose restart backend\`.
|
|
424
|
+
4. Remboursement : PayTech et NabooPay depuis l'admin (remboursement total) ; PayDunya dans son tableau de bord, puis
|
|
425
|
+
« Remboursement » dans l'admin pour l'enregistrer (guide client, section 4.3).
|
|
426
|
+
5. Test PayTech (\`PAYTECH_ENV=test\`) : pas de simulation, chaque paiement débite vraiment 100 à 150 FCFA sur le compte
|
|
427
|
+
du testeur, reçus sur le solde PayTech de la boutique (moins les frais).
|
|
428
|
+
|
|
429
|
+
## E-mails sans Resend : SMTP (ex. Gmail)
|
|
430
|
+
|
|
431
|
+
Si \`RESEND_API_KEY\` est vide, les e-mails partent par le serveur SMTP du \`.env\`. Avec Gmail :
|
|
432
|
+
1. Compte Google → Sécurité → validation en deux étapes activée → **Mots de passe des applications** → en créer un.
|
|
433
|
+
2. \`.env\` : \`SMTP_HOST=smtp.gmail.com\`, \`SMTP_PORT=465\`, \`SMTP_USER=adresse@gmail.com\`, \`SMTP_PASS=<mot de passe d'application>\`,
|
|
434
|
+
\`EMAIL_FROM\` vide (l'adresse Gmail est utilisée), puis \`docker compose restart backend\`.
|
|
435
|
+
3. Limite Gmail : environ 500 e-mails par jour. Au-delà, ou pour un domaine à la marque : Resend, Brevo, Mailjet (SMTP aussi).
|
|
436
|
+
|
|
368
437
|
## Avant de livrer : checklist
|
|
369
438
|
|
|
370
439
|
- [ ] Chaque boutique s'ouvre à son adresse, avec son thème et sa devise.
|
|
371
440
|
- [ ] Chaque produit a un prix dans la devise de chaque boutique où il est vendu.
|
|
372
441
|
- [ ] Logo, textes et dates de chaque campagne vérifiés (Admin → Campagnes).
|
|
373
442
|
- [ ] Stripe configuré (clés + webhook) ou paiement manuel assumé.
|
|
374
|
-
- [ ] E-mails :
|
|
443
|
+
- [ ] E-mails : Resend ou SMTP renseigné, une commande test reçue par le client et le marchand.
|
|
375
444
|
- [ ] Une commande test passée dans chaque boutique (\`create-boutique check .\`).
|
|
376
445
|
- [ ] Sauvegarde programmée (\`create-boutique backup .\`).
|
|
377
446
|
|
package/cli/dist/guide.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { PACKAGE_ROOT } from "./paths.js";
|
|
6
|
-
import { currencies, effective } from "./project.js";
|
|
6
|
+
import { boutiqueUrl, currencies, effective } from "./project.js";
|
|
7
7
|
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
8
8
|
/** Chrome / Edge / Chromium installé (CHROME_PATH prioritaire). */
|
|
9
9
|
function findBrowser() {
|
|
@@ -27,6 +27,14 @@ function render(source, out, dir, vars) {
|
|
|
27
27
|
let html = fs.readFileSync(source, "utf8");
|
|
28
28
|
for (const [k, v] of Object.entries(vars))
|
|
29
29
|
html = html.split(`{{${k}}}`).join(k.endsWith("_HTML") ? v : escapeHtml(v) || " ");
|
|
30
|
+
// Captures d'écran (docs/guide-images) intégrées au fichier : le PDF n'a besoin de rien à côté
|
|
31
|
+
html = html.replace(/src="guide-images\/([\w.-]+)"/g, (m, name) => {
|
|
32
|
+
const file = path.join(path.dirname(source), "guide-images", name);
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
return m;
|
|
35
|
+
const type = name.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
36
|
+
return `src="data:${type};base64,${fs.readFileSync(file).toString("base64")}"`;
|
|
37
|
+
});
|
|
30
38
|
const htmlFile = path.join(dir, `${out}.html`);
|
|
31
39
|
const pdfFile = path.join(dir, `${out}.pdf`);
|
|
32
40
|
fs.writeFileSync(htmlFile, html);
|
|
@@ -48,17 +56,17 @@ function render(source, out, dir, vars) {
|
|
|
48
56
|
*/
|
|
49
57
|
export function writeGuides(dir, p, adminEmail, support = "") {
|
|
50
58
|
const fr = p.defaults.language === "fr";
|
|
51
|
-
const adminUrl = `http://localhost:${p.apiPort}/app`;
|
|
59
|
+
const adminUrl = `${p.urls?.api ?? `http://localhost:${p.apiPort}`}/app`;
|
|
52
60
|
const stores = p.boutiques
|
|
53
61
|
.map((b) => {
|
|
54
62
|
const e = effective(p, b);
|
|
55
|
-
return `<tr><td>${escapeHtml(e.storeName)}</td><td><code>${escapeHtml(b.slug)}</code></td><td>${escapeHtml(b.theme)}</td><td>${e.currency.toUpperCase()}</td><td>${escapeHtml(
|
|
63
|
+
return `<tr><td>${escapeHtml(e.storeName)}</td><td><code>${escapeHtml(b.slug)}</code></td><td>${escapeHtml(b.theme)}</td><td>${e.currency.toUpperCase()}</td><td>${escapeHtml(boutiqueUrl(p, b))}</td></tr>`;
|
|
56
64
|
})
|
|
57
65
|
.join("");
|
|
58
66
|
const vars = {
|
|
59
67
|
ADMIN_URL: adminUrl,
|
|
60
68
|
ADMIN_EMAIL: adminEmail,
|
|
61
|
-
SUPPORT: support,
|
|
69
|
+
SUPPORT: support || "l'équipe qui a créé votre boutique",
|
|
62
70
|
PROJECT: p.project,
|
|
63
71
|
STORE_NAME: p.defaults.storeName,
|
|
64
72
|
CURRENCIES: currencies(p).map((c) => c.toUpperCase()).join(", "),
|
package/cli/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { add, admin, apply, backup, campaignExport, campaignImport, check, guide, remove, set, stripeWebhook, themes, update } from "./commands.js";
|
|
6
|
+
import { add, admin, apply, urls, backup, restore, campaignExport, campaignImport, check, guide, remove, set, stripeWebhook, themes, update } from "./commands.js";
|
|
7
7
|
import { answersSchema, create } from "./create.js";
|
|
8
8
|
import { ProjectError } from "./project.js";
|
|
9
9
|
const HELP = `create-boutique — Usine à Boutiques
|
|
@@ -14,28 +14,38 @@ const HELP = `create-boutique — Usine à Boutiques
|
|
|
14
14
|
--currency usd (défaut usd) --country us (défaut us) --language en (défaut en)
|
|
15
15
|
--themes a,b,c | all une boutique par thème, slug = <dossier>-<thème>
|
|
16
16
|
--boutique <slug>:<thème> ajoute une boutique au nom choisi (répétable)
|
|
17
|
+
--api-url <domaine> --files-url <domaine> production : domaines de l'admin et des images (avant le 1er démarrage)
|
|
18
|
+
--site-url <domaine> mode single : domaine du site
|
|
19
|
+
--mode multi|single multi (défaut) : un site par boutique ; single : un seul site, boutique active choisie dans l'admin
|
|
17
20
|
--stripe-pk / --stripe-sk / --stripe-whsec clés Stripe (optionnelles)
|
|
18
|
-
--resend-key / --email-from e-mails (optionnels)
|
|
21
|
+
--resend-key / --email-from e-mails par Resend (optionnels)
|
|
22
|
+
--smtp-host smtp.gmail.com --smtp-user <adresse> e-mails par SMTP (mot de passe : SMTP_PASS dans .env)
|
|
23
|
+
--mobile paytech,paydunya,naboopay paiement Wave / Orange Money (clés dans .env ; détails : create-boutique sans --yes)
|
|
19
24
|
--products <csv> --images <dossier> catalogue (optionnel)
|
|
20
25
|
--store-port 8001 --api-port 9000 --storage-port 9100 premiers ports essayés
|
|
21
26
|
--config <fichier.json> toutes les options dans un fichier (mêmes clés, en camelCase)
|
|
22
27
|
--yes aucune question, valeurs par défaut
|
|
23
28
|
|
|
24
29
|
create-boutique apply <dossier> [--no-up] applique config/project.json
|
|
30
|
+
create-boutique urls <dossier> [--api-url …] [--files-url …] [--site-url …] [--from <ancienne adresse des images>]
|
|
31
|
+
change les domaines publics ; réécrit l'adresse des images déjà en base
|
|
25
32
|
create-boutique add <dossier> --boutique <slug>:<thème>
|
|
26
33
|
create-boutique remove <dossier> --boutique <slug>
|
|
34
|
+
create-boutique set <dossier> [--mode multi|single] change de mode (un site par boutique ↔ un seul site)
|
|
27
35
|
create-boutique set <dossier> [--boutique <slug>] [--name …] [--theme …] [--currency …] [--country …] [--language …]
|
|
28
36
|
create-boutique themes liste les thèmes
|
|
29
|
-
create-boutique update <dossier> [--no-up]
|
|
37
|
+
create-boutique update <dossier> [--no-up] [--yes] met à jour le code (config, .env, données, custom.css conservés ;
|
|
38
|
+
code modifié à la main : listé, copié dans backups/, confirmation)
|
|
30
39
|
create-boutique check <dossier> commande test dans chaque boutique
|
|
31
|
-
create-boutique backup <dossier>
|
|
40
|
+
create-boutique backup <dossier> [--keep 7] base + images dans backups/<date>/ (--keep : garde les N plus récentes)
|
|
41
|
+
create-boutique restore <dossier> [--from <nom>] [--yes] remet une sauvegarde (la plus récente par défaut)
|
|
32
42
|
create-boutique admin <dossier> adresse, e-mail et mot de passe de l'admin
|
|
33
43
|
create-boutique guide <dossier> régénère GUIDE-CLIENT.pdf et GUIDE-COMPLET.pdf
|
|
34
44
|
create-boutique stripe-webhook <dossier> --url https://api.marque.com crée le webhook Stripe, écrit STRIPE_WEBHOOK_SECRET
|
|
35
45
|
create-boutique campaign export <dossier> campagnes en base → config/campaigns/*.json
|
|
36
46
|
create-boutique campaign import <dossier> [--boutique <slug>] [--yes] JSON → base (écrase)
|
|
37
47
|
`;
|
|
38
|
-
const COMMANDS = new Set(["apply", "add", "remove", "set", "themes", "update", "check", "backup", "campaign", "admin", "guide", "stripe-webhook", "help"]);
|
|
48
|
+
const COMMANDS = new Set(["restore", "urls", "apply", "add", "remove", "set", "themes", "update", "check", "backup", "campaign", "admin", "guide", "stripe-webhook", "help"]);
|
|
39
49
|
async function main() {
|
|
40
50
|
const { values, positionals } = parseArgs({
|
|
41
51
|
allowPositionals: true,
|
|
@@ -48,11 +58,20 @@ async function main() {
|
|
|
48
58
|
themes: { type: "string" },
|
|
49
59
|
boutique: { type: "string", multiple: true },
|
|
50
60
|
theme: { type: "string" },
|
|
61
|
+
mode: { type: "string" },
|
|
62
|
+
"api-url": { type: "string" },
|
|
63
|
+
"files-url": { type: "string" },
|
|
64
|
+
"site-url": { type: "string" },
|
|
65
|
+
from: { type: "string" },
|
|
66
|
+
keep: { type: "string" },
|
|
51
67
|
"stripe-pk": { type: "string" },
|
|
52
68
|
"stripe-sk": { type: "string" },
|
|
53
69
|
"stripe-whsec": { type: "string" },
|
|
54
70
|
"resend-key": { type: "string" },
|
|
55
71
|
"email-from": { type: "string" },
|
|
72
|
+
"smtp-host": { type: "string" },
|
|
73
|
+
"smtp-user": { type: "string" },
|
|
74
|
+
mobile: { type: "string" },
|
|
56
75
|
products: { type: "string" },
|
|
57
76
|
images: { type: "string" },
|
|
58
77
|
"store-port": { type: "string" },
|
|
@@ -88,11 +107,18 @@ async function main() {
|
|
|
88
107
|
language: lower(values.language),
|
|
89
108
|
themes: values.themes === "all" ? "all" : values.themes?.split(",").map((s) => s.trim()).filter(Boolean),
|
|
90
109
|
boutiques: values.boutique,
|
|
110
|
+
mode: lower(values.mode),
|
|
111
|
+
apiUrl: values["api-url"],
|
|
112
|
+
filesUrl: values["files-url"],
|
|
113
|
+
siteUrl: values["site-url"],
|
|
91
114
|
stripePk: values["stripe-pk"],
|
|
92
115
|
stripeSk: values["stripe-sk"],
|
|
93
116
|
stripeWhsec: values["stripe-whsec"],
|
|
94
117
|
resendKey: values["resend-key"],
|
|
95
118
|
emailFrom: values["email-from"],
|
|
119
|
+
smtpHost: values["smtp-host"],
|
|
120
|
+
smtpUser: values["smtp-user"],
|
|
121
|
+
mobile: values.mobile ? values.mobile.split(",").map((m) => m.trim()).filter(Boolean) : undefined,
|
|
96
122
|
products: values.products,
|
|
97
123
|
images: values.images,
|
|
98
124
|
storePort: port(values["store-port"]),
|
|
@@ -104,6 +130,8 @@ async function main() {
|
|
|
104
130
|
switch (first) {
|
|
105
131
|
case "apply":
|
|
106
132
|
return apply(dirArg(), up);
|
|
133
|
+
case "urls":
|
|
134
|
+
return urls(dirArg(), { api: values["api-url"], files: values["files-url"], site: values["site-url"], from: values.from }, up);
|
|
107
135
|
case "add":
|
|
108
136
|
return add(dirArg(), values.boutique ?? [], up);
|
|
109
137
|
case "remove":
|
|
@@ -118,15 +146,18 @@ async function main() {
|
|
|
118
146
|
currency: lower(values.currency),
|
|
119
147
|
country: lower(values.country),
|
|
120
148
|
language: lower(values.language),
|
|
149
|
+
mode: lower(values.mode),
|
|
121
150
|
}, up);
|
|
122
151
|
case "themes":
|
|
123
152
|
return themes();
|
|
124
153
|
case "update":
|
|
125
|
-
return update(dirArg(), up);
|
|
154
|
+
return update(dirArg(), { ...up, yes: !!values.yes });
|
|
126
155
|
case "check":
|
|
127
156
|
return check(dirArg());
|
|
128
157
|
case "backup":
|
|
129
|
-
return backup(dirArg());
|
|
158
|
+
return void backup(dirArg(), { keep: values.keep ? Number(values.keep) : undefined });
|
|
159
|
+
case "restore":
|
|
160
|
+
return restore(dirArg(), { from: values.from, yes: !!values.yes });
|
|
130
161
|
case "admin":
|
|
131
162
|
return admin(dirArg());
|
|
132
163
|
case "guide":
|