Halaman Product Detail
├─ ❌ Description = (tidak ada)
├─ ❌ Harga = Rp 0
├─ ❌ Stock Status = Out of Stock
├─ ❌ Select Variant Section:
│ ├─ [HDR Mode] ← SALAH! ini adalah option, bukan variant
│ ├─ [Refresh Rate]
│ └─ [Panel]
└─ ❌ Colors & Options = (tidak terlihat)
Halaman Product Detail ✅
├─ ✅ Nama Produk
├─ ✅ Description: "Monitor gaming premium 27 inch..."
├─ ✅ Harga: Rp 15.990.000 (bukan Rp 0)
├─ ✅ Brand: Asus
├─ ✅ Rating: 4.9 / 5.0
├─ ✅ Badges: Featured, Bestseller
├─ ✅ Availability: Tersedia (status benar)
├─ ✅ Features listed
├─ ✅ Image Gallery
│
├─ SECTION: Select Product Variant (hanya jika ada variants)
│ ├─ [Variant A - Rp 1.000.000]
│ ├─ [Variant B - Rp 1.200.000]
│ └─ (hidden jika tidak ada variants)
│
├─ SECTION: Colors & Specifications
│ ├─ Color: [Black] [Silver] [Gray]
│ └─ Specifications:
│ ├─ HDR Mode: [DisplayHDR 400 True Black]
│ ├─ Refresh Rate: [60Hz] [120Hz] [144Hz] [240Hz]
│ └─ Panel: [QD-OLED]
│
└─ Add to Cart Button
Fungsi: Centralized data normalization dengan field name mapping
// Before: Harus handle di service & component
const features = data.features || [];
// After: Single source of truth
const normalizeProductData = (data: RawProductData) => {
const features = getArrayValue(data.features)
|| getArrayValue(data.key_features);
const colors = getArrayValue(data.colors)
|| getArrayValue(data.available_colors);
// ... dll
}Field Mappings:
| Backend | Frontend | Type | Handler |
|---|---|---|---|
key_features |
features |
string[] | getArrayValue |
available_colors |
colors |
string[] | getArrayValue |
product_overview |
overview |
string[] | getArrayValue |
product_options |
options |
Record<string, string[]> | normalizeOptions |
selling_price | cost_price |
price |
number | priceChain |
gallery |
images |
string[] | mapImageURLs |
stock |
availability.status |
enum | stockToAvailability |
Sebelum: Inline normalization, duplikasi logic
Sesudah: Menggunakan helper function
// Before (duplicate code di service)
const normalizeProduct = (data) => { /* 200 lines */ }
// After (clean & reusable)
const normalizeProduct = (data) => normalizeProductData(data);// Before: Tidak ada pengecekan tipe
{product.variants && product.variants.map(...)}
{product.options && product.options.map(...)} // Rancu!
// After: Clear separation
{product.variants && product.variants.length > 0 && (
<div className="border-b pb-4">
<label>Select Product Variant</label>
{/* Hanya untuk real variants dengan harga berbeda */}
</div>
)}
{product.options && Object.keys(product.options).length > 0 && (
<div className="border-t pt-4">
<label>Specifications</label>
{/* Product options/specifications */}
</div>
)}// Before
const itemPrice = selectedVar ? selectedVar.price : getDisplayPrice(product);
// getDisplayPrice hanya fallback ke variant, tidak ke selling_price
// After (di productNormalizer)
let price = data.price || 0;
if (price === 0 && data.selling_price) price = data.selling_price;
if (price === 0 && data.cost_price) price = data.cost_price;// Before
availability: (data.availability as Record<string, unknown>)
// Jika tidak ada availability field, jadi undefined/null
// After
if (!availability && data.stock !== undefined) {
availability = {
status: stock > 0 ? 'in_stock' : 'out_of_stock',
label: stock > 0 ? 'Tersedia' : 'Habis',
color: stock > 0 ? 'green' : 'red',
};
}Backend API Response
↓
│ (raw data dengan field names bervariasi)
│ {
│ price: 0,
│ selling_price: 15990000,
│ key_features: [...],
│ available_colors: [...],
│ product_overview: [...],
│ product_options: {...}
│ }
↓
┌─────────────────────────────────┐
│ catalogService.getProductDetail │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ normalizeProductData() │
│ (src/utils/productNormalizer) │
│ │
│ - Map field names │
│ - Validate types │
│ - Handle fallbacks │
│ - Build full URLs │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ Product Interface (Frontend) │
│ { │
│ price: 15990000, ✅ │
│ features: [...], ✅ │
│ colors: [...], ✅ │
│ overview: [...], ✅ │
│ options: {...}, ✅ (mapped) │
│ availability: { ✅ (from price: 0 → stock)
│ status: 'in_stock' │
│ } │
│ } │
└────────────┬────────────────────┘
↓
┌─────────────────────────────────┐
│ ProductDetail Component │
│ (React UI Logic) │
│ │
│ if (product.variants) → Section│
│ if (product.colors) → Buttons │
│ if (product.options) → Pills │
└─────────────────────────────────┘
Frontend Rendering Verification:
✅ [ ] Description muncul
✅ [ ] Harga: Rp 15.990.000 (bukan Rp 0)
✅ [ ] Availability: "Tersedia" (bukan "Habis")
✅ [ ] Badges: Featured ⭐ + Bestseller 🔥
Variants Section:
✅ [ ] Jika ada variants → muncul di "Select Product Variant"
✅ [ ] Jika tidak ada → section tersembunyi
✅ [ ] Harga variant: Rp 1.000.000 (berbeda per varian)
✅ [ ] Stock info: "5 in stock" or "Out of stock"
Colors & Options:
✅ [ ] Colors: [Black] [Silver] [Gray] buttons
✅ [ ] Options: HDR Mode, Refresh Rate, Panel pills
✅ [ ] TIDAK ada di variants section
Images:
✅ [ ] Main image muncul
✅ [ ] Thumbnails: dapat diklik
✅ [ ] Fallback image jika error
Add to Cart:
✅ [ ] Button enabled/disabled sesuai state
✅ [ ] Harga di cart: correct (product atau variant)
✅ [ ] Quantity: +/- buttons berfungsi
Sebelum push ke production:
1. Backend Validation:
[ ] Ensure fieldnames konsisten (key_features vs features)
[ ] Validate price not always 0 (gunakan selling_price/cost_price)
[ ] Check stock field available untuk availability mapping
2. Frontend Validation:
[ ] npm run build (no errors)
[ ] Test di browser: npm run dev
[ ] Check console: no fetch errors
3. API Response Validation:
[ ] Test dengan backend API
[ ] Check response structure di Network tab
[ ] Verify all images URLs load properly
4. Edge Cases:
[ ] Product tanpa variants → section hidden ✓
[ ] Product tanpa options → specs section hidden ✓
[ ] Missing images → placeholder shown ✓
[ ] Price = 0 → fallback ke selling_price ✓
No changes to visible UI/UX, hanya internal logic perbaikan:
- Same visual layout
- Better data accuracy
- Cleaner code structure
Status: ✅ Complete & Ready for Testing
Files Modified: 3
Files Created: 1
Breaking Changes: None