// One-off data fix: the 5 hand-authored demo products (product-classic-ring, // product-woven-bracelet, product-pendant-necklace, product-hoop-earrings, // product-full-coin) each had exactly one ProductImage row pointing at // /img/no-items.png — a missing-image placeholder, not a real photo. This // replaces those with a real, varied set drawn from the 15 jewelry photos the // user pointed at (originally in "Downloads/New folder", already copied into // storage/uploads/ under their original filenames — confirmed present before // this script was written). // // Distribution, mirroring the one product in this DB that already has a full, // correctly-roled image set (`شسی`, id cmu4ia4iq0008s4zhwrz6wy3z): // - exactly one FEATURED (تصویر شاخص وب) // - exactly one FEATURED_MOBILE (تصویر شاخص موبایل) // - 2–3 GALLERY / GALLERY_MOBILE, split across both roles // // CHECKED, not assumed: CatalogService.listProducts() (apps/api/src/modules/ // catalog/catalog.service.ts) returns every ProductImage row unfiltered — // there is no role-based split on the read side today. So on the user-app // side ALL of these rows end up in ProductSummary.images and all of them are // swipeable in the gallery/preview — the FEATURED/FEATURED_MOBILE role still // matters (it is what the admin panel and any future "hero image" logic reads // by role), but right now it does not narrow what the shopper-facing gallery // shows. `sortOrder` is set to the creation index below, which is what // actually determines swipe order today, since nothing filters by role. // // Assignment across the 5 products is a random, non-repeating draw per // product from the 15-photo pool (Fisher-Yates, reseedable via IMG_SEED for a // reproducible run) — the same photo can end up on more than one product // across different products (15 photos split ~4 ways per product exceeds the // pool), which is fine: this is demo data standing in for real per-product // photography, not a claim that these are 5 distinct real items. const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); // The 15 files confirmed present in storage/uploads/ under their original // names before this script ran. const POOL = [ '152_5e1435ca-05a5-4146-a4ea-89726650c29c.webp', '59_0661f668-5d1d-4c05-87fd-50fd65cd8383.webp', '71085964.png', '79A202-1201_01.webp', 'Brooches-Vintage-Jewelry-full-1-322_60.60-9ed8e81b.webp', 'MB116662_Front_1080p_f364da1d-6019-4ca4-8dd8-a323387634c7.webp', 'TEMP_360x360.webp', 'channel_setting_480x480.webp', 'engagement-ring-in-18k-gold-with-diamonds-nr532-white-18k-semi-white-205224.webp', 'finished-jewelry.webp', 'kisspng-earring-gold-jewellery-ring-new-22ct-yellow-gold-ring-armari-45-sku27555-5d0796cb466539.4977043815607784432884-768x768.png', 'pl-background-free-product-photography-jewelery-3.webp', 'round-cut-engagement-ring-in-18k-gold-with-diamonds-lr3011-2t-18k-x-2t-964060.webp', 'td4-aam-n0_dd28f69d-9e3c-490d-be37-1ba074737992.webp', 'w=640,h=640,fit=crop.avif', ]; const PRODUCT_IDS = [ 'product-classic-ring', 'product-woven-bracelet', 'product-pendant-necklace', 'product-hoop-earrings', 'product-full-coin', ]; // Simple seeded PRNG (mulberry32) so a re-run with the same IMG_SEED is // reproducible — useful for re-checking a specific assignment without the // draw changing under you. function mulberry32(seed) { return function () { seed |= 0; seed = (seed + 0x6d2b79f5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const seed = Number(process.env.IMG_SEED) || Date.now(); const rand = mulberry32(seed); function shuffled(arr) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } async function main() { console.log(`seed=${seed}`); for (const productId of PRODUCT_IDS) { const product = await prisma.product.findUnique({ where: { id: productId } }); if (!product) { console.log(`skip ${productId}: not found`); continue; } // 3–5 photos per product, drawn without replacement from the pool. const count = 3 + Math.floor(rand() * 3); // 3, 4, or 5 const picks = shuffled(POOL).slice(0, count); // Role plan: first pick is FEATURED, second is FEATURED_MOBILE, the rest // alternate GALLERY / GALLERY_MOBILE — same shape as the شسی product's // existing 8-image set, just proportioned to a smaller pick count. const roles = ['FEATURED', 'FEATURED_MOBILE']; for (let i = 2; i < picks.length; i++) { roles.push(i % 2 === 0 ? 'GALLERY' : 'GALLERY_MOBILE'); } // Replace whatever this product currently has (the single no-items.png // placeholder row) rather than appending alongside it. await prisma.productImage.deleteMany({ where: { productId } }); for (let i = 0; i < picks.length; i++) { await prisma.productImage.create({ data: { id: `${productId}-img-${i + 1}`, productId, url: `/uploads/${picks[i]}`, sortOrder: i, role: roles[i], }, }); } console.log(`${productId}: ${picks.length} images -> ${picks.map((p, i) => `${roles[i]}:${p}`).join(', ')}`); } } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());