diff --git a/HANDOFF.md b/HANDOFF.md index d79d9ee..fab3946 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -26,7 +26,31 @@ Latest TypeScript verification after the validation/upload changes: 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 diff --git a/src/app/admin/review/[productId]/page.tsx b/src/app/admin/review/[productId]/page.tsx index 5e5f1bd..8b0b0c3 100644 --- a/src/app/admin/review/[productId]/page.tsx +++ b/src/app/admin/review/[productId]/page.tsx @@ -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(); + return refs.filter((item) => { + const key = item.id || item.url; + if (!key || seen.has(key)) return false; + seen.add(key); + return true; + }); +} + function ModelCard({ model, index, @@ -559,7 +585,6 @@ function ProductColumn({ ); 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 keywords = Array.isArray(product.productKeyWords) ? product.productKeyWords : []; const productInfos = Array.isArray(product.productInformations) @@ -571,21 +596,7 @@ function ProductColumn({ const productFiles = Array.isArray(product.productFiles) ? product.productFiles.filter((item) => item.file || item.fileId || item.id) : []; - const allImages = [ - ...(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 allImages = getProductImageRefs(product); const shouldRender = (key: CompareSectionKey) => !section || section === key; @@ -951,17 +962,7 @@ function AdminReviewDetailPageInner() { const categoryInfos = Array.isArray(product.categoryInformations) ? product.categoryInformations.filter((item) => item.paramName && item.paramValue) : []; - const allImages = [ - ...(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)) - : []), - ]; + const allImages = getProductImageRefs(product); // ── Reject modal ────────────────────────────────────────────────────────