Modul Pembelajaran Lengkap
Framework React Terbaik

Next.js Dasar

Pelajari framework React modern untuk membangun aplikasi web dengan performa tinggi, SEO friendly, dan pengalaman developer yang luar biasa.

Pengenalan Next.js

Next.js adalah framework React yang dikembangkan oleh Vercel. Framework ini memberikan solusi lengkap untuk membangun aplikasi web modern dengan fitur-fitur seperti:

1. Persiapan Node.js

Install Node.js

Next.js membutuhkan Node.js versi 18.17 atau lebih baru. Cek versi Node.js Anda:

node --version
npm --version

Jika belum terinstall, download di nodejs.org dan pilih versi LTS.

2. Membuat Project Next.js

Create Next.js App

Gunakan perintah berikut untuk membuat project Next.js baru:

npx create-next-app@latest my-next-app

# Navigasi ke folder project
cd my-next-app

# Jalankan development server
npm run dev

3. Menjalankan Project Next.js

Menjalankan Development Server

Setelah project berhasil dibuat, ikuti langkah-langkah berikut untuk menjalankannya:

Masuk ke Folder Project

Buka terminal/command prompt dan navigasi ke folder project yang telah dibuat:

cd my-next-app

Jalankan Development Server

Gunakan perintah berikut untuk menjalankan server development:

npm run dev

Akses Aplikasi di Browser

Buka browser dan akses alamat berikut:

http://localhost:3000

✨ Aplikasi Next.js Anda sekarang sudah berjalan!

Tips Penting
  • Server akan otomatis reload ketika Anda menyimpan perubahan file
  • Tekan Ctrl + C di terminal untuk menghentikan server
  • Pastikan port 3000 tidak digunakan oleh aplikasi lain

4. Struktur Folder Next.js

Struktur Project Lengkap, Ketika membuat file pastikan sesuaikan dengan struktur berikut agar aplikasi ini berjalan dengan baik.
my-next-app/
├── app/                    # App Router (halaman utama)
│   ├── layout.tsx         # Layout utama aplikasi
│   ├── page.tsx           # Halaman home (/)
│   ├── about/
│   │   └── page.tsx       # Halaman about (/about)
│   ├── login/
│   │   └── page.tsx       # Halaman login (/login)
│   ├── register/
│   │   └── page.tsx       # Halaman register (/register)
│   ├── admin/             # Admin panel pages
│   │   ├── products/
│   │   │   └── page.tsx
│   │   └── orders/
│   │       └── page.tsx
│   └── api/               # API Routes
│       ├── auth/
│       │   ├── login/
│       │   │   └── route.ts
│       │   ├── logout/
│       │   │   └── route.ts
│       │   └── me/
│       │       └── route.ts
│       ├── products/
│       │   ├── route.ts
│       │   └── [id]/
│       │       └── route.ts
│       └── transactions/
│           ├── route.ts
│           ├── user/
│           │   └── route.ts
│           └── [id]/
│               └── route.ts
├── components/             # Komponen reusable
│   ├── admin/             # Admin components
│   │   ├── Sidebar.tsx
│   │   └── Navbar.tsx
│   ├── Navbar.tsx
│   ├── Hero.tsx
│   ├── Footer.tsx
│   ├── AboutUs.tsx
│   └── ProductList.tsx
├── public/
├── styles/
├── .env.example           # Environment variables example
├── next.config.js
└── package.json

5. App Routing di Next.js

File-based Routing System

Next.js menggunakan sistem routing berbasis file. Setiap file page.tsx dalam folder app akan menjadi halaman yang dapat diakses.

Struktur Folder URL yang Dihasilkan
app/page.tsx /
app/login/page.tsx /login
app/register/page.tsx /register
app/about/page.tsx /about
app/admin/products/page.tsx /admin/products
app/admin/orders/page.tsx /admin/orders

Environment Variables (.env.example)

di root direktori(sejajar dengan file readme.md) buat file bernama env.example file ini bertujuan untuk membuat variavel global yang dimana nanti kita akan akses di file file yang membutuhkan data tertentu. dalam kasus ini, kita membuat variabel untuk url backend kita yaitu localhost:8000

File .env.example (Root Directory)

File contoh untuk environment variables yang digunakan dalam aplikasi Next.js dan backend Laravel.

NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api
Catatan Environment Variables
  • .env.example - File template untuk environment variables
  • NEXT_PUBLIC_API_BASE_URL - URL backend Laravel (perlu ditambahkan di .env.local)
  • NEXT_PUBLIC_MIDTRANS_CLIENT_KEY - Client key untuk Midtrans payment gateway
  • Copy .env.example ke .env untuk Laravel backend
  • Untuk Next.js, buat file .env.local dan tambahkan variable yang diperlukan

6. Layout di Next.js

Layout di Next.js adalah komponen yang membungkus halaman-halaman aplikasi

Root Layout dan Nested Layout
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Script from "next/script";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Mangs Ipul",
  description: "Marketplace jajanan pedas kesukaanmu.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">
        {children}
        <Script
          src="https://app.sandbox.midtrans.com/snap/snap.js"
          data-client-key={process.env.NEXT_PUBLIC_MIDTRANS_CLIENT_KEY}
          strategy="beforeInteractive"
        />
      </body>
    </html>
  );
}

7. Komponen di Next.js

Komponen di Next.js adalah bagian kecil tampilan (UI) yang bisa dibuat sekali lalu digunakan berulang untuk membangun halaman website dengan lebih rapi dan efisien. misalnya di instagram kamu bisa lihat ada berbagai postingan yang memiliki berbeda konten dan gambar namun memiliki bentuk yang sama misalnya setiap postingan pasti memiliki tampilan kotak dan terdapat tombol like dan komentar

Contoh Komponen Sederhana

<div style="border: 1px solid gray; padding: 16px; width: 300px;">
  <h2>Judul Postingan</h2>
  <p>Ini isi postingannya.</p>

  <button>Like (0)</button>

  <h3>Komentar:</h3>
  <ul>
    <li>Postingannya bagus!</li>
    <li>Saya suka ini</li>
  </ul>
</div>
            

8. Komponen Hero (hero.tsx)

komponen hero adalah komponen yang digunakan untuk menampilkan gambar besar untuk menampilkan kesan visual, silahkan buat folder components di dalam folder App lalu didalamnya buat file bernama Hero.tsx kemudian anda bisa menyalin kode program dibawah dan letakan di file itu

