# 🛍️ Dokumentasi Halaman Produk
**File:** [src/pages/ProgramList.tsx](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/ProgramList.tsx) | **Route:** `/produk`
**Stack:** React 18 · TypeScript · TailwindCSS v3 · Supabase · Vite

---

## 1. Ringkasan

Halaman Produk menampilkan paket-paket donasi yang bisa dipesan dalam jumlah tertentu (berbasis unit/qty). Berbeda dari program donasi biasa — produk memiliki `price_per_unit` dan `unit` (mis: per paket, per box, per ekor).

**Konten berurutan (mode normal — tidak ada filter):**
1. Category Filter Chips
2. Hero Banner Carousel
3. Produk Populer (4 grid)
4. Bento Layout (Featured card besar + Event Carousel)
5. Produk Baru (Infinity Carousel)
6. Produk Lainnya (horizontal scroll)

**Mode filter aktif:** Hanya menampilkan Category Filter Chips + Grid hasil.

---

## 2. Design System (sama dengan Beranda)

```js
// tailwind.config.js
brand: {
  DEFAULT: "#164B84",
  dark:    "#0e3460",
  light:   "#FFF7F7",   // background halaman
  accent:  "#9EEFE5",
  50:  "#e8f0fa",
  100: "#c5d9f0",
}
```

```css
/* index.css */
[data-slot="progress-indicator"] {
  background: linear-gradient(90deg, #164B84 0%, #9EEFE5 100%) !important;
}
.hide-scrollbar { -ms-overflow-style:none; scrollbar-width:none; }
.hide-scrollbar::-webkit-scrollbar { display:none; }
```

---

## 3. Layout Shell (sama dengan Beranda)

```tsx
// App.tsx
<div className="h-screen bg-brand-light overflow-hidden">
  <div className="bg-white h-full shadow-lg relative flex flex-col overflow-hidden">
    <Header />
    <main className="flex-1 overflow-y-auto app-scroll-container pt-[104px] md:pt-[68px] pb-24">
      <ProgramList />
    </main>
    <BottomNav />
  </div>
</div>
```

```tsx
// Container halaman
<div className="pb-4 max-w-6xl mx-auto">
```

> Header dan BottomNav tetap tampil di halaman `/produk`. Keduanya auto-hide saat scroll ke bawah > 60px.

---

## 4. State Utama

```tsx
const [currentBanner, setCurrentBanner] = useState(0);
const [currentEvent, setCurrentEvent] = useState(0);
const [selectedCategory, setSelectedCategory] = useState("");

const searchQuery = searchParams.get("q") || "";
const isFiltering = !!searchQuery || !!selectedCategory;
```

---

## 5. Data & Hooks

```ts
const { products }       = useProducts();     // tabel: product_packages
const { banners }        = useBanners();      // tabel: banners
const { events }         = useEvents();       // tabel: events
const { items: produkBaruItems } = useProdukBaru(); // tabel: produk_baru
```

**Layout data (mode normal):**
```ts
const topRow        = filteredProducts.slice(0, 4); // Produk Populer
const featuredPkg   = filteredProducts[4];           // Featured bento card
const scrollProducts = filteredProducts.slice(5);    // Produk Lainnya
```

**Filter produk:**
```ts
const filteredProducts = useMemo(() =>
  products.filter(p => {
    if (searchQuery && !p.title.toLowerCase().includes(searchQuery.toLowerCase())) return false;
    if (selectedCategory && p.category !== selectedCategory) return false;
    return true;
  }),
[products, searchQuery, selectedCategory]);
```

**Kategori unik:**
```ts
const categories = useMemo(() => {
  const cats = [...new Set(products.map(p => p.category))];
  return cats.sort();
}, [products]);
```

---

## 6. Seksi A — Category Filter Chips

Tampil ketika ada `?q=` atau ada kategori tersedia.

