@salifbiaye/create-boutique 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salifbiaye/create-boutique",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Usine à Boutiques : génère un projet complet (backend Medusa + une boutique Next.js par campagne) prêt pour docker compose",
5
5
  "type": "module",
6
6
  "repository": {
@@ -27,6 +27,7 @@ import {
27
27
  linkSalesChannelsToApiKeyWorkflow,
28
28
  linkSalesChannelsToStockLocationWorkflow,
29
29
  updateRegionsWorkflow,
30
+ updateProductsWorkflow,
30
31
  updateShippingOptionsWorkflow,
31
32
  updateStoresWorkflow,
32
33
  uploadFilesWorkflow,
@@ -210,9 +211,14 @@ export default async function seed({ container }: ExecArgs) {
210
211
  })
211
212
  channel = result[0]
212
213
  await linkSalesChannelsToStockLocationWorkflow(container).run({ input: { id: location.id, add: [channel.id] } })
213
- // Boutique ajoutée à un projet existant : elle reçoit tout le catalogue déjà en base
214
- // (le marchand retire ensuite dans l'admin ce qu'il ne veut pas y vendre).
215
- const existingProducts = await productService.listProducts({}, { select: ["id"], take: 10000 })
214
+ // Boutique ajoutée à un projet existant : elle reçoit le catalogue déjà en base, sauf les produits
215
+ // du CSV réservés à d'autres boutiques (colonnes boutiques/themes). Le marchand ajuste ensuite dans l'admin.
216
+ const rows = readCsv()
217
+ const csvRow = new Map(rows.map((r) => [r.handle, r]))
218
+ const existingProducts = (await productService.listProducts({}, { select: ["id", "handle"], take: 10000 })).filter((pr) => {
219
+ const r = csvRow.get(pr.handle)
220
+ return !r || rowFor(r, b.slug, b.theme)
221
+ })
216
222
  if (existingProducts.length) {
217
223
  await linkProductsToSalesChannelWorkflow(container).run({
218
224
  input: { id: channel.id, add: existingProducts.map((pr) => pr.id) },
@@ -308,6 +314,20 @@ export async function uploadSeedImage(container: ExecArgs["container"], name: st
308
314
 
309
315
  type Row = Record<string, string>
310
316
 
317
+ function readCsv(): Row[] {
318
+ const csvFile = path.join(SEED_DIR, "products.csv")
319
+ if (!fs.existsSync(csvFile)) return []
320
+ return parse(fs.readFileSync(csvFile, "utf8"), { columns: true, skip_empty_lines: true, trim: true, bom: true })
321
+ }
322
+
323
+ /** Le produit de cette ligne est-il vendu dans la boutique ? Colonne « boutiques » (slugs), sinon « themes », sinon partout. */
324
+ function rowFor(r: Row, slug: string, theme: string) {
325
+ const list = (v: string) => v.split("|").map((x) => x.trim()).filter(Boolean)
326
+ if (r.boutiques) return list(r.boutiques).includes(slug)
327
+ if (r.themes) return list(r.themes).includes(theme)
328
+ return true
329
+ }
330
+
311
331
  async function importProducts({
312
332
  container,
313
333
  logger,
@@ -329,35 +349,50 @@ async function importProducts({
329
349
  endsOf: Record<string, string | undefined>
330
350
  locationId: string
331
351
  profileId: string
332
- productService: { listProducts: (f: { handle: string[] }, c: { select: string[] }) => Promise<{ handle: string }[]> }
352
+ productService: { listProducts: (f: { handle: string[] }, c: { select: string[] }) => Promise<{ id: string; handle: string; thumbnail?: string | null }[]> }
333
353
  }) {
334
- const csvFile = path.join(SEED_DIR, "products.csv")
335
- if (!fs.existsSync(csvFile)) {
354
+ const rows = readCsv()
355
+ if (!rows.length) {
336
356
  logger.info("[seed] pas de seed/products.csv : aucun produit importé")
337
357
  return
338
358
  }
339
- const rows: Row[] = parse(fs.readFileSync(csvFile, "utf8"), { columns: true, skip_empty_lines: true, trim: true, bom: true })
340
- const existing = new Set((await productService.listProducts({ handle: rows.map((r) => r.handle) }, { select: ["handle"] })).map((p) => p.handle))
359
+ const inDb = await productService.listProducts({ handle: rows.map((r) => r.handle) }, { select: ["id", "handle", "thumbnail"] })
360
+ const existing = new Set(inDb.map((p) => p.handle))
341
361
  const toCreate = rows.filter((r) => r.handle && !existing.has(r.handle))
342
- if (!toCreate.length) return
343
362
 
344
363
  // Images : fichiers de seed/images envoyés au stockage ; URL http(s) gardées telles quelles.
345
364
  const uploaded = new Map<string, string>()
346
- const imageUrl = async (name: string): Promise<string | undefined> => {
365
+ const imageUrl = async (name: string, warn = true): Promise<string | undefined> => {
347
366
  if (uploaded.has(name)) return uploaded.get(name)
348
367
  const url = await uploadSeedImage(container, name)
349
368
  if (url) uploaded.set(name, url)
350
- else logger.warn(`[seed] image introuvable : ${path.join(SEED_DIR, "images", path.basename(name))}`)
369
+ else if (warn) logger.warn(`[seed] image introuvable : ${path.join(SEED_DIR, "images", path.basename(name))}`)
351
370
  return url
352
371
  }
353
372
 
373
+ // Produits déjà en base sans photo : photos ajoutées plus tard dans seed/images → on les complète
374
+ // (un produit qui a déjà une photo, par exemple choisie dans l'admin, n'est jamais modifié).
375
+ const csvRow = new Map(rows.map((r) => [r.handle, r]))
376
+ let filled = 0
377
+ for (const p of inDb.filter((p) => !p.thumbnail)) {
378
+ const names = (csvRow.get(p.handle)?.images || "").split("|").filter(Boolean)
379
+ const images = (await Promise.all(names.map((n) => imageUrl(n, false)))).filter((u): u is string => !!u)
380
+ if (!images.length) continue
381
+ await updateProductsWorkflow(container).run({
382
+ input: { selector: { id: p.id }, update: { thumbnail: images[0], images: images.map((url) => ({ url })) } },
383
+ })
384
+ filled++
385
+ }
386
+ if (filled) logger.info(`[seed] photos ajoutées à ${filled} produit(s) existant(s)`)
387
+ if (!toCreate.length) return
388
+
354
389
  const salePrices: { variantKey: string; currency_code: string; amount: number; endsAt?: string }[] = []
355
390
  const products: CreateProductWorkflowInputDTO[] = []
356
391
  for (const r of toCreate) {
357
392
  const missing = currencies.filter((c) => !r[`price_${c}`])
358
393
  if (missing.length) logger.warn(`[seed] ${r.handle} : pas de prix en ${missing.join(", ")} (colonne price_<devise>) — invisible dans ces boutiques`)
359
394
 
360
- const images = (await Promise.all((r.images || "").split("|").filter(Boolean).map(imageUrl))).filter((u): u is string => !!u)
395
+ const images = (await Promise.all((r.images || "").split("|").filter(Boolean).map((n) => imageUrl(n)))).filter((u): u is string => !!u)
361
396
  const optionTitle = r.variant_option || "Default option"
362
397
  const values = r.variant_values ? r.variant_values.split("|").map((v) => v.trim()) : ["Default option value"]
363
398
  // Boutiques du produit : colonne « boutiques » (slugs), ou « themes » (toutes les boutiques de ces thèmes,
@@ -90,7 +90,23 @@
90
90
  transform: rotate(-24deg);
91
91
  pointer-events: none;
92
92
  }
93
- .ub-tc-hero__cover { display: none; }
93
+ /* Cyber Monday 1 : le visuel fait partie du fond (comme Black Friday) : posé à droite sur toute la hauteur,
94
+ fondu dans le noir à gauche, en haut et en bas. Pas de cadre, pas d'ombre. */
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; }
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
+ .ub-tc-hero__cover::after {
99
+ content: "";
100
+ position: absolute;
101
+ inset: 0;
102
+ background:
103
+ linear-gradient(90deg, var(--hero-bg) 0%, rgb(5 3 11 / 0.55) 20%, transparent 45%),
104
+ linear-gradient(0deg, var(--hero-bg) 0%, transparent 18%),
105
+ linear-gradient(180deg, var(--hero-bg) 0%, transparent 12%);
106
+ }
107
+ .ub-tc-hero[data-has-image] .ub-tc-hero__side > img { display: none; }
108
+ .ub-tc-hero[data-has-image] .ub-tc-hero__side { aspect-ratio: auto; align-self: end; }
109
+ .ub-tc-hero[data-has-image] .ub-tc-badge { position: relative; left: auto; bottom: auto; justify-self: start; width: max-content; }
94
110
  .ub-tc-hero__in {
95
111
  display: grid;
96
112
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
@@ -397,6 +413,10 @@
397
413
  @media (max-width: 900px) {
398
414
  .ub-tc-hero__in { grid-template-columns: 1fr; gap: var(--space-8); min-height: 0; padding-top: var(--space-8); }
399
415
  .ub-tc-hero__side { justify-self: center; width: min(100%, 440px); margin-right: 0; }
416
+ .ub-tc-hero__cover { inset: 38% 0 0 0; }
417
+ .ub-tc-hero__cover img { object-position: 60% 60%; }
418
+ .ub-tc-hero__cover::after { background: linear-gradient(180deg, var(--hero-bg) 0%, rgb(5 3 11 / 0.4) 35%, transparent 60%), linear-gradient(0deg, var(--hero-bg) 0%, transparent 20%); }
419
+ .ub-tc-hero[data-has-image] .ub-tc-hero__side { min-height: 300px; justify-self: stretch; width: 100%; }
400
420
  .ub-tc-badge { left: 12px; bottom: 16px; padding: 10px 16px; }
401
421
  .ub-tc-badge b { font-size: 32px; }
402
422
  /* Cyber Monday 2 mobile : visuel en haut, texte dessous sur fond blanc */