Kode Lengkap Hero.tsx
export default function Hero() {
  return (
    <section className="bg-orange-50 text-gray-800 min-h-[80vh] flex items-center justify-center py-20 overflow-hidden">
      <div className="px-4 max-w-7xl mx-auto flex flex-col-reverse md:flex-row items-center justify-between w-full gap-12 lg:gap-20">
        <div className="w-full md:w-1/2 flex flex-col items-center text-center md:text-left md:items-start">
            <span className="text-orange-600 font-bold tracking-widest uppercase mb-4 inline-block bg-orange-100 px-4 py-1.5 rounded-full text-sm border border-orange-200">Spesial Menu Pedas</span>
            <h1 className="text-5xl lg:text-7xl font-extrabold text-gray-900 mb-6 drop-shadow-sm leading-tight tracking-tight">
              Sensasi Pedas <br/><span className="text-orange-500">Bikin Nagih!</span>
            </h1>
            <p className="text-lg lg:text-2xl text-gray-600 mb-10 max-w-xl font-medium leading-relaxed">
              Cobain aneka jajanan kami mulai dari seblak kering hingga makroni pedas.
            </p>
            <div className="flex flex-col sm:flex-row gap-4 justify-center md:justify-start w-full sm:w-auto">
              <a href="#products" className="bg-orange-500 hover:bg-orange-600 text-white font-extrabold py-4 px-10 rounded-full shadow-xl">Beli Sekarang</a>
              <a href="#about" className="bg-white border-2 border-orange-500 text-orange-500 hover:bg-orange-50 font-extrabold py-4 px-10 rounded-full">Lihat Menu</a>
            </div>
        </div>
        <div className="w-full md:w-1/2 flex justify-center md:justify-end relative">
             <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3/4 h-3/4 bg-orange-200 rounded-full blur-3xl opacity-50 -z-10"></div>
             <img src="https://mangsipul.vercel.app/logo.png" alt="Logo Mangs Ipul" className="w-full max-w-sm md:max-w-md lg:max-w-lg h-auto object-contain hover:scale-105 transition-transform duration-500 drop-shadow-2xl"/>
        </div>
      </div>
    </section>
  );
}

9. Komponen About Us (about.tsx)

Komponen About Us adalah komponen yang digunakan untuk menampilkan informasi tentang profil, tujuan, atau deskripsi singkat suatu website atau bisnis. Didalam folder folder components, buat file bernama AboutUs.tsx. Kemudian Anda bisa menyalin kode program di bawah dan letakkan di file tersebut.

