Toko Kue: Halaman Detail Produk
Bikin halaman spesifik untuk tiap kue! Pelajari cara membuat dynamic route di Next.js App Router (misal, `/kue/[id]`), mengambil parameter ID, dan fetch data detail produk dari API.
Tiap Kue Punya Cerita: Bikin Halaman Detail Produk Pake Dynamic Route!
Udah bisa nampilin semua kue enak kita di halaman utama? Keren! Sekarang, gimana caranya biar pas salah satu kartu kue diklik, kita bisa pindah ke halaman baru yang nampilin info lebih lengkap soal kue itu aja? Misalnya, deskripsi panjangnya, bahan-bahannya, mungkin gambar yang lebih gede.
Buat ini, kita perlu bikin Dynamic Route (Rute Dinamis). Artinya, kita bikin satu "template" halaman, tapi kontennya bisa berubah-ubah tergantung parameter yang ada di URL. Misalnya, /kue/kue-coklat-klasik-01
bakal nampilin detail kue coklat, sementara /kue/red-velvet-cupcake-04
bakal nampilin detail red velvet.
Langkah 1: Membuat Struktur Folder Dynamic Route
Di Next.js App Router, dynamic route dibuat dengan ngasih nama folder pake kurung siku []
yang diisi sama nama parameternya.
- Di dalam folder
src/app/
, bikin folder baru namanyakue
. - Di dalam folder
src/app/kue/
, bikin lagi folder baru yang namanya[idKue]
(atau[slug]
,[productId]
, bebas, tapi[idKue]
sesuai sama propertiid
di data kue kita). Strukturnya jadi:src/app/kue/[idKue]/
Nama idKue
di dalam kurung siku itu yang bakal jadi nama parameter dinamis kita.
Langkah 2: Membuat Komponen Halaman Detail (page.tsx
)
Sekarang, di dalam folder src/app/kue/[idKue]/
, bikin file baru bernama page.tsx
. Ini yang bakal jadi komponen buat nampilin UI halaman detail produk.
File src/app/kue/[idKue]/page.tsx
:
// src/app/kue/[idKue]/page.tsx
import React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Kue } from '@/types/kue'; // Impor tipe Kue
// import tombol-tambah-keranjang from '@/components/tombol-tambah-keranjang'; // Akan kita buat nanti
// Tipe untuk props yang diterima halaman ini (termasuk params dinamis)
interface DetailKuePageProps {
params: {
idKue: string; // 'idKue' harus sama dengan nama folder dinamis '[idKue]'
};
// searchParams?: { [key: string]: string | string[] | undefined }; // Jika butuh query params
}
// Fungsi untuk mengambil data satu kue berdasarkan ID dari API Route kita
// Ini adalah Server Component, jadi bisa langsung async/await fetch!
async function getDetailKue(id: string): Promise<Kue | null> {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
const response = await fetch(`${baseUrl}/api/kue/${id}`, {
cache: 'force-cache' // Atau 'no-store', atau atur di segment config
});
if (!response.ok) {
// Kalau 404 (gak ketemu) atau error lain, kita bisa return null atau throw error
// Untuk contoh ini, kita return null biar bisa ditangani di komponen Page
if (response.status === 404) {
return null;
}
// Untuk error lain, mungkin lebih baik throw error
// throw new Error(`Gagal mengambil data kue ID: ${id}`);
console.error("Gagal mengambil detail kue:", response.status, response.statusText);
return null;
}
return response.json();
}
// Komponen Page untuk halaman detail kue
// Ingat, di Next.js 15+, akses ke params di Server Component bisa jadi Promise
// Untuk konsistensi dengan contoh sebelumnya, kita buat fungsi async dan await params
export default async function HalamanDetailKue({ params }: DetailKuePageProps) {
// Di Next.js 15+, jika `params` adalah Promise, Anda perlu `await`
// const resolvedParams = await params; // Baris ini mungkin tidak perlu jika params sudah di-resolve oleh Next.js
// const idKue = resolvedParams.idKue;
// Namun, seringkali Next.js sudah otomatis me-resolve-nya untuk Server Component dasar.
// Kita akan coba akses langsung, jika error, baru di-await.
// Untuk contoh yang lebih robust dengan typing Promise di props:
// async function HalamanDetailKue({ params: paramsPromise }: { params: Promise<{ idKue: string }> }) {
// const params = await paramsPromise;
// const idKue = params.idKue;
const idKue = params.idKue; // Langsung akses dari props.params
const kue = await getDetailKue(idKue);
if (!kue) {
// Bisa juga pake fungsi notFound() dari next/navigation buat nampilin halaman not-found.tsx standar
// import { notFound } from 'next/navigation';
// notFound();
return (
<div className="text-center py-10">
<h1 className="text-2xl font-bold text-red-600">Kue Tidak Ditemukan!</h1>
<p className="mt-4">Maaf, kue dengan ID "{idKue}" tidak dapat kami temukan.</p>
<Link href="/" className="mt-6 inline-block bg-blue-500 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded">
Kembali ke Daftar Kue
</Link>
</div>
);
}
return (
<div className="container mx-auto p-4 md:p-8">
<Link href="/" className="text-blue-600 hover:underline mb-6 inline-block">
← Kembali ke Semua Kue
</Link>
<article className="bg-white shadow-xl rounded-lg overflow-hidden md:flex">
<div className="md:w-1/2 relative h-64 md:h-auto"> {/* Butuh tinggi untuk 'fill' atau set rasio aspek */}
<Image
src={kue.gambarUrl}
alt={kue.nama}
fill
style={{ objectFit: 'cover' }}
sizes="(max-width: 768px) 100vw, 50vw"
priority // Penting untuk gambar utama di atas lipatan (Above The Fold)
/>
</div>
<div className="md:w-1/2 p-6 md:p-8">
<h1 className="text-3xl md:text-4xl font-bold text-gray-800 mb-3">
{kue.nama}
</h1>
<p className="text-sm text-gray-500 mb-4">Kategori: {kue.kategori}</p>
<p className="text-2xl font-semibold text-pink-600 mb-6">
Rp {kue.harga.toLocaleString('id-ID')}
</p>
<h2 className="text-xl font-semibold text-gray-700 mb-2">Deskripsi Lengkap:</h2>
<p className="text-gray-600 leading-relaxed mb-6">
{kue.deskripsiLengkap}
</p>
{kue.bahanUtama && kue.bahanUtama.length > 0 && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-700 mb-1">Bahan Utama:</h3>
<ul className="list-disc list-inside text-gray-600">
{kue.bahanUtama.map((bahan, index) => (
<li key={index}>{bahan}</li>
))}
</ul>
</div>
)}
{kue.rating && (
<p className="text-yellow-500 font-semibold mb-6">
Rating: {'⭐'.repeat(Math.floor(kue.rating))} ({kue.rating.toFixed(1)})
</p>
)}
{/* Tombol Tambah ke Keranjang akan kita buat sebagai Client Component nanti */}
<button
className="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-lg transition-colors text-lg">
Tambah ke Keranjang
</button>
{/* <tombol-tambah-keranjang kue={kue} /> */}
</div>
</article>
</div>
);
}
Bedah src/app/kue/[idKue]/page.tsx
:
-
interface DetailKuePageProps
: Kita definisiin tipe buat props yang diterima halaman ini. Propertiparams
itu objek yang isinya parameter dinamis dari URL. Nama properti di dalamparams
(idKue
) harus sama persis sama nama folder dinamis kita ([idKue]
). -
async function getDetailKue(id: string): Promise<Kue | null>
:- Fungsi
async
baru buat ngambil data satu kue spesifik dari API endpoint/api/kue/${id}
. - Dia nerima
id
kue sebagai string. - Dia ngembaliin
Promise<Kue | null>
, artinya bisa objekKue
kalau ketemu, ataunull
kalau gak ketemu (misal, ID-nya salah). - Ada penanganan kalau
response.status === 404
.
- Fungsi
-
export default async function HalamanDetailKue({ params }: DetailKuePageProps)
:- Komponen Page kita jadi
async function
lagi. - Dia nerima
params
dari props. Kita ambilidKue
dariparams.idKue
. - PENTING SOAL
params
di Next.js 15+: Seperti disinggung di catatan kode, di versi terbaru Next.js,params
(dansearchParams
) yang diterima oleh Server Component bisa berupaPromise
. Untuk contoh ini, kita asumsikan Next.js sudah me-resolve-nya. Jika saat dijalankan ada error terkait promise, makaparams
perlu di-await
dulu:const resolvedParams = await params; const idKue = resolvedParams.idKue;
. Namun, untuk kasus akses langsung di props komponen Page seperti ini, Next.js seringkali sudah menanganinya. const kue = await getDetailKue(idKue);
: Kita panggil fungsi fetch data dengan ID yang didapet.
- Komponen Page kita jadi
-
Penanganan Kue Tidak Ditemukan:
- Kalau
getDetailKue
ngembaliinnull
(artinya kuenya gak ada), kita nampilin pesan "Kue Tidak Ditemukan" dan link buat balik. - Alternatifnya, kita bisa manggil fungsi
notFound()
darinext/navigation
yang bakal otomatis ngerender filenot-found.tsx
standar kita (kalau ada).
- Kalau
-
Tampilan Detail Produk:
- Kalau data kuenya ada, kita tampilin semua informasinya: gambar (pake
<Image>
lagi, danpriority
karena ini gambar utama), nama, kategori, harga, deskripsi lengkap, bahan utama (kalau ada, dirender pake.map()
), rating (kalau ada). - Tombol "Tambah ke Keranjang" masih placeholder, nanti kita bikin jadi komponen interaktif (Client Component) yang bisa ngelola state keranjang.
- Kalau data kuenya ada, kita tampilin semua informasinya: gambar (pake
Langkah 3: Update Link di Kartu Produk
Sekarang, kita perlu update komponen kartu-kue.tsx
kita biar nge-link ke halaman detail yang bener.
File src/components/kartu-kue.tsx
(Bagian <Link>
-nya aja yang diubah):
// ... (import dan interface props sama) ...
export default function KartuKue({ kue }: KartuKueProps) {
return (
// Pastikan href-nya bener ke /kue/[idKue]
<Link href={`/kue/${kue.id}`} className="block group ...">
{/* ... sisa kode KartuKue sama ... */}
</Link>
);
}
Ubah href
di <Link>
jadi {\`/kue/${kue.id}\`}
.
Jalankan dan Tes!
- Pastikan dev server (
npm run dev
) jalan. - Buka halaman utama (
http://localhost:3000
) yang nampilin daftar kue. - Klik salah satu kartu kue.
- Harusnya kamu sekarang dibawa ke halaman detail kue itu, dan URL di browser bakal jadi kayak
http://localhost:3000/kue/kue-coklat-klasik-01
(atau ID kue lain yang kamu klik). - Coba juga masukin URL dengan ID kue yang gak ada, misal
/kue/id-salah
, harusnya kamu liat pesan "Kue Tidak Ditemukan".
Keren! Kamu udah berhasil bikin halaman yang kontennya dinamis tergantung URL, dan ngambil datanya dari API pake Server Component di Next.js. Ini adalah pola yang sangat umum dan powerful.
Di bagian studi kasus berikutnya, kita bakal mulai nambahin interaktivitas: bikin fungsi keranjang belanja pake state, context (mungkin), dan localStorage
!
Kuis Halaman Detail Produk Toko Kue (Next.js+TS)
Pertanyaan 1 dari 5
Di Next.js App Router, bagaimana cara Anda membuat sebuah rute dinamis yang bisa menangkap segmen URL sebagai parameter (misalnya, untuk halaman detail produk dengan ID)?