Redesign product detail page and fix icon/cart reactivity bugs
- 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
This commit is contained in:
@@ -115,10 +115,16 @@ export class AdminProductsService {
|
||||
: null,
|
||||
// legacy grid slot reads `ProductImage`
|
||||
ProductImage: p.images.map((i) => ({ id: i.id, image: i.url, full: i.url, url: i.url })),
|
||||
images: p.images.map((i) => ({ id: i.id, url: i.url, sortOrder: i.sortOrder })),
|
||||
images: p.images.map((i) => ({ id: i.id, url: i.url, sortOrder: i.sortOrder, role: i.role })),
|
||||
// The order builder's grid reads `thumb.full` and `variants_all` directly
|
||||
// off each row, so both travel with the list as they did in legacy.
|
||||
thumb: p.images[0] ? { full: p.images[0].url } : null,
|
||||
// off each row, so both travel with the list as they did in legacy. Legacy's
|
||||
// grid thumbnail was always the featured image (تصویر شاخص) when one was
|
||||
// set — fall back to the first gallery image only if no featured image
|
||||
// was uploaded, rather than picking whichever image happens to sort first.
|
||||
thumb: (() => {
|
||||
const featured = p.images.find((i) => i.role === 'FEATURED') ?? p.images[0];
|
||||
return featured ? { full: featured.url } : null;
|
||||
})(),
|
||||
variants_all: p.variants.map((v) => this.variantRow(v)),
|
||||
variants_count: p.variants.length,
|
||||
// legacy min_gram / max_gram: the product's lightest and heaviest variant,
|
||||
@@ -160,6 +166,13 @@ export class AdminProductsService {
|
||||
cats: p.categoryId ? [p.categoryId] : [],
|
||||
ciny_ids: p.collections.map((c) => c.collectionId),
|
||||
variants_all: p.variants.map((v) => this.variantRow(v)),
|
||||
// The 4 image slots the product form edits, split back out by role —
|
||||
// mirrors the shape AddProduct.vue posts (indicator_id / mobile_pic /
|
||||
// images / images_mobile).
|
||||
indicator_id: p.images.find((i) => i.role === 'FEATURED')?.url ?? null,
|
||||
mobile_pic: p.images.find((i) => i.role === 'FEATURED_MOBILE')?.url ?? null,
|
||||
images_gallery: p.images.filter((i) => i.role === 'GALLERY').map((i) => i.url),
|
||||
images_mobile: p.images.filter((i) => i.role === 'GALLERY_MOBILE').map((i) => i.url),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -285,6 +298,36 @@ export class AdminProductsService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy had 4 distinct upload slots (تصویر شاخص / تصویر شاخص موبایل /
|
||||
* گالری تصاویر سایت / گالری تصاویر موبایل — AddProduct.vue's `indicator_id`,
|
||||
* `mobile_pic`, `images`, and a mobile gallery). The form posts one URL each
|
||||
* for the two featured slots and one URL array each for the two galleries;
|
||||
* this flattens all four into the role-tagged rows ProductImage.create wants.
|
||||
*/
|
||||
private imageCreateData(data: Record<string, unknown>): { url: string; sortOrder: number; role: Prisma.ProductImageCreateInput['role'] }[] {
|
||||
const rows: { url: string; sortOrder: number; role: Prisma.ProductImageCreateInput['role'] }[] = [];
|
||||
|
||||
const single = (v: unknown, role: Prisma.ProductImageCreateInput['role']) => {
|
||||
const url = typeof v === 'string' ? v : String((v as Record<string, unknown>)?.url ?? '');
|
||||
if (url) rows.push({ url, sortOrder: 0, role });
|
||||
};
|
||||
const gallery = (v: unknown, role: Prisma.ProductImageCreateInput['role']) => {
|
||||
if (!Array.isArray(v)) return;
|
||||
(v as unknown[]).forEach((item, i) => {
|
||||
const url = typeof item === 'string' ? item : String((item as Record<string, unknown>)?.url ?? '');
|
||||
if (url) rows.push({ url, sortOrder: i, role });
|
||||
});
|
||||
};
|
||||
|
||||
single(data.indicator_id, 'FEATURED');
|
||||
single(data.mobile_pic, 'FEATURED_MOBILE');
|
||||
gallery(data.images, 'GALLERY');
|
||||
gallery(data.images_mobile, 'GALLERY_MOBILE');
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Legacy `POST product/create`. */
|
||||
async create(data: Record<string, unknown>) {
|
||||
const title = String(data.title ?? '').trim();
|
||||
@@ -295,6 +338,7 @@ export class AdminProductsService {
|
||||
const variants = Array.isArray(data.variants)
|
||||
? (data.variants as Record<string, unknown>[])
|
||||
: [];
|
||||
const images = this.imageCreateData(data);
|
||||
|
||||
return this.prisma.product.create({
|
||||
data: {
|
||||
@@ -311,8 +355,9 @@ export class AdminProductsService {
|
||||
variants: variants.length
|
||||
? { create: variants.map((v) => this.variantCreateData(v)) }
|
||||
: undefined,
|
||||
images: images.length ? { create: images } : undefined,
|
||||
},
|
||||
include: { variants: true },
|
||||
include: { variants: true, images: true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -371,6 +416,21 @@ export class AdminProductsService {
|
||||
});
|
||||
}
|
||||
|
||||
// Images carry no downstream reference (unlike variants/OrderItem), so all
|
||||
// 4 slots are applied as a straight replace: clear and recreate from
|
||||
// whatever the form posted. The product form always posts all 4 image
|
||||
// fields together (even as empty/absent), so any one of them present means
|
||||
// this is a real submit, not a partial PATCH missing the others.
|
||||
if (['indicator_id', 'mobile_pic', 'images', 'images_mobile'].some((k) => k in data)) {
|
||||
const images = this.imageCreateData(data);
|
||||
await this.prisma.productImage.deleteMany({ where: { productId: id } });
|
||||
if (images.length) {
|
||||
await this.prisma.productImage.createMany({
|
||||
data: images.map((img) => ({ ...img, productId: id })),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user