Kode Lengkap AboutUs.tsx
export default function AboutUs() {
  return (
    <section id="about" className="py-24 bg-orange-50">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex flex-col lg:flex-row items-center gap-16">
          <div className="w-full lg:w-1/2 relative">
             <img src="https://mangsipul.vercel.app/logo.png" alt="Tentang Mangs Ipul" className="relative z-10 w-full h-auto object-cover"/>
          </div>
          <div className="w-full lg:w-1/2">
            <h2 className="text-orange-500 font-extrabold tracking-widest uppercase mb-3 inline-block border-b-4 border-orange-500 pb-1">Tentang Kami</h2>
            <h3 className="text-4xl md:text-5xl font-black text-gray-900 mb-6 leading-tight">Kisah Dari Sepiring Kepedasan</h3>
            <p className="text-lg text-gray-600 mb-6 leading-relaxed font-medium">
              Mangs Ipul lahir dari kecintaan kami terhadap jajanan nusantara yang kaya akan rempah dan rasa pedas yang otentik.
            </p>
            <p className="text-lg text-gray-600 mb-10 leading-relaxed font-medium">
              Kami berkomitmen untuk terus menyajikan camilan berkualitas tinggi.
            </p>
            <div className="grid grid-cols-2 gap-6">
               <div className="bg-white p-6 rounded-2xl shadow-sm text-center hover:shadow-md transition">
                  <span className="text-4xl font-black text-orange-500 mb-2">10k+</span>
                  <span className="text-gray-600 font-bold uppercase text-sm">Pelanggan Setia</span>
               </div>
               <div className="bg-white p-6 rounded-2xl shadow-sm text-center hover:shadow-md transition">
                  <span className="text-4xl font-black text-orange-500 mb-2">100%</span>
                  <span className="text-gray-600 font-bold uppercase text-sm">Halal & Higienis</span>
               </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

Komponen Footer adalah komponen yang digunakan untuk menampilkan bagian bawah website yang biasanya berisi informasi tambahan seperti kontak, copyright, dan navigasi. Didalam folder components, buat file bernama Footer.tsx. Kemudian Anda bisa menyalin kode program di bawah dan letakkan di file tersebut

Kode Lengkap Footer.tsx
export default function Footer() {
  return (
    <footer className="bg-orange-500 text-white pt-20 pb-8">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12">
          <div>
            <h3 className="text-3xl font-black mb-6 tracking-wider uppercase drop-shadow-md">Mangs Ipul</h3>
            <p className="text-orange-100 mb-6 leading-relaxed font-medium text-lg">
              Toko jajanan kekinian yang menghadirkan sensasi pedas dan gurih.
            </p>
          </div>
          <div>
             <h4 className="text-xl font-bold mb-6 border-b-2 border-orange-400 pb-2 inline-block">Menu Cepat</h4>
             <ul className="space-y-4 font-semibold text-lg">
               <li><a href="#" className="hover:text-white text-orange-100 transition flex items-center"><span className="text-white mr-2">›</span> Beranda</a></li>
               <li><a href="#products" className="hover:text-white text-orange-100 transition flex items-center"><span className="text-white mr-2">›</span> Produk Jajanan</a></li>
               <li><a href="#about" className="hover:text-white text-orange-100 transition flex items-center"><span className="text-white mr-2">›</span> Tentang Kami</a></li>
             </ul>
          </div>
          <div>
            <h4 className="text-xl font-bold mb-6 border-b-2 border-orange-400 pb-2 inline-block">Kontak</h4>
            <ul className="space-y-4 font-medium text-lg text-orange-100">
               <li className="flex items-start"><span className="mr-3 text-2xl">📍</span> Jl. Kenangan Pedas No. 99, Bandung</li>
               <li className="flex items-center"><span className="mr-3 text-2xl">📞</span> +62 812-3456-7890</li>
               <li className="flex items-center"><span className="mr-3 text-2xl">✉️</span> halo@mangsipul.com</li>
            </ul>
          </div>
        </div>
        <div className="border-t border-orange-600/50 pt-8 text-center text-orange-200 font-bold">
          <p>© {new Date().getFullYear()} MANGS IPUL. HAK CIPTA DILINDUNGI.</p>
        </div>
      </div>
    </footer>
  );
}

11. Product List Component (ProductList.tsx)

Komponen ProductList adalah komponen yang digunakan untuk menampilkan daftar produk pada website, biasanya berisi informasi seperti nama produk, harga, gambar, dan deskripsi singkat. Di dalam folder components, buat file bernama ProductList.tsx. Kemudian Anda bisa menyalin kode program di bawah dan letakkan di file tersebut.

Kode Lengkap ProductList.tsx
"use client";
import { useState, useEffect } from "react";

type Product = {
  id: number;
  name: string;
  description: string;
  price: number;
  stock: number;
};

export default function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        if (Array.isArray(data)) setProducts(data);
        setLoading(false);
      })
      .catch(() => setLoading(false));
  }, []);

  const formatPrice = (price: number) => {
    return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(price);
  };

  return (
    <section id="products" className="py-24 bg-white">
      <div className="max-w-7xl mx-auto px-4">
        <div className="text-center mb-16">
          <h2 className="text-4xl font-extrabold">Jajanan <span className="text-orange-500">Favorit</span></h2>
        </div>
        {loading ? (
          <div className="text-center py-20">Loading...</div>
        ) : (
          <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
            {products.map(p => (
              <div key={p.id} className="border rounded-xl p-6">
                <h3 className="text-xl font-bold">{p.name}</h3>
                <p className="text-gray-500">{p.description}</p>
                <div className="mt-4 flex justify-between">
                  <span className="text-orange-600 font-bold">{formatPrice(p.price)}</span>
                  <button className="bg-orange-500 text-white px-4 py-2 rounded">Beli</button>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </section>
  );
}

Komponen Navbar adalah komponen yang digunakan untuk menampilkan bagian navigasi utama pada website yang biasanya berisi menu seperti beranda, produk, kontak, dan lainnya. Di dalam folder components, buat file bernama Navbar.tsx. Kemudian Anda bisa menyalin kode program di bawah dan letakkan di file tersebut.

Kode Lengkap Navbar.tsx
"use client";
import Link from 'next/link';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';

export default function Navbar() {
  const [user, setUser] = useState<{ fullname?: string } | null>(null);
  const router = useRouter();

  useEffect(() => {
    fetch('/api/auth/me')
      .then(res => res.ok ? res.json() : null)
      .then(data => data?.fullname && setUser(data));
  }, []);

  const handleLogout = async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    setUser(null);
    router.push('/');
  };

  return (
    <nav className="bg-white shadow-md sticky top-0 z-50">
      <div className="max-w-7xl mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          <Link href="/" className="text-2xl font-bold text-orange-600">MANGS IPUL</Link>
          <div className="flex items-center space-x-4">
            <Link href="/" className="text-gray-600 hover:text-orange-600">Beranda</Link>
            <Link href="/#products" className="text-gray-600 hover:text-orange-600">Produk</Link>
            <Link href="/#about" className="text-gray-600 hover:text-orange-600">Tentang Kami</Link>
            {user ? (
              <button onClick={handleLogout} className="text-red-500">Logout</button>
            ) : (
              <>
                <Link href="/login" className="text-orange-500 hover:text-orange-600">Login</Link>
                <Link href="/register" className="bg-orange-500 text-white px-4 py-2 rounded">Register</Link>
              </>
            )}
          </div>
        </div>
      </div>
    </nav>
  );
}

13. Halaman Login (app/login/page.tsx)

sekarang kita akan membuat tampilan login, kamu dapat membuat folder Login didalam folder App, lalu anda bisa membuat file bernama page.tsx dan menempelkan kode program di bawah ini

Kode Lengkap Login Page
"use client";
import Link from "next/link";
import Navbar from "@/components/Navbar";
import { useState } from "react";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    try {
      const res = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password }),
      });
      const data = await res.json();
      if (res.ok && data.token) {
        alert("Login Berhasil!");
        if (data.role === 'admin') {
           router.push("/admin/products");
        } else {
           router.push("/");
        }
      } else {
        alert("Login gagal: " + (data.message || "Kredensial tidak valid"));
      }
    } catch (err) {
      alert("Terjadi kesalahan pada sistem.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-orange-50 flex flex-col">
      <Navbar />
      <main className="flex-1 w-full max-w-6xl mx-auto flex flex-col lg:flex-row items-center justify-between p-6 py-12 lg:py-20 lg:p-12 gap-12">
        <div className="w-full lg:w-1/2 text-center lg:text-left">
           <h1 className="text-4xl lg:text-6xl font-extrabold text-gray-900 mb-6">
             Selamat Datang<br/><span className="text-orange-500">Kembali!</span>
           </h1>
           <p className="text-lg lg:text-xl text-gray-600 font-medium">
             Masuk ke akun Anda dan borong berbagai jajanan super pedas.
           </p>
        </div>
        <div className="w-full lg:w-1/2 flex justify-center">
          <div className="bg-white p-8 md:p-12 rounded-3xl shadow-2xl w-full max-w-md">
            <form onSubmit={handleLogin} className="space-y-6">
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2">Email</label>
                <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full px-4 py-3.5 rounded-xl border border-gray-300" required />
              </div>
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2">Password</label>
                <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-4 py-3.5 rounded-xl border border-gray-300" required />
              </div>
              <button type="submit" disabled={loading} className="w-full bg-orange-500 hover:bg-orange-600 text-white font-extrabold py-4 rounded-xl">
                {loading ? 'Memproses...' : 'Masuk Sekarang'}
              </button>
            </form>
            <div className="mt-8 text-center">
              Belum punya akun?{' '}
              <Link href="/register" className="text-orange-500 font-bold">Daftar Gratis</Link>
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

14. Halaman Register (app/register/page.tsx)

Sekarang kita akan membuat tampilan register, kamu dapat membuat folder Register di dalam folder App, lalu Anda bisa membuat file bernama page.tsx dan menempelkan kode program di bawah ini.

Kode Lengkap Register Page
"use client";
import Link from "next/link";
import Navbar from "@/components/Navbar";
import { useState } from "react";
import { useRouter } from "next/navigation";

export default function RegisterPage() {
  const [formData, setFormData] = useState({
    fullname: '', email: '', no_telp: '', address: '', password: ''
  });
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const handleRegister = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    try {
      const API_URL = (process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api') + '/register';
      const res = await fetch(API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
      });
      const data = await res.json();
      if (res.ok) {
        alert("Pendaftaran berhasil! Silakan masuk.");
        router.push('/login');
      } else {
        alert("Gagal mendaftar: " + (data.message || "Terjadi kesalahan"));
      }
    } catch(err) {
      alert("Terjadi kesalahan pada sistem.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="min-h-screen bg-orange-50 flex flex-col">
      <Navbar />
      <main className="flex-1 w-full max-w-7xl mx-auto flex flex-col lg:flex-row items-center justify-between p-6 py-12 lg:py-20 lg:p-12 gap-12">
        <div className="w-full lg:w-5/12 text-center lg:text-left">
           <h1 className="text-4xl lg:text-6xl font-extrabold text-gray-900 mb-6">
             Mulai Petualangan<br/><span className="text-orange-500">Pedasmu!</span>
           </h1>
           <p className="text-lg lg:text-xl text-gray-600 font-medium">
             Buat akun barumu dan nikmati kemudahan berbelanja.
           </p>
        </div>
        <div className="w-full lg:w-7/12 flex justify-center">
          <div className="bg-white p-8 md:p-10 rounded-3xl shadow-2xl w-full max-w-xl">
            <h2 className="text-2xl font-extrabold text-gray-900 mb-6 text-center lg:text-left">Formulir Pendaftaran</h2>
            <form onSubmit={handleRegister} className="space-y-5">
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2">Nama Lengkap</label>
                <input type="text" value={formData.fullname} onChange={(e) => setFormData({...formData, fullname: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-300" required />
              </div>
              <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
                <div>
                  <label className="block text-sm font-bold text-gray-700 mb-2">Email</label>
                  <input type="email" value={formData.email} onChange={(e) => setFormData({...formData, email: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-300" required />
                </div>
                <div>
                  <label className="block text-sm font-bold text-gray-700 mb-2">No Telepon</label>
                  <input type="tel" value={formData.no_telp} onChange={(e) => setFormData({...formData, no_telp: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-300" required />
                </div>
              </div>
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2">Alamat Pengiriman</label>
                <textarea rows={2} value={formData.address} onChange={(e) => setFormData({...formData, address: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-300" required></textarea>
              </div>
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2">Password</label>
                <input type="password" value={formData.password} onChange={(e) => setFormData({...formData, password: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-300" required />
              </div>
              <button type="submit" disabled={loading} className="w-full bg-orange-500 hover:bg-orange-600 text-white font-extrabold py-4 rounded-xl">
                {loading ? 'Memproses...' : 'Daftar Sekarang'}
              </button>
            </form>
            <div className="mt-6 text-center">
              Sudah punya akun?{' '}
              <Link href="/login" className="text-orange-500 font-bold">Masuk di sini</Link>
            </div>
          </div>
        </div>
      </main>
    </div>
  );
}

15. Struktur Folder Admin Panel

ini struktur folder untuk admin jadi terdapat 2 page yang masing masing memuat 2 komponen yaitu navbar dan sidebar. bisa kita lihat bahwa terdapat tampilan produk dan tampilan pesanan karena kita telah membuat folder products dan orders

Struktur Folder Admin
components/
├── admin/
│   ├── Sidebar.tsx      # Sidebar untuk admin panel
│   └── Navbar.tsx       # Navbar untuk admin panel
└── ...

app/
├── admin/
│   ├── products/
│   │   └── page.tsx     # Halaman admin produk
│   └── orders/
│       └── page.tsx      # Halaman admin pesanan
└── ...

16. Komponen Sidebar Admin (components/admin/Sidebar.tsx)

buat folder admin di folder components lalu tambahkan Sidebar.tsx

Kode Lengkap Sidebar Admin

Sidebar untuk panel admin yang berisi menu navigasi dan tombol logout.

"use client";
import Link from 'next/link';
import { useRouter } from 'next/navigation';

export default function Sidebar() {
  const router = useRouter();

  const handleLogout = async (e: React.MouseEvent) => {
    e.preventDefault();
    if(confirm("Apakah Anda yakin ingin keluar?")) {
       try {
           await fetch('/api/auth/logout', { method: 'POST' });
           router.push('/login');
       } catch (error) {
           console.error("Logout gagal:", error);
           router.push('/login');
       }
    }
  };

  return (
    <aside className="w-64 bg-white border-r border-gray-200 hidden md:flex flex-col h-screen fixed top-0 left-0 z-40">
      <div className="h-16 flex items-center px-6 border-b border-gray-200">
        <Link href="/" className="text-2xl font-extrabold text-orange-600 tracking-wider">MANGS IPUL</Link>
      </div>
      <nav className="flex-1 p-4 space-y-2 overflow-y-auto">
        <p className="px-2 text-xs font-bold text-gray-400 uppercase tracking-widest mb-3 mt-4">Menu Admin</p>
        
        <Link href="/admin/products" className="flex items-center px-4 py-3 text-gray-600 hover:bg-orange-50 hover:text-orange-600 font-medium rounded-xl transition-colors">
          <span className="mr-3 text-xl">📦</span> Produk
        </Link>
        <Link href="/admin/orders" className="flex items-center px-4 py-3 text-gray-600 hover:bg-orange-50 hover:text-orange-600 font-medium rounded-xl transition-colors">
          <span className="mr-3 text-xl">🛒</span> Pesanan
        </Link>
      </nav>
      <div className="p-4 border-t border-gray-200">
         <button onClick={handleLogout} className="w-full flex items-center px-4 py-3 text-red-600 hover:bg-red-50 rounded-xl font-bold transition-colors">
          <span className="mr-3 text-xl">🚪</span> Keluar
        </button>
      </div>
    </aside>
  );
}
Fitur Sidebar Admin
  • Menu Produk dan Pesanan untuk admin
  • Tombol logout dengan konfirmasi
  • Fixed sidebar dengan posisi kiri
  • Hover effect pada menu
  • Responsive (hidden di mobile)

17. Komponen Navbar Admin (components/admin/Navbar.tsx)

buat folder admin di components lalu tambahkan Navbar.tsx

Kode Lengkap Navbar Admin

Navbar untuk panel admin yang menampilkan informasi user dan dropdown profile.

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";

export default function Navbar() {
  const [user, setUser] = useState<{ fullname?: string; email?: string } | null>(null);
  const [dropdownOpen, setDropdownOpen] = useState(false);
  const router = useRouter();

  useEffect(() => {
    fetch('/api/auth/me')
      .then(res => res.json())
      .then(data => {
        if (data && data.fullname) {
           setUser(data);
        }
      })
      .catch(err => console.error(err));
  }, []);

  const handleLogout = async () => {
    if (confirm("Apakah Anda yakin ingin keluar?")) {
      try {
        await fetch('/api/auth/logout', { method: 'POST' });
        router.push('/login');
      } catch (error) {
        console.error("Logout gagal:", error);
      }
    }
  };

  const displayName = user?.fullname || "Admin";

  return (
    <header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 sm:px-6 sticky top-0 z-30">
      <div className="flex items-center">
        <button className="md:hidden mr-4 text-gray-500 hover:text-orange-500">
          <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg>
        </button>
        <span className="text-xl font-extrabold text-orange-600 tracking-wider md:hidden">MANGS IPUL</span>
        <h2 className="hidden md:block text-gray-800 font-extrabold md:text-lg">Dashboard Admin</h2>
      </div>
      
      <div className="flex items-center space-x-5 relative">
        <button className="text-gray-400 hover:text-orange-500 transition-colors p-2 rounded-full hover:bg-orange-50">
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" /></svg>
        </button>

        <div className="relative">
            <div onClick={() => setDropdownOpen(!dropdownOpen)} className="flex items-center space-x-3 cursor-pointer p-1 rounded-full hover:bg-gray-50 pr-3 transition">
              <img src={`https://ui-avatars.com/api/?name=${encodeURIComponent(displayName)}&background=f97316&color=fff&bold=true`} alt="Admin" className="w-8 h-8 rounded-full shadow-sm" />
              <span className="text-sm font-bold text-gray-700 hidden sm:block truncate max-w-[120px]">{displayName}</span>
              <svg className={`w-4 h-4 text-gray-500 hidden sm:block transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
            </div>

            {dropdownOpen && (
                <div className="absolute right-0 mt-3 w-56 bg-white border border-gray-100 rounded-xl shadow-2xl py-2 z-50">
                    <div className="px-4 py-3 border-b border-gray-50 mb-1">
                        <p className="text-sm font-bold text-gray-800 truncate">{displayName}</p>
                        <p className="text-xs text-gray-500 truncate mt-0.5">{user?.email || 'Administrator'}</p>
                    </div>
                    <button onClick={handleLogout} className="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 font-bold flex items-center transition-colors">
                        <svg className="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
                        Keluar Akun
                    </button>
                </div>
            )}
        </div>
      </div>
    </header>
  );
}
Fitur Navbar Admin
  • Menampilkan nama dan email admin dari API /api/auth/me
  • Avatar otomatis menggunakan UI Avatars API
  • Dropdown profile dengan tombol logout
  • Notifikasi icon (placeholder)
  • Responsive design (menu mobile button)

18. Struktur Folder API Routes

ini adalah struktur folder api, karena kita pakai laravel maka setiap http request yang kita buat di laravel dan kita akan menggunakannya di Next.Js maka kita harus membuat folder api di dalam folder app untuk mengatur endpointnya. jadi alur ketika user request sesuatu akan memiliki alur seperti ini: user request ke dalam file api dari next js lalu file api itu akan request ke backend sesuai permintaan

Struktur Lengkap Folder API
app/
├── api/
│   ├── auth/
│   │   ├── login/
│   │   │   └── route.ts      # POST /api/auth/login
│   │   ├── logout/
│   │   │   └── route.ts     # POST /api/auth/logout
│   │   └── me/
│   │       └── route.ts        # GET /api/auth/me
│   ├── products/
│   │   ├── route.ts            # GET /api/products, POST /api/products
│   │   └── [id]/
│   │       └── route.ts        # GET /api/products/:id, PUT, DELETE
│   └── transactions/
│       ├── route.ts            # GET /api/transactions, POST /api/transactions
│       ├── user/
│       │   └── route.ts        # GET /api/transactions/user
│       └── [id]/
│           └── route.ts        # PUT /api/transactions/:id
├── admin/
│   ├── products/
│   │   └── page.tsx
│   └── orders/
│       └── page.tsx
└── ...

19. API Authentication (Auth)

buat folder api di dalam folder app lalu buat folder bernama auth lalu didalamnya lagi buat folder bernama login. lalu salin dan tempelkan kode program di bawah.

kode program di bawah ini memuat alur yang dimana dia akan menangkap inputan email dan password di Login/page.tsx dan kemudian mengirimkannya ke backend laravel api yaitu localhost:8000/api/login

API Login (app/api/auth/login/route.ts)

POST /api/auth/login
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

export async function POST(request: Request) {
  const body = await request.json();
  
  const res = await fetch(`${API_BASE}/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
    body: JSON.stringify(body)
  });
  
  const data = await res.json();
  
  if (res.ok && data.token) {
    const userRes = await fetch(`${API_BASE}/user`, {
      method: 'GET',
      headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${data.token}` }
    });

    let role = 'member';
    if (userRes.ok) {
        const userData = await userRes.json();
        role = userData.role || 'member'; 
    }

    const cookieStore = await cookies();
    
    cookieStore.set('auth_token', data.token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      path: '/',
      sameSite: 'lax',
      maxAge: 7 * 24 * 60 * 60
    });

    cookieStore.set('user_role', role, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      path: '/',
      sameSite: 'lax',
      maxAge: 7 * 24 * 60 * 60
    });
    
    return NextResponse.json({ ...data, role }, { status: res.status });
  }
  
  return NextResponse.json(data, { status: res.status });
}

API Logout (app/api/auth/logout/route.ts)

Di folder auth buat folder bernama logout. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan menghapus data autentikasi pengguna (seperti token) dan mengirimkan permintaan ke backend Laravel API yaitu localhost:8000/api/logout untuk mengakhiri sesi pengguna.

POST /api/auth/logout
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

export async function POST() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;

  if (token) {
    await fetch(`${API_BASE}/logout`, {
      method: 'POST',
      headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${token}` }
    });
  }
  
  cookieStore.delete('auth_token');
  cookieStore.delete('user_role');
  return NextResponse.json({ message: 'Success Logout' });
}

API Me (app/api/auth/me/route.ts)

Di folder auth buat folder bernama me. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan mengambil data pengguna yang sedang login berdasarkan token autentikasi, lalu mengirimkan permintaan ke backend Laravel API yaitu localhost:8000/api/user untuk mendapatkan informasi user yang sedang aktif.

GET /api/auth/me
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;

  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const res = await fetch(`${API_BASE}/user`, {
    headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${token}` }
  });

  const data = await res.json();
  return NextResponse.json(data, { status: res.status });
}

20. API Products

Di folder api buat folder bernama products. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan mengambil data daftar produk dari backend Laravel API yaitu localhost:8000/api/products dan menampilkannya pada aplikasi.

API Products List (app/api/products/route.ts)

GET POST /api/products
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

async function getAuthHeaders() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;
  return {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    ...(token && { 'Authorization': `Bearer ${token}` })
  };
}

export async function GET() {
  const res = await fetch(`${API_BASE}/products`, { headers: await getAuthHeaders() });
  const data = await res.json();
  return NextResponse.json(data, { status: res.status });
}

export async function POST(request: Request) {
  const body = await request.json();
  const res = await fetch(`${API_BASE}/products`, {
    method: 'POST',
    headers: await getAuthHeaders(),
    body: JSON.stringify(body)
  });
  
  const text = await res.text();
  try {
     const data = JSON.parse(text);
     return NextResponse.json(data, { status: res.status });
  } catch (err) {
     return NextResponse.json({ message: 'Error processing response', details: text }, { status: res.status });
  }
}

API Product Detail (app/api/products/[id]/route.ts)

Di folder api/products buat folder bernama [id]. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan mengambil data detail produk berdasarkan id dari backend Laravel API yaitu localhost:8000/api/products/{id} dan menampilkannya pada aplikasi.

GET PUT DELETE /api/products/:id
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

async function getAuthHeaders() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;
  return {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    ...(token && { 'Authorization': `Bearer ${token}` })
  };
}

export async function GET(request: Request, context: any) {
  const { id } = await context.params;
  const res = await fetch(`${API_BASE}/products/${id}`, {
    method: 'GET',
    headers: await getAuthHeaders(),
  });
  const data = await res.json();
  return NextResponse.json(data, { status: res.status });
}

export async function PUT(request: Request, context: any) {
  const { id } = await context.params;
  const body = await request.json();
  const res = await fetch(`${API_BASE}/products/${id}`, {
    method: 'PUT',
    headers: await getAuthHeaders(),
    body: JSON.stringify(body)
  });
  const data = await res.json();
  return NextResponse.json(data, { status: res.status });
}

export async function DELETE(request: Request, context: any) {
  const { id } = await context.params;
  const res = await fetch(`${API_BASE}/products/${id}`, {
    method: 'DELETE',
    headers: await getAuthHeaders(),
  });
  const data = await res.json();
  return NextResponse.json(data, { status: res.status });
}

21. API Transactions

API Transactions List (app/api/transactions/route.ts)

Di folder api buat folder bernama transactions. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan mengambil dan mengirim data transaksi ke backend Laravel API yaitu localhost:8000/api/transactions, seperti membuat transaksi baru dan menampilkan daftar transaksi pengguna.

GET POST /api/transactions
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

async function getAuthHeaders() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;
  return {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    ...(token && { 'Authorization': `Bearer ${token}` })
  };
}

export async function GET() {
  const res = await fetch(`${API_BASE}/transactions`, { headers: await getAuthHeaders() });
  const text = await res.text();
  try {
     const data = JSON.parse(text);
     return NextResponse.json(data, { status: res.status });
  } catch(e) {
     return NextResponse.json({ message: 'Error processing response', details: text }, { status: res.status });
  }
}

export async function POST(request: Request) {
  const body = await request.json();
  const res = await fetch(`${API_BASE}/transactions`, {
    method: 'POST',
    headers: await getAuthHeaders(),
    body: JSON.stringify(body)
  });
  
  const text = await res.text();
  try {
     const data = JSON.parse(text);
     return NextResponse.json(data, { status: res.status });
  } catch (err) {
     return NextResponse.json({ message: 'Error processing response', details: text }, { status: res.status });
  }
}

API User Transactions (app/api/transactions/user/route.ts)

Di dalam folder api/transactions buat folder bernama user. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan mengambil data transaksi atau riwayat pembelian milik user yang sedang login dari backend Laravel API yaitu localhost:8000/api/transactions/user dan menampilkannya pada aplikasi.

GET /api/transactions/user
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

export async function GET() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;

  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const res = await fetch(`${API_BASE}/transactions/user`, {
    headers: {
      'Accept': 'application/json',
      'Authorization': `Bearer ${token}`
    }
  });

  const data = await res.json();
  return NextResponse.json(data, { status: res.status });
}

API Update Transaction (app/api/transactions/[id]/route.ts)

Di dalam folder api/transactions buat folder bernama [id]. Lalu salin dan tempelkan kode program di bawah.

Kode program di bawah ini memuat alur yang dimana dia akan mengambil data detail transaksi berdasarkan id dari backend Laravel API yaitu localhost:8000/api/transactions/{id} dan menampilkannya pada aplikasi.

PUT /api/transactions/:id
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api';

async function getAuthHeaders() {
  const cookieStore = await cookies();
  const token = cookieStore.get('auth_token')?.value;
  return {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    ...(token && { 'Authorization': `Bearer ${token}` })
  };
}

export async function PUT(request: Request, context: any) {
  const { id } = await context.params;
  const body = await request.json();
  const res = await fetch(`${API_BASE}/transactions/${id}`, {
    method: 'PUT',
    headers: await getAuthHeaders(),
    body: JSON.stringify(body)
  });
  
  const text = await res.text();
  try {
     const data = JSON.parse(text);
     return NextResponse.json(data, { status: res.status });
  } catch (e) {
     return NextResponse.json({ message: 'Error processing response', details: text }, { status: res.status });
  }
}

22. Layout Admin (app/admin/layout.tsx)

buat folder admin di dalam folder app lalu buat file bernama layout.tsx

Kode Lengkap Layout Admin

Layout khusus untuk semua halaman di dalam folder /admin. Layout ini menyertakan Sidebar dan Navbar Admin.

// app/admin/layout.tsx
import Sidebar from "@/components/admin/Sidebar";
import Navbar from "@/components/admin/Navbar";

export default function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex h-screen bg-gray-50">
      <Sidebar />
      <div className="flex-1 flex flex-col md:ml-64">
        <Navbar />
        <main className="flex-1 p-6 md:p-8 overflow-y-auto">
          {children}
        </main>
      </div>
    </div>
  );
}
Penjelasan Layout Admin
  • Sidebar - Fixed di sisi kiri, hidden di mobile (md:block)
  • md:ml-64 - Margin left 16rem (64 * 4px = 256px) untuk mengakomodasi lebar sidebar
  • Navbar - Sticky header di bagian atas konten utama
  • main - Area konten yang dapat di-scroll secara vertikal

23. Halaman Admin Orders (app/admin/orders/page.tsx)

Fitur Halaman Orders
  • Menampilkan daftar transaksi dari API /api/transactions
  • Update status transaksi (pending, success, failed)
  • Menampilkan detail pelanggan: nama, email, no telepon, alamat
Kode Lengkap Admin Orders Page
"use client";
import { useState, useEffect } from "react";

type Transaction = {
  id: number;
  midtrans_order_id: string;
  status: string;
  qty: number;
  total_price: number;
  user_id: number;
  product?: {
    name: string;
    price: number;
  };
  user?: {
    fullname: string;
    email: string;
    no_telp: string;
    address: string;
  };
  created_at: string;
};

export default function AdminOrders() {
  const [orders, setOrders] = useState<Transaction[]>([]);
  const [loading, setLoading] = useState(true);

  const fetchOrders = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/transactions');
      if (res.ok) {
         const data = await res.json();
         if (Array.isArray(data)) setOrders(data);
      }
    } catch (err) {
      console.error("Gagal mengambil data transaksi:", err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchOrders();
  }, []);

  const handleUpdateStatus = async (id: number, currentStatus: string, newStatus: string) => {
    if (currentStatus === newStatus) return; // Mencegah update berulang
    
    if (confirm(`Apakah Anda yakin mengubah status pesanan ini menjadi "${newStatus.toUpperCase()}"?`)) {
       try {
           const res = await fetch(`/api/transactions/${id}`, {
               method: 'PUT',
               headers: { 'Content-Type': 'application/json' },
               body: JSON.stringify({ status: newStatus })
           });
           if (res.ok) {
               fetchOrders();
           } else {
               const data = await res.json();
               alert("Gagal mengupdate status: " + (data.message || JSON.stringify(data.errors)));
           }
       } catch (err) {
           console.error("Gagal memperbarui status transaksi:", err);
           alert("Terjadi kesalahan teknis pada sistem.");
       }
    }
  };

  const getStatusBadge = (status: string) => {
      switch(status) {
          case 'success':
              return <span className="px-3 py-1 bg-green-100 text-green-700 text-xs font-bold rounded-full border border-green-200">Berhasil</span>;
          case 'pending':
              return <span className="px-3 py-1 bg-yellow-100 text-yellow-700 text-xs font-bold rounded-full border border-yellow-200">Menunggu</span>;
          case 'failed':
              return <span className="px-3 py-1 bg-red-100 text-red-700 text-xs font-bold rounded-full border border-red-200">Gagal/Batal</span>;
          default:
              return <span className="px-3 py-1 bg-gray-100 text-gray-700 text-xs font-bold rounded-full">{status}</span>;
      }
  };

  const formatPrice = (price: number) => {
    return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(price);
  };

  const formatDate = (dateString: string) => {
    return new Date(dateString).toLocaleDateString('id-ID', {
      day: 'numeric', month: 'short', year: 'numeric',
      hour: '2-digit', minute: '2-digit'
    });
  };

  return (
    <div className="animate-in fade-in duration-500 relative">
      <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
        <div>
          <h1 className="text-2xl lg:text-3xl font-extrabold text-gray-900">Manajemen Pesanan</h1>
          <p className="text-gray-500 mt-1 font-medium">Pantau lalu-lintas transaksi dan identitas pemesan.</p>
        </div>
      </div>

      <div className="bg-white rounded-3xl shadow-sm border border-gray-100 overflow-hidden relative min-h-[400px]">
        {/* Kontrol Tabel Sederhana */}
        <div className="p-5 border-b border-gray-100 bg-gray-50/50">
            <div className="relative w-full md:w-96">
              <svg className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
              <input 
                type="text" 
                placeholder="Cari Order ID..." 
                className="pl-10 pr-4 py-3 bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 w-full transition-all text-sm font-medium"
              />
            </div>
        </div>

        {/* Tabel Pesanan */}
        <div className="overflow-x-auto">
          {loading ? (
             <div className="flex justify-center items-center h-64">
               <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-orange-500"></div>
             </div>
          ) : (
            <table className="w-full text-left border-collapse min-w-max">
              <thead>
                <tr className="bg-white text-gray-400 text-xs uppercase tracking-widest border-b border-gray-100">
                  <th className="p-5 font-bold">Waktu</th>
                  <th className="p-5 font-bold">Order ID</th>
                  <th className="p-5 font-bold">Pelanggan</th>
                  <th className="p-5 font-bold">Produk</th>
                  <th className="p-5 font-bold">Total</th>
                  <th className="p-5 font-bold text-center">Status</th>
                  <th className="p-5 font-bold text-center">Ubah Status</th>
                </tr>
              </thead>
              <tbody className="divide-y divide-gray-50">
                {orders.length === 0 ? (
                  <tr>
                    <td colSpan={7} className="text-center p-8 text-gray-500 font-medium bg-gray-50/50">
                       Belum terdapat riwayat pesanan (transaksi) pelanggan.
                    </td>
                  </tr>
                ) : (
                  orders.map((order) => (
                    <tr key={order.id} className="hover:bg-orange-50/40 transition-colors">
                      <td className="p-5 align-middle text-sm text-gray-500 font-medium whitespace-nowrap">
                        {formatDate(order.created_at)}
                      </td>
                      <td className="p-5 align-middle">
                        <span className="font-mono font-bold text-gray-700 bg-gray-100 px-2 py-1 rounded-md text-xs">{order.midtrans_order_id}</span>
                      </td>
                      <td className="p-5 align-middle">
                        <div className="flex items-start space-x-3">
                           <div className="w-9 h-9 mt-1 shrink-0 rounded-full bg-orange-100 flex items-center justify-center text-orange-600 font-bold text-sm">
                             {order.user?.fullname ? order.user.fullname.charAt(0).toUpperCase() : '?'}
                           </div>
                           <div>
                             <p className="font-bold text-gray-900 text-sm">{order.user?.fullname || 'Pengguna Tidak Diketahui'}</p>
                             <p className="text-xs text-gray-500 font-medium">{order.user?.email || '-'}</p>
                             {order.user?.no_telp && <p className="text-xs text-gray-600 mt-1.5 flex items-center gap-1"><svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg> {order.user.no_telp}</p>}
                             {order.user?.address && <p className="text-xs text-gray-500 mt-0.5 flex items-start gap-1 max-w-[220px]"><svg className="w-3 h-3 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg> <span className="line-clamp-2" title={order.user.address}>{order.user.address}</span></p>}
                           </div>
                        </div>
                      </td>
                      <td className="p-5 align-middle">
                        <p className="font-bold text-gray-800">{order.product?.name || 'Produk Anonim'}</p>
                        <p className="text-xs text-gray-500 font-medium mt-0.5">{order.qty} Porsi</p>
                      </td>
                      <td className="p-5 align-middle text-orange-600 font-black">
                         {formatPrice(order.total_price)}
                      </td>
                      <td className="p-5 align-middle text-center">
                        {getStatusBadge(order.status)}
                      </td>
                      <td className="p-5 align-middle text-center">
                         <div className="inline-flex rounded-xl bg-gray-100 p-1">
                            <button 
                               onClick={() => handleUpdateStatus(order.id, order.status, 'pending')}
                               className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${order.status === 'pending' ? 'bg-white shadow text-yellow-600' : 'text-gray-500 hover:text-gray-800'}`}
                            >
                               Pending
                            </button>
                            <button 
                               onClick={() => handleUpdateStatus(order.id, order.status, 'success')}
                               className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${order.status === 'success' ? 'bg-white shadow text-green-600' : 'text-gray-500 hover:text-gray-800'}`}
                            >
                               Sukses
                            </button>
                            <button 
                               onClick={() => handleUpdateStatus(order.id, order.status, 'failed')}
                               className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${order.status === 'failed' ? 'bg-white shadow text-red-600' : 'text-gray-500 hover:text-gray-800'}`}
                            >
                               Gagal
                            </button>
                         </div>
                      </td>
                    </tr>
                  ))
                )}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </div>
  );
}

24. Halaman Admin Products (app/admin/products/page.tsx)

Fitur Halaman Products
  • CRUD lengkap: Create, Read, Update, Delete produk
  • Modal form untuk tambah/edit produk dengan styling modern
  • Format harga otomatis dalam Rupiah
  • Konfirmasi sebelum menghapus produk
Kode Lengkap Admin Products Page
"use client";

import { useState, useEffect } from "react";

type Product = {
  id: number;
  name: string;
  description: string;
  price: number;
  stock: number;
};

export default function AdminProducts() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  
  // Modal state
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [isEditing, setIsEditing] = useState(false);
  const [currentProduct, setCurrentProduct] = useState<Partial<Product>>({});
  
  // Base URL merujuk pada proxy Next.js agar token dikirim ke Laravel via backend Next
  const API_URL = '/api/products';

  const fetchProducts = async () => {
    setLoading(true);
    try {
      const res = await fetch(API_URL);
      if (!res.ok) throw new Error("Gagal mengambil respon dari server");
      const data = await res.json();
      setProducts(data);
    } catch (error) {
      console.error("Error fetching products:", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchProducts();
  }, []);


  console.log(products);

  const openCreateModal = () => {
    setIsEditing(false);
    setCurrentProduct({ name: '', description: '', price: 0, stock: 0 });
    setIsModalOpen(true);
  };

  const openEditModal = (product: Product) => {
    setIsEditing(true);
    setCurrentProduct(product);
    setIsModalOpen(true);
  };

  const closeModal = () => {
    setIsModalOpen(false);
  };

  const handleDelete = async (id: number) => {
    if (confirm("Apakah Anda yakin ingin menghapus produk ini? Tindakan ini tidak dapat dibatalkan.")) {
      try {
        const res = await fetch(`${API_URL}/${id}`, {
          method: 'DELETE',
        });
        if (res.ok) {
           fetchProducts();
        } else {
           alert("Gagal menghapus produk!");
        }
      } catch (error) {
        console.error("Error deleting product:", error);
      }
    }
  };

  const handleSave = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const method = isEditing ? 'PUT' : 'POST';
      const url = isEditing ? `${API_URL}/${currentProduct.id}` : API_URL;
      
      const res = await fetch(url, {
        method,
        headers: { 
          'Content-Type': 'application/json',
          'Accept': 'application/json' 
        },
        body: JSON.stringify(currentProduct),
      });
      
      if(res.ok) {
         closeModal();
         fetchProducts();
      } else {
         const errorData = await res.json();
         alert("Gagal menyimpan produk! Cek console untuk error.");
         console.error(errorData);
      }
    } catch (error) {
      console.error("Error saving product:", error);
      alert("Terjadi kesalahan sistem ketika menyimpan data.");
    }
  };

  // formatting currency
  const formatPrice = (price: number) => {
    // Handling possible null/undefined nicely
    if (price === undefined || price === null) return "Rp 0";
    return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(price);
  };

  return (
    <div className="animate-in fade-in duration-500 relative">
      <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4">
        <div>
          <h1 className="text-2xl lg:text-3xl font-extrabold text-gray-900">Manajemen Produk</h1>
          <p className="text-gray-500 mt-1 font-medium">Kelola daftar jajanan melalui API Laravel.</p>
        </div>
        <button 
          onClick={openCreateModal}
          className="bg-orange-500 hover:bg-orange-600 text-white px-5 py-2.5 rounded-xl font-bold shadow-md hover:shadow-lg transition-all flex items-center transform hover:-translate-y-0.5"
        >
          <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M12 4v16m8-8H4" /></svg>
          Tambah Produk
        </button>
      </div>

      <div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden relative min-h-[400px]">
        {/* Table */}
        <div className="overflow-x-auto">
          {loading ? (
             <div className="flex justify-center items-center h-64">
               <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-orange-500"></div>
             </div>
          ) : (
            <table className="w-full text-left border-collapse min-w-max">
              <thead>
                <tr className="bg-white text-gray-400 text-xs uppercase tracking-widest border-b border-gray-100">
                  <th className="p-5 font-bold">Nama Produk</th>
                  <th className="p-5 font-bold">Deskripsi</th>
                  <th className="p-5 font-bold">Harga</th>
                  <th className="p-5 font-bold">Stok</th>
                  <th className="p-5 font-bold text-center">Aksi</th>
                </tr>
              </thead>
              <tbody className="divide-y divide-gray-50">
                {products.length === 0 ? (
                  <tr>
                    <td colSpan={5} className="text-center p-8 text-gray-500 font-medium bg-gray-50/50">
                       Belum ada data produk dari API. Silakan tambah produk baru.
                    </td>
                  </tr>
                ) : (
                  products.map((product) => (
                    <tr key={product.id} className="hover:bg-orange-50/50 transition-colors group">
                      <td className="p-5 cursor-pointer">
                        <p className="font-bold text-gray-800 group-hover:text-orange-600 transition-colors">{product.name}</p>
                        <p className="text-xs text-gray-400 mt-1">ID: {product.id}</p>
                      </td>
                      <td className="p-5 font-medium text-gray-600 max-w-xs truncate" title={product.description}>
                        {product.description}
                      </td>
                      <td className="p-5 text-gray-800 font-extrabold">{formatPrice(product.price)}</td>
                      <td className="p-5">
                        <span className={`px-3 py-1 rounded-full text-xs font-bold ${product.stock < 10 ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
                          {product.stock} pcs
                        </span>
                      </td>
                      <td className="p-5">
                        <div className="flex items-center justify-center space-x-2">
                          <button onClick={() => openEditModal(product)} className="text-blue-600 font-bold transition-colors bg-blue-50 px-4 py-2 rounded-lg hover:bg-blue-600 hover:text-white shadow-sm hover:shadow">Edit</button>
                          <button onClick={() => handleDelete(product.id)} className="text-red-600 font-bold transition-colors bg-red-50 px-4 py-2 rounded-lg hover:bg-red-600 hover:text-white shadow-sm hover:shadow">Hapus</button>
                        </div>
                      </td>
                    </tr>
                  ))
                )}
              </tbody>
            </table>
          )}
        </div>
      </div>

      {/* Modal CRUD */}
      {isModalOpen && (
        <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
          <div className="bg-white rounded-3xl w-full max-w-lg shadow-2xl overflow-hidden border border-gray-100 flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200">
            <div className="flex justify-between items-center p-6 border-b border-gray-100 bg-orange-50/50">
              <h2 className="text-2xl font-extrabold text-gray-900">{isEditing ? 'Edit Informasi Produk' : 'Tambah Produk Baru'}</h2>
              <button type="button" onClick={closeModal} className="text-gray-400 hover:text-red-500 bg-white shadow-sm w-8 h-8 rounded-full flex items-center justify-center transition-colors">
                <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
              </button>
            </div>
            
            <form onSubmit={handleSave} className="p-6 overflow-y-auto space-y-5">
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2" htmlFor="name">Nama Produk</label>
                <input 
                  type="text" 
                  id="name" 
                  value={currentProduct.name || ''}
                  onChange={(e) => setCurrentProduct({...currentProduct, name: e.target.value})}
                  className="w-full px-4 py-3 rounded-xl bg-gray-50 border border-gray-200 focus:bg-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-all"
                  placeholder="Mis. Seblak Mercon"
                  required
                />
              </div>
              
              <div>
                <label className="block text-sm font-bold text-gray-700 mb-2" htmlFor="description">Deskripsi</label>
                <textarea 
                  id="description" 
                  rows={3}
                  value={currentProduct.description || ''}
                  onChange={(e) => setCurrentProduct({...currentProduct, description: e.target.value})}
                  className="w-full px-4 py-3 rounded-xl bg-gray-50 border border-gray-200 focus:bg-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-all resize-none"
                  placeholder="Deskripsikan rasa, isi, dll."
                  required
                ></textarea>
              </div>

              <div className="grid grid-cols-2 gap-5">
                <div>
                  <label className="block text-sm font-bold text-gray-700 mb-2" htmlFor="price">Harga (Rp)</label>
                  <input 
                    type="number" 
                    id="price" 
                    min="0"
                    value={currentProduct.price || ''}
                    onChange={(e) => setCurrentProduct({...currentProduct, price: Number(e.target.value)})}
                    className="w-full px-4 py-3 rounded-xl bg-gray-50 border border-gray-200 focus:bg-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-all"
                    placeholder="10000"
                    required
                  />
                </div>
                <div>
                  <label className="block text-sm font-bold text-gray-700 mb-2" htmlFor="stock">Stok Awal</label>
                  <input 
                    type="number" 
                    id="stock"
                    min="0"
                    value={currentProduct.stock || ''}
                    onChange={(e) => setCurrentProduct({...currentProduct, stock: Number(e.target.value)})}
                    className="w-full px-4 py-3 rounded-xl bg-gray-50 border border-gray-200 focus:bg-white focus:ring-2 focus:ring-orange-500 focus:border-orange-500 outline-none transition-all"
                    placeholder="50"
                    required
                  />
                </div>
              </div>

              <div className="pt-6 border-t border-gray-100 flex justify-end gap-3 mt-8">
                <button 
                  type="button" 
                  onClick={closeModal}
                  className="px-6 py-3 font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-xl transition-colors"
                >
                  Batal
                </button>
                <button 
                  type="submit"
                  className="px-6 py-3 font-bold text-white bg-orange-500 hover:bg-orange-600 shadow-lg shadow-orange-500/30 hover:shadow-orange-500/50 rounded-xl transition-all"
                >
                  {isEditing ? 'Simpan Perubahan' : 'Tambah Data Produk'}
                </button>
              </div>
            </form>
          </div>
        </div>
      )}
    </div>
  );
}
Selamat!

setelah selesai, anda sekarang telah memiliki sebuah aplikasi mini e-commerce yang sudah bisa digunakan