PART 1 - Setup Proyek dan membuat halaman Home, Produk, dan Keranjang
Pada bagian pertama ini, kita akan memulai dengan menyiapkan proyek Next.js dan menginstal beberapa library pendukung yang akan digunakan untuk membangun toko online sederhana. Tahapan ini penting agar struktur proyek rapi, mudah dikelola, dan siap untuk dikembangkan pada bagian-bagian selanjutnya.
Tujuan utama pada Part 1 adalah
- Setup Next.js di komputer
- Menginstal library yang diperlukan
- Menyiapkan struktur folder yang rapi
- Membuat komponen dasar UI
- Membuat halaman Home, Produk, Produk Detail, dan Keranjang
- Menjalankan aplikasi di browser
- Setup Next.js
Langkah awal adalah membuat project baru menggunakan Next.js. Framework ini dipilih karena mendukung fitur server-side rendering (SSR), routing bawaan, serta fitur lainnya yang sangat dibutuhkan dalam proses pengembangan web modern.
- Install Library yang Dibutuhkan
- Axios - Untuk Fetch Data API
- Zustand - Untuk State Management
- React Icons - Untuk Menampilkan Ikon
- React Hot Toast - Untuk Menampilkan Notifikasi
- Setup Next.js di Lokal
Langkah pertama adalah menginstal Next.js. Buka folder dimana anda akan
menyimpan project nya, kemudian buka terminal dan jalankan perintah berikut
pada terminal:
npx create-next-app@latest
Lanjutkan intruksi seperti pada Gambar 1 sampai selesai
|
|
| Gambar 2 |
- Instal Library yang Dibutuhkan
Selanjutnya kita menginstal beberapa package yang akan digunakan:
- axios → melakukan fetch data API
- zustand → state management
- react-icons → icon
- react-hot-toast → notifikasi
npm install axios zustand react-icons react-hot-toast
- Membuka Proyek di VS Code
Gunakan perintah pada terminal seperti pada code berikut:
code .
Folder build yang dibuat otomatis oleh Next.js. Berisi hasil kompilasi, cache, dan file yang diperlukan untuk menjalankan aplikasi. Tidak perlu diedit secara manual.
Berisi semua library dan dependensi yang diinstal oleh project. Folder ini dibuat otomatis setelah menjalankan
npm install.Folder utama untuk mengatur halaman, routing, layout, dan komponen berbasis App Router. Di dalamnya terdapat file seperti
layout.js dan page.js yang mengatur tampilan aplikasi.
- Mengganti Format File
Next.js secara default membuat file layout.js dan page.js.Agar konsisten dengan standar React, ubah keduanya menjadi:
- layout.jsx
- page.jsx
| Gambar 6 |
- Membuat Struktur Folder Komponen
Agar mudah di-maintenance, kita pisahkan semua component ke dalam folder khusus.
Buat folder di dalam src dengan nama components, di dalam folder components buat beberapa folder berikut: layouts, pages, dan ui
Setelah anda membuat akan seperti Gambar 7 berikut:
| Gambar 7 |
- layouts → berisi layout global atau layout halaman
- pages → berisi komponen untuk halaman
- ui → komponen kecil seperti button, card, navbar, dsb
- Membuat Komponen di Folder UI
ui, buat folder lagi dengan nama berikut:- button
- card
- error
- footer
- loading
- navbar
- section
| Gambar 8 |
Selanjutnya kita akan membuat componen di dalam folder tersebut.
Dalam folder button buat file dengan nama Button.jsx, kemudian masukan code berikut pada file tersebut:
import React from "react";
export default function Button({ className, children, ...props }) {
return (
<button
type="button"
className={`text-white flex cursor-pointer items-center gap-1 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg px-3 py-2 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ${className}`}
{...props}
>
{children}
</button>
);
}
- ProductCard.jsx
- ProductCartCard.jsx
- SummaryOrderCard.jsx
import Link from "next/link";
import { FaBagShopping } from "react-icons/fa6";
import Button from "../button/Button";
export default function ProductCard({ product }) {
return (
<div className="w-full bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700 hover:shadow-xl shadow-gray-300 transition-all duration-300 ">
<Link href={`/products/${product.id}`}>
<img
className="p-4 rounded-t-lg h-72 w-full object-contain"
src={product.image}
alt="product image"
/>
</Link>
<div className="px-5 pb-5 flex flex-col gap-2">
<Link href={`/products/${product.id}`}>
<h5 className="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">
{product.name}
</h5>
</Link>
<div className="flex justify-between gap-2 items-center">
<span className="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-blue-400 border border-blue-400">
{product.category.name}
</span>
<span className="text-gray-700 text-base">
{product.stock} Tersedia
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-gray-900 dark:text-white">
{new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(parseFloat(product.price))}
</span>
<Button
className={`text-xs ${product.stock === 0 && "opacity-50"}`}
disabled={product.stock === 0}
>
<FaBagShopping className="size-4" />
Tambah ke Keranjang
</Button>
</div>
</div>
</div>
);
}
import Link from "next/link";
import { FaTrashAlt } from "react-icons/fa";
import { FaCircleMinus, FaCirclePlus } from "react-icons/fa6";
export default function ProductCartCard({ productCart }) {
return (
<div className="flex gap-4 bg-white px-4 py-6 rounded-md shadow-sm border border-gray-200">
<div className="flex items-center w-fit">
<input
id="checked-checkbox"
type="checkbox"
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<div className="flex gap-6 sm:gap-4 max-sm:flex-col">
<div className="w-24 h-24 max-sm:w-24 max-sm:h-24 shrink-0">
<img
src={productCart.image}
alt="product image"
className="w-full h-full object-contain border rounded-sm border-gray-200"
/>
</div>
<div className="flex flex-col gap-4">
<div>
<Link href={`/products/${productCart.id}`}>
<h3 className="text-sm sm:text-base font-semibold text-slate-900 hover:underline">
{productCart.name}
</h3>
</Link>
<span className="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-blue-400 border border-blue-400">
{productCart.category.name}
</span>
</div>
<div className="mt-auto">
<h3 className="text-sm font-semibold text-slate-900">
{new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(parseFloat(productCart.price * productCart.quantity))}
</h3>
</div>
</div>
</div>
<div className="ml-auto flex flex-col">
<div className="flex items-start gap-4 justify-end">
<button type="button">
<FaTrashAlt className="text-red-500" />
</button>
</div>
<div className="flex items-center gap-3 mt-auto">
<button type="button" disabled={productCart.quantity === 1}>
<FaCircleMinus
className={`size-4 ${
productCart.quantity === 1 ? "text-gray-400" : "text-gray-800"
}`}
/>
</button>
<span className="font-semibold text-base leading-[18px]">
{productCart.quantity}
</span>
<button
type="button"
disabled={productCart.quantity === productCart.stock}
>
<FaCirclePlus
className={`size-4 ${
productCart.quantity === productCart.stock
? "text-gray-400"
: "text-gray-800"
}`}
/>
</button>
</div>
</div>
</div>
);
}
export default function SummaryOrderCard() {
const SHIPMENT_COST = 0;
const subTotal = 10000;
const total = subTotal + SHIPMENT_COST;
return (
<div className="bg-white rounded-md px-4 py-6 h-max shadow-sm border border-gray-200">
<ul className="text-slate-500 font-medium space-y-4">
<li className="flex flex-wrap gap-4 text-sm">
Subtotal{" "}
<span className="ml-auto font-semibold text-slate-900">
{new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(parseFloat(subTotal))}
</span>
</li>
<li className="flex flex-wrap gap-4 text-sm">
Pengiriman{" "}
<span className="ml-auto font-semibold text-slate-900">
{new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(parseFloat(SHIPMENT_COST))}
</span>
</li>
<hr className="border-slate-300" />
<li className="flex flex-wrap gap-4 text-sm font-semibold text-slate-900">
Total{" "}
<span className="ml-auto">
{new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(parseFloat(total))}
</span>
</li>
</ul>
<div className="mt-8 space-y-4">
<button
type="button"
className={`text-sm px-4 py-2.5 w-full font-medium tracking-wide bg-blue-800 hover:bg-blue-900 text-white rounded-md cursor-pointer`}
>
Beli Sekarang
</button>
</div>
</div>
);
}
import React from "react";
import { BiSolidError } from "react-icons/bi";
export default function FetchError({ text = "Terjadi Kesalahan" }) {
return (
<div className="flex flex-col items-center">
<BiSolidError className="size-16 text-red-600" />
<p className="text-red-600 text-base">{text}</p>
</div>
);
}
import React from "react";
export default function FrontpageFooter() {
return (
<footer className="bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
<div className="mx-auto w-full max-w-screen-xl p-6 lg:py-8">
<div className="md:flex md:justify-between">
<div className="mb-6 md:mb-0">
<a href="/" className="flex items-center">
<img src="/favicon.ico" className="h-8 me-3" alt="Logo" />
<span className="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">
Simple Ecommerce
</span>
</a>
</div>
<div className="grid grid-cols-2 gap-8 sm:gap-6 sm:grid-cols-3">
<div>
<h2 className="mb-4 text-sm font-semibold text-gray-900 uppercase dark:text-white">
Tentang Kami
</h2>
<ul className="text-gray-500 dark:text-gray-400 font-medium space-y-2">
<li>
<a href="#" className="hover:underline">
Profil Kami
</a>
</li>
<li>
<a href="#" className="hover:underline">
Kontak Kami
</a>
</li>
<li>
<a href="#" className="hover:underline">
Karir
</a>
</li>
</ul>
</div>
<div>
<h2 className="mb-4 text-sm font-semibold text-gray-900 uppercase dark:text-white">
Bantuan
</h2>
<ul className="text-gray-500 dark:text-gray-400 font-medium space-y-2">
<li>
<a href="#" className="hover:underline">
FAQ
</a>
</li>
<li>
<a href="#" className="hover:underline">
Pengiriman
</a>
</li>
<li>
<a href="#" className="hover:underline">
Kebijakan Pengembalian
</a>
</li>
</ul>
</div>
<div>
<h2 className="mb-4 text-sm font-semibold text-gray-900 uppercase dark:text-white">
Ikuti Kami
</h2>
<ul className="text-gray-500 dark:text-gray-400 font-medium space-y-2">
<li>
<a href="#" className="hover:underline">
Instagram
</a>
</li>
<li>
<a href="#" className="hover:underline">
Facebook
</a>
</li>
<li>
<a href="#" className="hover:underline">
Twitter
</a>
</li>
</ul>
</div>
</div>
</div>
<hr className="my-6 border-gray-200 sm:mx-auto dark:border-gray-700 lg:my-8" />
<div className="sm:flex sm:items-center sm:justify-between">
<span className="text-sm text-gray-500 sm:text-center dark:text-gray-400">
© {new Date().getFullYear()}{" "}
<a href="/" className="hover:underline">
Simple Ecommerce
</a>
</span>
<div className="flex mt-4 sm:justify-center sm:mt-0 space-x-5">
<a
href="#"
className="text-gray-500 hover:text-gray-900 dark:hover:text-white"
aria-label="Facebook"
>
<i className="fa-brands fa-facebook-f"></i>
</a>
<a
href="#"
className="text-gray-500 hover:text-gray-900 dark:hover:text-white"
aria-label="Instagram"
>
<i className="fa-brands fa-instagram"></i>
</a>
<a
href="#"
className="text-gray-500 hover:text-gray-900 dark:hover:text-white"
aria-label="Twitter"
>
<i className="fa-brands fa-x-twitter"></i>
</a>
</div>
</div>
</div>
</footer>
);
}
import React from "react";
export default function SpinnerLoading() {
return (
<div
role="status"
className="flex flex-col justify-center items-center gap-1"
>
<svg
aria-hidden="true"
className="inline size-12 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<p>Loading...</p>
</div>
);
}
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { FaBagShopping } from "react-icons/fa6";
import Button from "../button/Button";
export default function FrontpageNavbar() {
const [isOpenDropdown, setIsOpenDropdown] = useState(false);
const navbarRef = useRef(null);
const pathname = usePathname();
// click outside
useEffect(() => {
const handleClickOutside = (event) => {
if (navbarRef.current && !navbarRef.current.contains(event.target)) {
setIsOpenDropdown(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [navbarRef]);
return (
<nav
ref={navbarRef}
className="bg-white dark:bg-gray-900 fixed w-full z-20 top-0 start-0 border-b border-gray-200 dark:border-gray-600"
>
<div className="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<Link
href="/"
className="flex items-center space-x-3 rtl:space-x-reverse"
>
<img src="/favicon.ico" className="h-8" alt="Flowbite Logo" />
<span className="self-center text-lg sm:text-2xl font-semibold whitespace-nowrap dark:text-white">
Simple Ecommerce
</span>
</Link>
<div className="flex md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
<Link href="/carts">
<Button className={"text-xs"}>
<FaBagShopping className="size-4" />
<span>0</span>
</Button>
</Link>
<button
data-collapse-toggle="navbar-sticky"
type="button"
className="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
aria-controls="navbar-sticky"
aria-expanded="false"
onClick={() => setIsOpenDropdown(!isOpenDropdown)}
>
<span className="sr-only">Open main menu</span>
<svg
className="w-5 h-5"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 17 14"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M1 1h15M1 7h15M1 13h15"
/>
</svg>
</button>
</div>
<div
className={`items-center justify-between ${
isOpenDropdown ? "" : "hidden"
} w-full md:flex md:w-auto md:order-1`}
id="navbar-sticky"
>
<ul className="flex flex-col p-4 md:p-0 mt-4 font-medium border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li>
<Link
href="/"
className={`
block py-2 px-3 rounded-sm
${
pathname === "/"
? "text-white bg-blue-700 md:bg-transparent md:text-blue-700 md:dark:text-blue-500"
: "text-gray-900 hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:dark:hover:text-blue-500 dark:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
}
`}
>
Home
</Link>
</li>
<li>
<Link
href="/products"
className={`
block py-2 px-3 rounded-sm
${
pathname.startsWith("/products")
? "text-white bg-blue-700 md:bg-transparent md:text-blue-700 md:dark:text-blue-500"
: "text-gray-900 hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:dark:hover:text-blue-500 dark:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent"
}
`}
>
Produk
</Link>
</li>
</ul>
</div>
</div>
</nav>
);
}
import Link from "next/link";
import React from "react";
import Button from "../button/Button";
import { FaArrowRightLong } from "react-icons/fa6";
export default function HomeHeroSection() {
return (
<section className="relative isolate overflow-hidden bg-white dark:bg-gray-900 py-12">
<div className="mx-auto max-w-2xl text-center">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-6xl">
Belanja Mudah & Aman di{" "}
<span className="text-blue-600">Simple Ecommerce</span>
</h1>
<p className="mt-6 text-lg leading-8 text-gray-600 dark:text-gray-400">
Temukan berbagai produk pilihan dengan harga terbaik. Nikmati
pengalaman belanja online yang cepat, aman, dan menyenangkan.
</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<Link href="/products">
<Button>
Belanja Sekarang
<FaArrowRightLong />
</Button>
</Link>
</div>
</div>
</section>
);
}
| Gambar 9 |
- Membuat Layout dan Halaman
Pada folder layout buat file dengan nama FrontpageLayout.jsx kemudian masukan kode berikut:
import { Toaster } from "react-hot-toast";
import FrontpageFooter from "../ui/footer/FrontpageFooter";
import FrontpageNavbar from "../ui/navbar/FrontpageNavbar";
export default function FrontpageLayout({ children }) {
return (
<>
<Toaster position="top-center" reverseOrder={false} />
<FrontpageNavbar />
<div className="min-h-[60vh] mt-24 mb-16 max-w-screen-xl mx-auto">
{children}
</div>
<FrontpageFooter />
</>
);
}
<Toaster position="top-center" reverseOrder={false} />
| Gambar 10 |
Selanjutnya pada folder cart buatkan file dengan nama CartPage.jsx, kemudian masukan kode berikut:
"use client";
import FrontpageLayout from "@/components/layouts/FrontpageLayout";
import Button from "@/components/ui/button/Button";
import ProductCartCard from "@/components/ui/card/ProductCartCard";
import SummaryOrderCard from "@/components/ui/card/SummaryOrderCard";
import FetchError from "@/components/ui/error/FetchError";
import Link from "next/link";
import { FaArrowRightLong } from "react-icons/fa6";
export default function CartPageContent() {
const carts = [
{
id: 1,
name: "Smartphone Android Terbaru",
description:
"Ponsel pintar dengan kamera 108MP dan RAM 8GB. Performa tinggi untuk gaming dan multitasking.",
image:
"https://images.unsplash.com/photo-1592890288564-76628a30a657?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1170",
price: "5999000.00",
stock: 15,
slug: "smartphone-android-terbaru-OfxcQ",
category: {
id: 1,
name: "Elektronik",
description: "Produk-produk seperti ponsel, laptop, dan TV.",
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
quantity: 1,
isSelected: false,
},
{
id: 2,
name: "Laptop Ultra Tipis 13 Inci",
description:
"Laptop ringan dan portabel. Ideal untuk profesional yang sering bepergian.",
image:
"https://images.unsplash.com/photo-1496181133206-80ce9b88a853?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1171",
price: "12500000.00",
stock: 0,
slug: "laptop-ultra-tipis-13-inci-Dr7oL",
category: {
id: 1,
name: "Elektronik",
description: "Produk-produk seperti ponsel, laptop, dan TV.",
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
quantity: 2,
isSelected: true,
},
];
return (
<FrontpageLayout>
<h3 className="font-bold text-gray-800 text-3xl text-start px-4 mb-4">
Keranjang
</h3>
{!carts.length ? (
<div className="flex flex-col gap-4 items-center">
<FetchError text="Produk tidak ditemukan, silahkan tambahkan produk ke keranjang" />
<Link href={"/products"}>
<Button>
Belanja Sekarang
<FaArrowRightLong />
</Button>
</Link>
</div>
) : (
<>
<div
key={carts.length}
className="grid lg:grid-cols-3 lg:gap-x-8 gap-x-6 gap-y-8 mt-6 px-4"
>
<div className="lg:col-span-2 space-y-6">
{carts.map((cart, index) => (
<ProductCartCard key={index} productCart={cart} />
))}
</div>
<SummaryOrderCard />
</div>
<div className="p-4">
<button
type="button"
className="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900"
>
Bersihkan Keranjang
</button>
</div>
</>
)}
</FrontpageLayout>
);
}
import FrontpageLayout from "@/components/layouts/FrontpageLayout";
import HomeHeroSection from "@/components/ui/section/HomeHeroSection";
export default function HomePageContent() {
return (
<FrontpageLayout>
<HomeHeroSection />
</FrontpageLayout>
);
}
Pada folder product kita akan membuat 2 file yaitu:
- ProductPage.jsx
- ProductDetailPage.jsx
"use client";
import FrontpageLayout from "@/components/layouts/FrontpageLayout";
import ProductCard from "@/components/ui/card/ProductCard";
import FetchError from "@/components/ui/error/FetchError";
export default function ProductPageContent() {
const productList = [
{
id: 1,
name: "Smartphone Android Terbaru",
description:
"Ponsel pintar dengan kamera 108MP dan RAM 8GB. Performa tinggi untuk gaming dan multitasking.",
image:
"https://images.unsplash.com/photo-1592890288564-76628a30a657?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1170",
price: "5999000.00",
stock: 15,
slug: "smartphone-android-terbaru-OfxcQ",
category: {
id: 1,
name: "Elektronik",
description: "Produk-produk seperti ponsel, laptop, dan TV.",
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
{
id: 2,
name: "Laptop Ultra Tipis 13 Inci",
description:
"Laptop ringan dan portabel. Ideal untuk profesional yang sering bepergian.",
image:
"https://images.unsplash.com/photo-1496181133206-80ce9b88a853?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1171",
price: "12500000.00",
stock: 0,
slug: "laptop-ultra-tipis-13-inci-Dr7oL",
category: {
id: 1,
name: "Elektronik",
description: "Produk-produk seperti ponsel, laptop, dan TV.",
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
];
return (
<FrontpageLayout>
<div className="flex flex-col gap-4 w-full px-4">
<div className="flex justify-between items-center gap-2">
<h3 className="font-bold text-gray-800 text-3xl text-start mb-4">
Produk
</h3>
<form className="w-1/2 sm:w-1/3">
<label
htmlFor="default-search"
className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white"
>
Search
</label>
<div className="relative">
<input
type="search"
id="default-search"
className="block w-full p-4 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Cari produk..."
/>
<button
type="submit"
className="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Cari
</button>
</div>
</form>
</div>
{productList.length === 0 && (
<FetchError text={"Produk tidak ditemukan"} />
)}
{productList.length > 0 && (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{productList.map((product, index) => (
<ProductCard key={index} product={product} />
))}
</div>
</>
)}
</div>
</FrontpageLayout>
);
}
"use client";
import FrontpageLayout from "@/components/layouts/FrontpageLayout";
import Button from "@/components/ui/button/Button";
import { useParams } from "next/navigation";
import { FaBagShopping } from "react-icons/fa6";
export default function ProductDetailPageContent() {
const { productId } = useParams();
const product = {
id: productId,
name: `Product dengan id ${productId}`,
description:
"Ponsel pintar dengan kamera 108MP dan RAM 8GB. Performa tinggi untuk gaming dan multitasking.",
image:
"https://images.unsplash.com/photo-1592890288564-76628a30a657?ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&q=80&w=1170",
price: "5999000.00",
stock: 15,
slug: "smartphone-android-terbaru-OfxcQ",
category: {
id: 1,
name: "Elektronik",
description: "Produk-produk seperti ponsel, laptop, dan TV.",
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
},
created_at: "2025-11-03 08:06:01",
updated_at: "2025-11-03 08:06:01",
};
return (
<FrontpageLayout>
<h3 className="font-bold text-gray-800 text-3xl text-start px-4 mb-4">
Detail Produk
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 px-4">
<img src={product.image} alt="product image" className="rounded-sm" />
<ul className="flex flex-col gap-4">
<li className="text-3xl font-semibold">{product.name}</li>{" "}
<li>Kategori: {product?.category?.name}</li>{" "}
<li>
{" "}
Harga:{" "}
{product.price
? new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(parseFloat(product.price))
: "Rp 0"}{" "}
</li>
<li>Stok: {product.stock}</li>{" "}
<li>Deskripsi: {product.description}</li>{" "}
<li>
<Button
className={`text-xs ${product.stock === 0 && "opacity-50"}`}
disabled={product.stock === 0}
>
<FaBagShopping className="size-4" />
Tambah ke Keranjang
</Button>
</li>
</ul>
</div>
</FrontpageLayout>
);
}
- Menghubungkan ke Routing Next.js (Folder app)
@import "tailwindcss";
import { Geist, Geist_Mono } from "next/font/google";
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 = {
title: "Simple Ecommerce",
description: "Generated by create next app",
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
import HomePageContent from "@/components/pages/home/HomePage";
export default function HomePage() {
return <HomePageContent />;
}npm run dev
| Gambar 12 |
Yess, sudah berhasil membuat halaman home nya, kelihatan menarik bukann??
Eitss, tapi ini baru halam home nya saja, rencana kita adalah membuat halaman Home, Produk, dan Keranjang. jadi, ayoo lanjut lagi ke tahap selanjutnya..
Dalam folder app/products buat file dengan nama page.jsx, kemudian masukan kode berikut:
import ProductPageContent from "@/components/pages/product/ProductPage";
import { Suspense } from "react";
export default function ProductPage() {
return (
<Suspense fallback={<div className="p-4">Loading...</div>}>
<ProductPageContent />
</Suspense>
);
}
| Gambar 13 |
Itu baru tampilan list produk nya, untuk tampilan detail produk nya lanjutkan tahapan selanjutnya.
import ProductDetailPageContent from "@/components/pages/product/ProductDetailPage";
import React from "react";
export default function ProductDetailPage() {
return <ProductDetailPageContent />;
}
Satu tahap lagi untuk halaman keranjang.
Buat folder di dalam folder app dengan nama carts, dan file di dalam carts dengan nama page.jsx
struktur nya seperti Gambar 16 berikut:
| Gambar 16 |
Pada file app/carts/page.jsx masukan kode berikut:
import CartPageContent from "@/components/pages/cart/CartPage";
import React from "react";
export default function CartPage() {
return <CartPageContent />;
}
Kita sudah berhasil membuat struktur dan halaman dasar toko online sederhana menggunakan Next.js.

0 Comments
Posting Komentar