- SingleProduct.vue: rebuild gallery/header/action bar as a self-scrolling layout with a sticky photo, solid-on-scroll header matching PageBar, and a full-screen image preview (blurred overlay, centered thumbnail strip) - Add GallerySlider.vue: pointer-based swipe carousel isolated in an LTR subtree, fixing RTL/transform sign mismatch that caused slide jumps - Fix Icon.vue: icon was read into a plain ref once at setup and never updated on prop change; switch to computed so icon swaps (e.g. cart qty stepper trash/minus toggle) actually re-render - Fix quantity stepper DOM order in SingleProduct.vue/CartItem.vue so plus/minus render correctly under RTL flex - prisma/assign-downloaded-images.js: seed real product photos (FEATURED/ FEATURED_MOBILE/GALLERY/GALLERY_MOBILE) onto the 5 placeholder products
135 lines
5.4 KiB
JavaScript
135 lines
5.4 KiB
JavaScript
// 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());
|