```tsx
<div className="px-4 md:px-8 mb-4">
  <div className="flex gap-2 overflow-x-auto hide-scrollbar pb-1">
    {/* Chip "Semua" */}
    <button onClick={() => setSelectedCategory("")}
      className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors flex-shrink-0
        ${!selectedCategory ? "bg-brand text-white" : "bg-gray-100 text-gray-600 hover:bg-brand-50"}`}>
      Semua
    </button>
    {/* Chip per kategori */}
    {categories.map(cat => (
      <button key={cat}
        onClick={() => setSelectedCategory(selectedCategory === cat ? "" : cat)}
        className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap transition-colors flex-shrink-0
          ${selectedCategory === cat ? "bg-brand text-white" : "bg-gray-100 text-gray-600 hover:bg-brand-50"}`}>
        {cat}
      </button>
    ))}
  </div>
  {isFiltering && (
    <p className="text-xs text-gray-500 mt-2">{filteredProducts.length} produk ditemukan</p>
  )}
</div>
```

**Style chip aktif:** `bg-brand text-white`
**Style chip inaktif:** `bg-gray-100 text-gray-600 hover:bg-brand-50`

---

## 7. Mode Filter Aktif

Ketika `isFiltering = true`, tampilkan grid sederhana menggantikan seluruh layout normal:

```tsx
{isFiltering ? (
  <div className="px-4 md:px-8 mb-6">
    {filteredProducts.length === 0 ? (
      <div className="text-center py-16">
        <p className="text-gray-500 mb-2">Tidak ada produk ditemukan</p>
        <button onClick={() => setSelectedCategory("")} className="text-brand text-sm font-medium">
          Hapus filter
        </button>
      </div>
    ) : (
      <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
        {filteredProducts.map(pkg => (
          <SmallPackageCard key={pkg.id} pkg={pkg} onClick={() => navigate(`/produk/${pkg.id}`)} />
        ))}
      </div>
    )}
  </div>
) : (
  /* layout normal */
)}
```

---

## 8. Seksi B — Hero Banner Carousel

```
px-4 md:px-8 mb-6 | h-48 md:h-64 | auto-advance 5 detik
```

Sama strukturnya dengan banner di Beranda, **beda tinggi** (`md:h-64`) dan **beda style tombol** (putih solid):

```tsx
<button className="self-start px-5 py-2.5 bg-white text-brand rounded-full text-sm font-semibold hover:bg-brand-accent transition-colors shadow-md">
  {banner.button_text}
</button>
```

**Overlay:** `bg-gradient-to-r ${banner.color} opacity-60` (lebih transparan dari Beranda yang 90%)

**Supabase tabel `banners`:**
| Field | Tipe | Keterangan |
|-------|------|-----------|
| `title` | text | Judul |
| `subtitle` | text | Sub-judul |
| `description` | text | Deskripsi |
| `button_text` | text | Label CTA |
| `image` | text | URL gambar |
| `color` | text | Tailwind gradient class |
| `sort_order` | int | Urutan |
| `is_active` | bool | Toggle |

---

## 9. Seksi C — Produk Populer

```
px-4 md:px-8 mb-6 | Data: filteredProducts.slice(0, 4)
Grid: grid grid-cols-2 md:grid-cols-4 gap-3
```

```tsx
<div className="flex items-center justify-between mb-4">
  <h3 className="text-base font-semibold text-gray-800">Produk Populer</h3>
  <button onClick={() => navigate('/donasi')}
    className="text-sm text-brand font-medium flex items-center gap-1 hover:text-brand-dark">
    Lihat Semua <ChevronRight className="w-4 h-4" />
  </button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
  {topRow.map(pkg => (
    <SmallPackageCard key={pkg.id} pkg={pkg} onClick={() => navigate(`/produk/${pkg.id}`)} />
  ))}
</div>
```

---

## 10. Komponen SmallPackageCard

Digunakan di: Produk Populer, Filter Grid, Produk Lainnya

```tsx
function SmallPackageCard({ pkg, onClick }) {
  return (
    <div onClick={onClick}
      className="relative overflow-hidden rounded-xl cursor-pointer group
        bg-white border border-gray-100 shadow-sm hover:shadow-md transition-shadow">
      
      {/* Gambar */}
      <div className="relative h-32">
        <img src={pkg.image} alt={pkg.title}
          className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
        
        {/* Badge */}
        {pkg.badge && (
          <span className={`absolute top-2 left-2 px-2 py-0.5 text-[10px] font-bold rounded-full
            ${pkg.badge === "Mendesak" ? "bg-red-500 text-white"
            : pkg.badge === "Baru"     ? "bg-brand-accent text-brand-dark"
            :                            "bg-brand text-white"}`}>
            {pkg.badge}
          </span>
        )}
      </div>
      
      {/* Body */}
      <div className="p-3">
        <span className="text-[10px] text-gray-400 uppercase tracking-wider">{pkg.category}</span>
        <h4 className="text-sm font-semibold text-gray-800 line-clamp-2 mb-1.5 min-h-[2.5rem]">
          {pkg.title}
        </h4>
        <div className="flex items-center justify-between">
          <div>
            <span className="text-sm font-bold text-brand">{formatCurrency(pkg.price_per_unit)}</span>
            <span className="text-[10px] text-gray-400 ml-1">/{pkg.unit}</span>
          </div>
          <button className="w-7 h-7 bg-brand-50 rounded-full flex items-center justify-center hover:bg-brand-100 transition-colors">
            <ShoppingBag className="w-3.5 h-3.5 text-brand" />
          </button>
        </div>
      </div>
    </div>
  );
}
```

**Badge colors:**
- `"Mendesak"` → `bg-red-500 text-white`
- `"Baru"` → `bg-brand-accent text-brand-dark`
- lainnya → `bg-brand text-white`

---

## 11. Seksi D — Bento Layout (Featured + Event)

```
px-4 md:px-8 mb-6
grid grid-cols-1 md:grid-cols-2 gap-4
```

### Kiri — BentoPackageCard (size="large")

```tsx
function BentoPackageCard({ pkg, size = "normal", onClick }) {
  const height = size === "large" ? "h-64 md:h-80" : "h-44 md:h-52";
  return (
    <div onClick={onClick}
      className={`relative ${height} rounded-2xl overflow-hidden cursor-pointer group shadow-md`}>
      
      {/* Gambar dengan zoom hover */}
      <img src={pkg.image} alt={pkg.title}
        className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
      
      {/* Overlay gelap dari bawah */}
      <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
      
      {/* Teks di bawah */}
      <div className="absolute bottom-0 left-0 right-0 p-4 text-white">
        {pkg.category && (
          <span className="text-[10px] uppercase tracking-wider text-white/80 font-semibold">
            {pkg.category}
          </span>
        )}
        <h3 className="text-lg font-bold">{pkg.title}</h3>
        <p className="text-xs text-white/80 line-clamp-2 mb-1">{pkg.description}</p>
        <p className="text-sm font-bold text-brand-accent">
          {formatCurrency(pkg.price_per_unit)}
          <span className="text-xs font-normal text-white/60"> /{pkg.unit}</span>
        </p>
      </div>
    </div>
  );
}
```

**Digunakan dengan:**
```tsx
{featuredPkg && (
  <BentoPackageCard
    pkg={featuredPkg}
    size="large"
    onClick={() => navigate(`/produk/${featuredPkg.id}`)}
  />
)}
```

### Kanan — Event Carousel (image only)

```tsx
{events.length > 0 && (
  <div className="relative overflow-hidden rounded-2xl h-64 md:h-80">
    <div className="flex transition-transform duration-500 ease-out h-full"
      style={{ transform: `translateX(-${currentEvent * 100}%)` }}>
      {events.map((event) => (
        <div key={event.id} className="min-w-full h-full">
          {event.link_url ? (
            <a href={event.link_url}
              target={event.link_url.startsWith('http') ? '_blank' : '_self'}
              rel="noopener noreferrer" className="block h-full">
              <img src={event.image} alt={event.title} className="w-full h-full object-cover" />
            </a>
          ) : (
            <img src={event.image} alt={event.title} className="w-full h-full object-cover" />
          )}
        </div>
      ))}
    </div>
    {/* Dots */}
    {events.length > 1 && (
      <div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2">
        {events.map((_, index) => (
          <button key={index} onClick={() => setCurrentEvent(index)}
            className={`h-2 rounded-full transition-all duration-300
              ${index === currentEvent ? 'bg-white w-5' : 'bg-white/50 w-2'}`} />
        ))}
      </div>
    )}
  </div>
)}
```

**Auto-advance event:** 5 detik, `setInterval`

**Supabase tabel `events`:**
| Field | Tipe | Keterangan |
|-------|------|-----------|
| `title` | text | Judul event |
| `subtitle` | text | Sub-judul |
| `description` | text | Deskripsi |
| `image` | text | URL gambar |
| [date](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/data/programs.ts#19-27) | date | Tanggal event |
| `color` | text | Warna (opsional) |
| `button_text` | text | Label CTA |
| `link_url` | text | URL redirect saat klik (opsional) |
| `sort_order` | int | Urutan |
| `is_active` | bool | Toggle |

---

## 12. Seksi E — Produk Baru (Infinity Carousel)

```
px-4 md:px-8 mb-6
```

Komponen [ProdukBaruCarousel](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/ProgramList.tsx#11-81) — kode identik dengan [InfinityCarousel](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/Home.tsx#37-141) di Beranda:

```tsx
<h3 className="text-base font-semibold text-gray-800 mb-4">Produk Baru</h3>
<ProdukBaruCarousel items={produkBaruItems} />
```

**Teknik infinite sama:** clone item pertama & terakhir + reset tanpa animasi saat melewati batas.

**Tampilan slide identik dengan Highlights di Beranda:**
- Layout 2 kolom: teks kiri (flex-1) + gambar kanan (w-[45%])
- Tag kecil rounded-full di atas judul
- Background gradient dari `bg_color` field
- Overlay gambar kiri: `bg-gradient-to-l from-transparent to-white/30`
- Tombol CTA merah brand

**Supabase tabel `produk_baru`:**
| Field | Tipe | Keterangan |
|-------|------|-----------|
| `tag` | text | Label kecil (mis: `PRODUK BARU`, `TERLARIS`) |
| `title` | text | Judul slide |
| `subtitle` | text | Deskripsi singkat |
| `button_text` | text | Label CTA |
| `image` | text | URL gambar |
| `bg_color` | text | Tailwind gradient (mis: `from-sky-100 via-sky-50 to-white`) |
| `sort_order` | int | Urutan |
| `is_active` | bool | Toggle |

---

## 13. Seksi F — Produk Lainnya (Horizontal Scroll)

```
mb-6
Data: filteredProducts.slice(5) — produk ke-6 dst
```

```tsx
<div className="mb-6">
  <div className="px-4 md:px-8 flex items-center justify-between mb-4">
    <h3 className="text-base font-semibold text-gray-800">Produk Lainnya</h3>
    <button onClick={() => navigate('/donasi')}
      className="text-sm text-brand font-medium flex items-center gap-1 hover:text-brand-dark">
      Lihat Semua <ChevronRight className="w-4 h-4" />
    </button>
  </div>
  <div className="flex gap-3 overflow-x-auto pb-2 px-4 md:px-8 hide-scrollbar [&>*]:min-w-[200px] [&>*]:flex-shrink-0">
    {scrollProducts.map(pkg => (
      <SmallPackageCard key={pkg.id} pkg={pkg} onClick={() => navigate(`/produk/${pkg.id}`)} />
    ))}
  </div>
</div>
```

**Selector Tailwind:** `[&>*]:min-w-[200px] [&>*]:flex-shrink-0` — memastikan semua child card tidak menyusut.

---

## 14. Data Model — Tabel `product_packages`

| Field | Tipe | Keterangan |
|-------|------|-----------|
| `id` | uuid | Primary key |
| `title` | text | Nama produk |
| `category` | text | Kategori (Sosial, Dakwah, Qurban, dll) |
| `image` | text | URL gambar utama |
| `gallery` | text[] | Array URL gambar tambahan |
| `price_per_unit` | numeric | Harga per satuan |
| `unit` | text | Satuan (paket, box, mushaf, ekor, dll) |
| `min_qty` | int | Minimum pemesanan |
| `description` | text | Deskripsi produk |
| `badge` | text | Label badge (Terlaris / Baru / Mendesak / null) |
| `is_active` | bool | Toggle aktif |
| `created_at` | timestamptz | Waktu dibuat |

**Contoh data:**
```ts
{
  id: "p1",
  title: "Paket Sembako Keluarga",
  category: "Sosial",
  image: "https://...",
  price_per_unit: 150000,
  unit: "paket",
  min_qty: 1,
  description: "Berisi beras 5kg, minyak goreng, gula, teh...",
  badge: "Terlaris",
  is_active: true
}
```

---

## 15. Utility — formatCurrency

```ts
export function formatCurrency(amount: number): string {
  return new Intl.NumberFormat('id-ID', {
    style: 'currency', currency: 'IDR', minimumFractionDigits: 0,
  }).format(amount);
}
// 150000 → "Rp 150.000"
// 2500000 → "Rp 2.500.000"
```

---

## 16. Peta Visual Komponen

```
/produk → ProgramList.tsx
│
├── [Filter] Category Chips (horizontal scroll)
│
├── [isFiltering=true]
│   └── Grid SmallPackageCard (cols-2 / cols-4)
│
└── [isFiltering=false]
    ├── Hero Banner Carousel (h-48/h-64, auto 5s, dots)
    ├── Produk Populer
    │   └── Grid 4× SmallPackageCard
    ├── Bento Layout (grid cols-1 / cols-2)
    │   ├── Kiri: BentoPackageCard size="large" (h-64/h-80)
    │   └── Kanan: Event Carousel (image only, auto 5s, dots)
    ├── Produk Baru
    │   └── ProdukBaruCarousel (Infinity, 4s, dots)
    └── Produk Lainnya
        └── Horizontal Scroll SmallPackageCard (min-w-[200px])
```

---

## 17. Perbedaan Beranda vs Produk

| Aspek | Beranda (`/`) | Produk (`/produk`) |
|-------|-------------|-------------------|
| Data utama | `programs` (donasi) | `product_packages` |
| Metric | `collected / target` | `price_per_unit / unit` |
| Card utama | [ProgramCard](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/Home.tsx#142-182) (progress bar) | [SmallPackageCard](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/ProgramList.tsx#99-131) (harga + ShoppingBag) |
| Filter | Via URL `?akad=` / `?category=` | State lokal `selectedCategory` + `?q=` |
| Banner overlay | opacity-90 | opacity-60 |
| Tombol banner | `bg-white/20 backdrop-blur` | `bg-white text-brand rounded-full` |
| Extra seksi | Doa Orang Baik, Tentang Kami | Bento (featured + events) |
| Infinity carousel | `highlights` | `produk_baru` |

---

## 18. Checklist Replikasi Produk

- [ ] Brand colors dan CSS utilities (identik dengan Beranda)
- [ ] Komponen [SmallPackageCard](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/ProgramList.tsx#99-131) dengan badge 3 warna
- [ ] Komponen [BentoPackageCard](file:///d:/Build/Personal%20Branding/Project/Klien/4.%20Hayfala/Kejayaan/src/pages/ProgramList.tsx#82-98) (size normal/large + gradient overlay bawah)
- [ ] Category Filter Chips (horizontal scroll, toggle aktif/inaktif)
- [ ] Mode filter: grid 2×2/4×4, tombol reset filter
- [ ] Hero Banner Carousel (5 detik, opacity-60, tombol putih solid)
- [ ] Produk Populer grid `cols-2 / cols-4`
- [ ] Bento layout `cols-1 / cols-2`
- [ ] Event Carousel (image only, auto 5 detik, klik ke `link_url`)
- [ ] ProdukBaruCarousel (clone trick infinite, 4 detik)
- [ ] Produk Lainnya horizontal scroll (`[&>*]:min-w-[200px]`)
- [ ] Tabel Supabase: `product_packages, events, produk_baru`
- [ ] Hooks: `useProducts`, `useEvents`, `useProdukBaru`
- [ ] formatCurrency utility
