@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
|
@@ -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
|
|
214
|
-
// (
|
|
215
|
-
const
|
|
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
|
|
335
|
-
if (!
|
|
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
|
|
340
|
-
const existing = new Set(
|
|
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
|
-
|
|
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 */
|