K

Command Palette

Search for a command to run...

Daftar

title: "Toko Kue: Halaman Daftar Produk" # Judul Pendek

seotitle: "Studi Kasus Toko Kue (Next.js+TS): Menampilkan Daftar Produk dengan Server Component & Fetch" # Judul panjang deskriptif

description: "Saatnya menampilkan produk kue! Pelajari cara membuat halaman utama Toko Kue kita di Next.js App Router, mengambil data dari API Route menggunakan Server Component dan fetch, lalu merendernya sebagai daftar kartu produk."

order: 7 # Setelah Studi Kasus Toko Kue: API Produk

quiz: "./kuis/nextjs-07-studi-kasus-daftar-produk-kuis.yml" # Path ke file kuis


Etalase Toko Kue Dibuka: Nampilin Produk Pake Server Component & Fetch!

Udah punya "dapur" API yang siap nyajiin data produk kue kita? Mantap! Sekarang, saatnya kita "nge-display" semua kue enak itu di "etalase" utama toko kita, yaitu halaman beranda.

Kita bakal ngoprek file src/app/page.tsx (yang jadi halaman utama / di App Router) buat:

  1. Ngambil (fetch) data semua kue dari API endpoint /api/kue yang udah kita bikin.

  2. Karena page.tsx di App Router itu Server Component secara default, kita bisa ngelakuin fetch data ini langsung di server pake async/await!

  3. Nampilin data kue itu jadi daftar kartu produk yang menarik.

Langkah 1: Menyiapkan Komponen Kartu Produk (kartu-kue.tsx)

Sebelum kita nampilin banyak kue, kita bikin dulu "cetakan" buat nampilin satu item kue. Ini bakal jadi komponen React reusable kita.

  1. Di dalam folder src/, bikin folder baru components (kalau belum ada).

  2. Di dalam src/components/, bikin file baru kartu-kue.tsx.

File src/components/kartu-kue.tsx:

tsx
 
// src/components/kartu-kue.tsx
import React from "react";
import Link from "next/link"; // Buat link ke halaman detail
import Image from "next/image"; // Buat optimasi gambar Next.js
import { Kue } from "@/types/kue"; // Impor tipe Kue kita
// Definisikan tipe untuk props yang diterima komponen ini
 
interface KartuKueProps {
  kue: Kue; // Komponen ini nerima satu objek kue
}
 
export default function KartuKue({ kue }: KartuKueProps) {
  return (
    <Link
      href={`/kue/${kue.id}`}
      className="block group border border-gray-200 rounded-lg shadow-sm hover:shadow-lg transition-shadow duration-300 bg-white overflow-hidden"
    >
      <div className="relative w-full h-48">
        <Image
          src={kue.gambarUrl} // Pastikan path gambar ini benar & ada di folder public
          alt={kue.nama}
          fill // Mengisi parent div, butuh parent dengan position relative dan ukuran
          style={{ objectFit: "cover" }} // Sama kayak object-fit: cover di CSS
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" // Bantuan untuk optimasi gambar responsif
        />
      </div>
 
      <div className="p-4">
        <h3 className="text-lg font-semibold text-gray-800 group-hover:text-pink-600 transition-colors">
          {kue.nama}
        </h3>
 
        <p className="text-sm text-gray-600 mt-1 truncate">
          {kue.deskripsiSingkat}
        </p>
 
        <p className="text-lg font-bold text-pink-500 mt-2">
          Rp {kue.harga.toLocaleString("id-ID")}
        </p>
 
        <button className="mt-3 w-full bg-pink-500 hover:bg-pink-600 text-white py-2 px-4 rounded-md transition-colors text-sm">
          Lihat Detail / Tambah ke Keranjang
        </button>
      </div>
    </Link>
  );
}
 
 

