K

Command Palette

Search for a command to run...

Daftar

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.

  1. Di dalam folder src/app/, bikin folder baru namanya kue.
  2. Di dalam folder src/app/kue/, bikin lagi folder baru yang namanya [idKue] (atau [slug], [productId], bebas, tapi [idKue] sesuai sama properti id 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:

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">
        &larr; 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:

  1. interface DetailKuePageProps: Kita definisiin tipe buat props yang diterima halaman ini. Properti params itu objek yang isinya parameter dinamis dari URL. Nama properti di dalam params (idKue) harus sama persis sama nama folder dinamis kita ([idKue]).

  2. 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 objek Kue kalau ketemu, atau null kalau gak ketemu (misal, ID-nya salah).
    • Ada penanganan kalau response.status === 404.
  3. export default async function HalamanDetailKue({ params }: DetailKuePageProps):

    • Komponen Page kita jadi async function lagi.
    • Dia nerima params dari props. Kita ambil idKue dari params.idKue.
    • PENTING SOAL params di Next.js 15+: Seperti disinggung di catatan kode, di versi terbaru Next.js, params (dan searchParams) yang diterima oleh Server Component bisa berupa Promise. Untuk contoh ini, kita asumsikan Next.js sudah me-resolve-nya. Jika saat dijalankan ada error terkait promise, maka params 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.
  4. Penanganan Kue Tidak Ditemukan:

    • Kalau getDetailKue ngembaliin null (artinya kuenya gak ada), kita nampilin pesan "Kue Tidak Ditemukan" dan link buat balik.
    • Alternatifnya, kita bisa manggil fungsi notFound() dari next/navigation yang bakal otomatis ngerender file not-found.tsx standar kita (kalau ada).
  5. Tampilan Detail Produk:

    • Kalau data kuenya ada, kita tampilin semua informasinya: gambar (pake <Image> lagi, dan priority 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.

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):

tsx
// ... (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!

  1. Pastikan dev server (npm run dev) jalan.
  2. Buka halaman utama (http://localhost:3000) yang nampilin daftar kue.
  3. Klik salah satu kartu kue.
  4. 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).
  5. 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)?