Deduplicate admin review product images
This commit is contained in:
26
HANDOFF.md
26
HANDOFF.md
@ -26,7 +26,31 @@ Latest TypeScript verification after the validation/upload changes:
|
|||||||
npx tsc --noEmit
|
npx tsc --noEmit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Latest Codex Changes After `2cfff02`
|
## Latest Codex Changes After `4e28ccd`
|
||||||
|
|
||||||
|
### Admin review image dedupe
|
||||||
|
|
||||||
|
File:
|
||||||
|
- `src/app/admin/review/[productId]/page.tsx`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Admin product review now deduplicates product images before rendering.
|
||||||
|
- The dedupe applies to both:
|
||||||
|
- update compare section (`Gambar Produk`)
|
||||||
|
- normal admin review gallery (`Galeri`)
|
||||||
|
- Root `imageId` / `image` and entries inside `productImages` are merged and sorted by `sequence`.
|
||||||
|
- Duplicate images are removed by `imageId`, with URL as a fallback key.
|
||||||
|
- This prevents the main image from appearing twice when backend sends it both as root `imageId` and inside `productImages`.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The local production server was restarted on `http://localhost:3000` for manual admin review testing.
|
||||||
|
|
||||||
|
## Previous Codex Changes After `2cfff02`
|
||||||
|
|
||||||
### Product edit image payload preservation
|
### Product edit image payload preservation
|
||||||
|
|
||||||
|
|||||||
@ -166,6 +166,32 @@ function hasChangesForPaths(rows: CompareRow[], paths: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProductImageRefs(product: ReviewProductData | null) {
|
||||||
|
if (!product) return [];
|
||||||
|
|
||||||
|
const refs = [
|
||||||
|
...(product.imageId || product.image
|
||||||
|
? [{ id: product.imageId || "", url: imgUrl(product.imageId, product.image) || "" }]
|
||||||
|
: []),
|
||||||
|
...(Array.isArray(product.productImages)
|
||||||
|
? [...product.productImages]
|
||||||
|
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
|
||||||
|
.map((item) => ({
|
||||||
|
id: item.imageId || "",
|
||||||
|
url: imgUrl(item.imageId, item.image) || "",
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return refs.filter((item) => {
|
||||||
|
const key = item.id || item.url;
|
||||||
|
if (!key || seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function ModelCard({
|
function ModelCard({
|
||||||
model,
|
model,
|
||||||
index,
|
index,
|
||||||
@ -559,7 +585,6 @@ function ProductColumn({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const models = Array.isArray(product.productModels) ? product.productModels : [];
|
const models = Array.isArray(product.productModels) ? product.productModels : [];
|
||||||
const images = Array.isArray(product.productImages) ? product.productImages : [];
|
|
||||||
const features = Array.isArray(product.productFeatures) ? product.productFeatures : [];
|
const features = Array.isArray(product.productFeatures) ? product.productFeatures : [];
|
||||||
const keywords = Array.isArray(product.productKeyWords) ? product.productKeyWords : [];
|
const keywords = Array.isArray(product.productKeyWords) ? product.productKeyWords : [];
|
||||||
const productInfos = Array.isArray(product.productInformations)
|
const productInfos = Array.isArray(product.productInformations)
|
||||||
@ -571,21 +596,7 @@ function ProductColumn({
|
|||||||
const productFiles = Array.isArray(product.productFiles)
|
const productFiles = Array.isArray(product.productFiles)
|
||||||
? product.productFiles.filter((item) => item.file || item.fileId || item.id)
|
? product.productFiles.filter((item) => item.file || item.fileId || item.id)
|
||||||
: [];
|
: [];
|
||||||
const allImages = [
|
const allImages = getProductImageRefs(product);
|
||||||
...(product.imageId || product.image
|
|
||||||
? [{ id: product.imageId, url: imgUrl(product.imageId, product.image) }]
|
|
||||||
: []),
|
|
||||||
...images
|
|
||||||
.sort(
|
|
||||||
(a: { sequence?: number }, b: { sequence?: number }) =>
|
|
||||||
(a.sequence ?? 0) - (b.sequence ?? 0)
|
|
||||||
)
|
|
||||||
.map((img: { imageId?: string; image?: string }) => ({
|
|
||||||
id: img.imageId,
|
|
||||||
url: imgUrl(img.imageId, img.image),
|
|
||||||
}))
|
|
||||||
.filter((img) => Boolean(img.url)),
|
|
||||||
];
|
|
||||||
|
|
||||||
const shouldRender = (key: CompareSectionKey) => !section || section === key;
|
const shouldRender = (key: CompareSectionKey) => !section || section === key;
|
||||||
|
|
||||||
@ -951,17 +962,7 @@ function AdminReviewDetailPageInner() {
|
|||||||
const categoryInfos = Array.isArray(product.categoryInformations)
|
const categoryInfos = Array.isArray(product.categoryInformations)
|
||||||
? product.categoryInformations.filter((item) => item.paramName && item.paramValue)
|
? product.categoryInformations.filter((item) => item.paramName && item.paramValue)
|
||||||
: [];
|
: [];
|
||||||
const allImages = [
|
const allImages = getProductImageRefs(product);
|
||||||
...(product.imageId || product.image
|
|
||||||
? [{ id: product.imageId, url: imgUrl(product.imageId, product.image) }]
|
|
||||||
: []),
|
|
||||||
...(Array.isArray(product.productImages)
|
|
||||||
? product.productImages
|
|
||||||
.sort((a, b) => (a.sequence ?? 0) - (b.sequence ?? 0))
|
|
||||||
.map((item) => ({ id: item.imageId || "", url: imgUrl(item.imageId, item.image) || "" }))
|
|
||||||
.filter((item) => Boolean(item.url))
|
|
||||||
: []),
|
|
||||||
];
|
|
||||||
|
|
||||||
// ── Reject modal ────────────────────────────────────────────────────────
|
// ── Reject modal ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user