Detail kartu-kue.tsx:

  • Props: Dia nerima satu objek kue yang tipenya Kue (dari interface yang kita buat).

  • <Link>: Seluruh kartu dibungkus <Link> dari next/link biar bisa diklik buat pindah ke halaman detail produk. Path-nya dinamis menggunakan href={\`/kue/${kue.id}\`}.

  • <Image>: Kita pake komponen Image dari next/image buat nampilin gambar kue.

  • fill: Bikin gambar ngisi penuh parent div-nya (yang udah kita kasih h-48 dan position: relative).

  • style={{ objectFit: 'cover' }}: Biar gambarnya nge-cover area tanpa distorsi.

  • sizes: Atribut buat ngasih hint ke browser soal ukuran gambar di berbagai viewport, ngebantu optimasi.

  • PENTING: Pastikan gambar-gambar kue (sesuai gambarUrl di daftar-kue.json) udah ada di folder public/images/kue/ atau path yang sesuai.

  • Styling: Kita pake utility class Tailwind CSS buat ngasih style ke kartu, judul, deskripsi, harga, dan tombol.

  • group: Dikasih ke <Link> paling luar.

  • group-hover:text-pink-600: Warna judul berubah pas seluruh kartu di-hover.

  • truncate: Kalau deskripsi singkatnya kepanjangan, dia bakal nampilin "..." di akhir.

  • kue.harga.toLocaleString('id-ID'): Buat nampilin harga dalam format Rupiah yang enak dibaca.

Langkah 2: Mengambil dan Menampilkan Daftar Produk di Halaman Utama

Sekarang, kita modifikasi src/app/page.tsx buat ngambil data dari API /api/kue dan nampilinnya pake komponen KartuKue.

File src/app/page.tsx:

tsx
// src/app/page.tsx
 
import React from "react";
import Link from "next/link";
import { Kue } from "@/types/kue"; // Impor tipe Kue
import KartuKue from "@/components/kartu-kue"; // Impor komponen KartuKue
 
async function getDaftarKue(): Promise<Kue[]> {
  const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
 
  const response = await fetch(`${baseUrl}/api/kue`, {
    cache: "force-cache", // Atau 'no-store' jika mau selalu data baru.
  });
 
  if (!response.ok) {
    console.error(
      "Gagal mengambil data kue:",
      response.status,
      response.statusText
    );
 
    // throw new Error('Gagal mengambil data produk kue');
 
    return []; // Kembalikan array kosong jika gagal
  }
 
  return response.json(); // Otomatis di-parse jadi array objek Kue (jika API-nya bener)
}
 
export default async function HalamanUtama() {
  const daftarKue = await getDaftarKue();
 
  return (
    <div>
      <h1 className="text-3xl font-bold text-center my-8 text-pink-700">
        Selamat Datang di Toko Kue Mahir.dev!
      </h1>
 
      {daftarKue.length === 0 ? (
        <p className="text-center text-gray-500">
          Ups, sepertinya belum ada kue yang dijual saat ini. Coba lagi nanti
          ya!
        </p>
      ) : (
        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
          {daftarKue.map((kue) => (
            <KartuKue key={kue.id} kue={kue} />
          ))}
        </div>
      )}
 
      {/* Contoh link ke halaman lain (misal, keranjang) */}
 
      <div className="text-center mt-10">
        <Link
          href="/keranjang"
          className="bg-green-500 hover:bg-green-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors"
        >
          Lihat Keranjang Saya
        </Link>
      </div>
    </div>
  );
}
 

Detail src/app/page.tsx:

  1. async function getDaftarKue(): Promise<Kue[]>:
  • Kita bikin fungsi async buat ngambil data. Dia ngembaliin Promise<Kue[]>, yang artinya dia bakal ngasilin array dari objek Kue nantinya.
ts
await fetch(\`${baseUrl}/api/kue\`, {  cache: 'force-cache'  })`
  • Kita fetch ke API endpoint /api/kue yang udah kita bikin.

  • baseUrl: Untuk memastikan URL benar saat development (Server Component fetch ke API route di project yang sama). process.env.NEXT_PUBLIC_BASE_URL bisa diset di file .env.local. Jika tidak ada, default ke http://localhost:3000.

  • cache: 'force-cache': Ini penting di Next.js 15+! Karena fetch di Server Component tidak lagi di-cache secara default. Kalau data kuenya statis, kita suruh Next.js buat nge-cache hasilnya. Opsi lain: 'no-store' (selalu ambil baru), atau atur export const fetchCache = 'default-cache' di atas komponen ini.

  • Error handling simpel: Kalau response gak ok (misal, 404 atau 500), kita log error dan balikin array kosong (atau bisa juga throw new Error).

  • response.json(): Ngubah response body jadi objek/array JavaScript.

  1. export default async function HalamanUtama():
  • Komponen page.tsx kita jadi async function karena dia perlu await hasil dari getDaftarKue(). Ini ciri khas Server Component di App Router!

  • const daftarKue = await getDaftarKue();: Kita panggil fungsi fetch dan tunggu hasilnya.

  1. Conditional Rendering:
  • Kalau daftarKue.length === 0 (misal, API error atau datanya emang kosong), kita tampilin pesan.

  • Kalau ada data, kita pake div dengan class grid (Tailwind CSS Grid) buat nampilin kartu-kartu kue.

  • grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4: Ini ngatur jumlah kolom secara responsif. Di layar paling kecil (mobile) 1 kolom, di sm ke atas 2 kolom, di md ke atas 3 kolom, dst.

  • gap-6: Jarak antar kartu.

  • daftarKue.map((kue) => (<KartuKue key={kue.id} kue={kue} />)): Kita nge-map array daftarKue, dan buat tiap objek kue, kita render komponen <KartuKue /> sambil ngasih kue.id sebagai key (WAJIB!) dan seluruh objek kue sebagai prop kue.

  1. Link ke Keranjang: Contoh penggunaan <Link> buat navigasi.

Jangan Lupa Styling Global dan Font (Jika Perlu)

Pastikan src/app/globals.css (atau src/index.css yang diimpor di layout.tsx) udah punya style dasar atau impor font yang kamu mau. Kalau kamu pilih Tailwind CSS pas setup create-next-app, globals.css biasanya udah ada isinya directive Tailwind:

File src/app/globals.css (jika pake Tailwind):

css
 
@import "tailwindcss";
 
:root {
  --background: #ffffff;
  --foreground: #171717;
}
 
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}
 
@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}
 
body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}
 
 

Jika tidak menggunakan Tailwind, pastikan kamu punya file CSS global yang diimpor di src/app/layout.tsx.

Jalankan dan Lihat Hasilnya!

Simpen semua file, pastiin dev server (npm run dev) jalan, terus buka http://localhost:3000 di browser. Harusnya kamu udah liat daftar produk kue kita tampil dengan cantik! Coba ubah ukuran browser buat liat efek responsif dari gridnya.

Toko Kue

Keren! Kita udah berhasil nampilin data dinamis dari API kita sendiri pake Server Component di Next.js. Ini adalah salah satu kekuatan utama App Router.

Di bagian berikutnya, kita bakal bikin halaman detail produk, jadi pas salah satu kartu kue diklik, kita bisa liat info lengkapnya. Ini bakal ngelibatin Dynamic Routes!