Membuat Cart Menggunakan Next.js dan Supabase

Galih Setiawan Nurohim
7 min readNov 25, 2024

--

Aplikasi Kasir

Dalam artikel ini, kita akan membahas bagaimana membuat sistem cart sederhana menggunakan Next.js dan Supabase. Cart ini akan memungkinkan pengguna untuk menambahkan barang, mengurangi barang, menghitung total harga, dan melakukan checkout yang menyimpan transaksi ke database.

Berikut adalah langkah-langkah yang telah dirangkum:

#pendahuluan

a. Instalasi Next.js

Jika Anda belum memiliki proyek Next.js, buat proyek baru dengan perintah:

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

b. Instalasi Supabase

Instal Supabase untuk integrasi dengan backend:

npm install @supabase/supabase-js

Buat file supabaseClient.js untuk menginisialisasi koneksi ke Supabase:

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = 'https://your-supabase-url.supabase.co';
const supabaseKey = 'your-anon-key';

export const supabase = createClient(supabaseUrl, supabaseKey);

2. Struktur Komponen

Buat folder components dan file Layout.js untuk membuat tata letak aplikasi:

export default function Layout({ children }) {
return (
<div className="container mx-auto px-4">
{children}
</div>
);
}

3. Pembuatan Halaman Cart

Buat file pages/cart.js yang berisi logika dan tampilan cart. Anda dapat menggunakan kode berikut sebagai referensi:

a. Fetch Barang

Gunakan Supabase untuk mengambil data barang:

useEffect(() => {
const fetchBarangData = async () => {
const { data, error } = await supabase
.from('barang')
.select('*')
.order('barang', { ascending: true });

if (error) {
console.error('Error fetching barang:', error.message);
} else {
setBarangList(data);
}
};

fetchBarangData();
}, []);

Data yang diambil akan ditampilkan dalam daftar barang.

b. Menambahkan Barang ke Cart

Untuk menambah barang ke cart:

const addToCart = (barang) => {
if (barang.stok <= 0) {
alert('Stok barang ini kosong.');
return;
}

setCartItems((prevItems) => {
const itemExists = prevItems.find((item) => item.kode_barang === barang.kode_barang);

if (itemExists) {
return prevItems.map((item) =>
item.kode_barang === barang.kode_barang && item.jumlah < barang.stok
? { ...item, jumlah: item.jumlah + 1 }
: item
);
} else {
return [...prevItems, { ...barang, jumlah: 1, totalHarga: barang.harga_jual }];
}
});
};

c. Checkout dan Simpan Transaksi

Ketika pengguna selesai berbelanja, data transaksi akan disimpan ke database Supabase:

const handleCheckout = async () => {
if (cartItems.length === 0) {
alert('Keranjang kosong.');
return;
}

const todayDate = new Date().toISOString().split("T")[0];
const waktuSekarang = new Date().toLocaleTimeString('id-ID').replace(/\./g, ':');
const timestamp = `${todayDate} ${waktuSekarang}`;

// Simpan transaksi ke tabel penjualan
const { data: penjualanData, error: penjualanError } = await supabase
.from('penjualanbrg')
.insert([{ total_harga: totalHarga, nama_pelanggan, tanggal: timestamp }])
.select()
.single();

if (penjualanError) {
console.error('Error:', penjualanError.message);
alert('Gagal membuat transaksi.');
return;
}

// Simpan detail penjualan
const detailPenjualanData = cartItems.map((item) => ({
id_penjualanbrg: penjualanData.id_penjualanbrg,
kode_barang: item.kode_barang,
jumlah: item.jumlah,
harga_jual: item.totalHarga,
}));

const { error: detailError } = await supabase
.from('detail_penjualan')
.insert(detailPenjualanData);

if (detailError) {
console.error('Error:', detailError.message);
alert('Gagal menyimpan detail transaksi.');
} else {
alert('Transaksi berhasil.');
setCartItems([]);
setTotalHarga(0);
setNamaPelanggan('');
}
};

d. Total Harga

Hitung total harga secara otomatis setiap kali item diubah:

useEffect(() => {
const total = cartItems.reduce((acc, item) => acc + item.totalHarga * item.jumlah, 0);
setTotalHarga(total);
}, [cartItems]);

4. Styling dengan Tailwind CSS

Gunakan Tailwind CSS untuk styling. Install Tailwind CSS dengan perintah berikut:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

Tambahkan konfigurasi di tailwind.config.js dan file globals.css untuk mengaktifkan Tailwind.

5. Fitur Tambahan

  • Validasi stok: Pastikan stok barang cukup sebelum menambahkan ke keranjang.
  • Search filter: Memungkinkan pencarian barang berdasarkan nama atau kode.
  • Laporan transaksi: Redirect ke halaman laporan setelah checkout berhasil.

Dengan implementasi ini, Anda telah berhasil membuat fitur cart sederhana dengan Next.js dan Supabase yang lengkap dengan kemampuan checkout dan penyimpanan data. Selamat mencoba!

Full Code

