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:
-
Ngambil (fetch) data semua kue dari API endpoint
/api/kue
yang udah kita bikin. -
Karena
page.tsx
di App Router itu Server Component secara default, kita bisa ngelakuin fetch data ini langsung di server pakeasync/await
! -
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.
-
Di dalam folder
src/
, bikin folder barucomponents
(kalau belum ada). -
Di dalam
src/components/
, bikin file barukartu-kue.tsx
.
File src/components/kartu-kue.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 tipenyaKue
(dariinterface
yang kita buat). -
<Link>
: Seluruh kartu dibungkus<Link>
darinext/link
biar bisa diklik buat pindah ke halaman detail produk. Path-nya dinamis menggunakanhref={\`/kue/${kue.id}\`}
. -
<Image>
: Kita pake komponenImage
darinext/image
buat nampilin gambar kue. -
fill
: Bikin gambar ngisi penuh parentdiv
-nya (yang udah kita kasihh-48
danposition: 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
didaftar-kue.json
) udah ada di folderpublic/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
:
// 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
:
async function getDaftarKue(): Promise<Kue[]>
:
- Kita bikin fungsi
async
buat ngambil data. Dia ngembaliinPromise<Kue[]>
, yang artinya dia bakal ngasilin array dari objekKue
nantinya.
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 kehttp://localhost:3000
. -
cache: 'force-cache'
: Ini penting di Next.js 15+! Karenafetch
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 aturexport 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 jugathrow new Error
). -
response.json()
: Ngubah response body jadi objek/array JavaScript.
export default async function HalamanUtama()
:
-
Komponen
page.tsx
kita jadiasync function
karena dia perluawait
hasil darigetDaftarKue()
. Ini ciri khas Server Component di App Router! -
const daftarKue = await getDaftarKue();
: Kita panggil fungsi fetch dan tunggu hasilnya.
- Conditional Rendering:
-
Kalau
daftarKue.length === 0
(misal, API error atau datanya emang kosong), kita tampilin pesan. -
Kalau ada data, kita pake
div
dengan classgrid
(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, dism
ke atas 2 kolom, dimd
ke atas 3 kolom, dst. -
gap-6
: Jarak antar kartu. -
daftarKue.map((kue) => (<KartuKue key={kue.id} kue={kue} />))
: Kita nge-map arraydaftarKue
, dan buat tiap objekkue
, kita render komponen<KartuKue />
sambil ngasihkue.id
sebagaikey
(WAJIB!) dan seluruh objekkue
sebagai propkue
.
- 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):
@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.
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!