"use client"; // Mengindikasikan bahwa komponen ini menggunakan rendering sisi klien (client-side rendering).

import React, { useEffect, useState } from 'react'; // Mengimpor React serta hook untuk state dan efek.
import { supabase } from '/supabaseClient'; // Mengimpor instans Supabase untuk berinteraksi dengan database.
import Layout from '../components/Layout'; // Mengimpor komponen Layout untuk membungkus konten halaman.
import { useRouter } from 'next/navigation'; // Mengimpor useRouter untuk navigasi antar halaman.

export default function CartPage() {
const router = useRouter(); // Menginisialisasi router untuk navigasi.

// State untuk menyimpan data barang dari database.
const [barangList, setBarangList] = useState([]);

// State untuk menyimpan item yang ditambahkan ke keranjang.
const [cartItems, setCartItems] = useState([]);

// State untuk menghitung total harga barang di keranjang.
const [totalHarga, setTotalHarga] = useState(0);

// State untuk menyimpan kata kunci pencarian barang.
const [searchTerm, setSearchTerm] = useState('');

// State untuk menampilkan indikator loading saat memuat data.
const [loading, setLoading] = useState(false);

// State untuk menyimpan nama pelanggan.
const [namaPelanggan, setNamaPelanggan] = useState('');

// Mengambil data barang dari database Supabase saat komponen pertama kali dirender.
useEffect(() => {
const fetchBarangData = async () => {
setLoading(true); // Menyalakan indikator loading.
const { data, error } = await supabase
.from('barang') // Memilih tabel 'barang'.
.select('*') // Mengambil semua kolom.
.order('barang', { ascending: true }); // Mengurutkan data berdasarkan nama barang.

if (error) {
console.error('Error fetching barang:', error.message); // Log error jika terjadi kesalahan.
} else {
setBarangList(data); // Menyimpan data barang ke state.
}
setLoading(false); // Mematikan indikator loading.
};

fetchBarangData(); // Memanggil fungsi fetchBarangData saat komponen dirender.
}, []); // [] memastikan efek hanya dijalankan sekali.

// Memfilter data barang berdasarkan kata kunci pencarian.
const filteredBarangList = barangList.filter(
(barang) =>
(barang.barang && barang.barang.toLowerCase().includes(searchTerm.toLowerCase())) || // Mencocokkan nama barang.
(barang.kode_barang && barang.kode_barang.toLowerCase().includes(searchTerm.toLowerCase())) // Mencocokkan kode barang.
);

// Menambahkan barang ke keranjang dengan validasi stok.
const addToCart = (barang) => {
if (barang.stok <= 0) {
alert('Stok barang ini kosong. Tidak dapat menambah ke keranjang.'); // Alert jika stok kosong.
return;
}
setCartItems((prevItems) => {
const itemExists = prevItems.find((item) => item.kode_barang === barang.kode_barang); // Mengecek apakah barang sudah ada di keranjang.
if (itemExists) {
return prevItems.map((item) =>
item.kode_barang === barang.kode_barang && item.jumlah < barang.stok
? { ...item, jumlah: item.jumlah + 1 } // Menambah jumlah barang jika stok mencukupi.
: item
);
} else {
return [...prevItems, { ...barang, jumlah: 1, totalHarga: barang.harga_jual }]; // Menambahkan barang baru ke keranjang.
}
});
};

// Mengurangi jumlah barang di keranjang atau menghapusnya jika jumlah mencapai 0.
const removeFromCart = (kode_barang) => {
setCartItems((prevItems) => {
const itemExists = prevItems.find((item) => item.kode_barang === kode_barang);
if (itemExists && itemExists.jumlah > 1) {
return prevItems.map((item) =>
item.kode_barang === kode_barang
? { ...item, jumlah: item.jumlah - 1 } // Mengurangi jumlah barang.
: item
);
} else {
return prevItems.filter((item) => item.kode_barang !== kode_barang); // Menghapus barang dari keranjang jika jumlahnya 0.
}
});
};

// Menghitung total harga barang di keranjang setiap kali isi keranjang berubah.
useEffect(() => {
const total = cartItems.reduce((acc, item) => acc + item.totalHarga * item.jumlah, 0); // Mengakumulasi harga barang.
setTotalHarga(total); // Menyimpan total harga ke state.
}, [cartItems]); // Efek dijalankan setiap kali cartItems berubah.

// Mengubah harga total barang tertentu langsung di keranjang.
const handleTotalPriceChange = (kode_barang, newPrice) => {
setCartItems((prevItems) =>
prevItems.map((item) =>
item.kode_barang === kode_barang ? { ...item, totalHarga: newPrice } : item // Memperbarui harga total barang.
)
);
};

// Proses checkout untuk menyimpan data transaksi ke database.
const handleCheckout = async () => {
if (cartItems.length === 0) {
alert("Keranjang kosong. Tambahkan barang terlebih dahulu."); // Alert jika keranjang kosong.
return;
}

setLoading(true); // Menyalakan indikator loading.

// Mendapatkan tanggal dan waktu saat ini dalam format yang sesuai.
const todayDate = new Date().toISOString().split("T")[0]; // Format: 'YYYY-MM-DD'.
const waktuSekarang = new Date().toLocaleTimeString("id-ID", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).replace(/\./g, ":"); // Format: 'HH:MM:SS'.
const timestamp = `${todayDate} ${waktuSekarang}`; // Menggabungkan tanggal dan waktu.

// Menyimpan transaksi ke tabel 'penjualanbrg'.
const { data: penjualanData, error: penjualanError } = await supabase
.from('penjualanbrg')
.insert([{ total_harga: totalHarga, nama_pelanggan: namaPelanggan, tanggal: timestamp }])
.select()
.single();

if (penjualanError) {
console.error('Error creating penjualan:', penjualanError.message); // Log error jika gagal menyimpan transaksi.
alert('Gagal membuat transaksi penjualan. Silakan coba lagi.');
setLoading(false);
return;
}

// Menyimpan detail barang ke tabel 'detail_penjualan'.
const detailPenjualanData = cartItems.map((item) => ({
id_penjualanbrg: penjualanData.id_penjualanbrg,
kode_barang: item.kode_barang,
jumlah: item.jumlah,
harga_jual: item.totalHarga,
}));

const { error: detailError } = await supabase
.from('detail_penjualan')
.insert(detailPenjualanData);

if (detailError) {
console.error('Error creating detail penjualan:', detailError.message); // Log error jika gagal menyimpan detail transaksi.
alert('Gagal menyimpan detail transaksi penjualan.');
} else {
alert("Transaksi berhasil disimpan."); // Alert jika transaksi berhasil.
setCartItems([]); // Mengosongkan keranjang.
setTotalHarga(0); // Mereset total harga.
setNamaPelanggan(''); // Mereset nama pelanggan.

router.push('/cart/laporan'); // Redirect ke halaman laporan.
}

setLoading(false); // Mematikan indikator loading.
};

return (
<Layout>
<div className="min-h-screen bg-gray-100 p-4 flex flex-col items-center">
<h1 className="text-2xl font-bold mb-4">Kasir</h1>
{/* Customer Name Input */}
<div className="w-full max-w-md mb-4">
<input
type="text"
placeholder="Nama Pelanggan"
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
value={namaPelanggan}
onChange={(e) => setNamaPelanggan(e.target.value)}
/>
</div>


{/* Search Input */}
<div className="w-full max-w-md mb-4">
<input
type="text"
placeholder="Cari barang atau kode..."
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>

{/* Daftar Barang */}
<div className="grid grid-cols-2 gap-4 w-full max-w-md bg-white rounded-lg shadow-lg p-4 mb-6 overflow-y-auto" style={{ maxHeight: '400px' }}>
{loading ? (
<p>Loading...</p>
) : (
filteredBarangList.slice(0, 4).map((barang) => (
<div key={barang.kode_barang} className="flex items-center justify-between border-b py-2">
<div>
<h3 className="font-semibold text-sm">{barang.barang}</h3>
<p className="text-gray-500 text-xs">Rp {barang.harga_jual}</p>
<p className="text-gray-500 text-xs">Stok: {barang.stok}</p>
</div>
<button
onClick={() => addToCart(barang)}
className="bg-blue-500 text-white rounded-full p-2 hover:bg-blue-600"
>
+
</button>
</div>
))
)}
</div>

{/* Keranjang */}
<div className="w-full max-w-md bg-white rounded-lg shadow-lg p-4">
<h2 className="text-lg font-semibold mb-4">Keranjang</h2>
{cartItems.length > 0 ? (
cartItems.map((item) => (
<div key={item.kode_barang} className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-sm">{item.barang}</h3>
<p className="text-gray-500 text-xs">Jumlah: {item.jumlah}</p>
</div>
<div className="flex items-center space-x-2">
<input
type="number"
className="w-24 text-right"
value={item.totalHarga}
onChange={(e) => handleTotalPriceChange(item.kode_barang, Number(e.target.value))}
/>
<button
onClick={() => removeFromCart(item.kode_barang)}
className="bg-red-500 text-white rounded-full px-2 hover:bg-red-600"
>

</button>
</div>
</div>
))
) : (
<p className="text-center text-gray-600">Keranjang kosong</p>
)}

{/* Total Harga */}
<div className="flex justify-between items-center mt-4 pt-4 border-t border-gray-200">
<span className="text-lg font-semibold">Total:</span>
<span className="text-xl font-bold">Rp {totalHarga.toLocaleString()}</span>
</div>

{/* Tombol Checkout */}
<button
onClick={handleCheckout}
className="w-full bg-blue-600 text-white font-semibold py-2 rounded-lg mt-4 hover:bg-blue-700 transition disabled:opacity-50"
disabled={cartItems.length === 0 || loading}
>
{loading ? 'Proses...' : 'Checkout'}
</button>
</div>
</div>
</Layout>
);
}

--

--

Galih Setiawan Nurohim
Galih Setiawan Nurohim

No responses yet