OmahCitra store template: catalog, cart, checkout, shipping, payment (HTML/CSS/jQuery)
This commit is contained in:
250
js/app.js
Normal file
250
js/app.js
Normal file
@@ -0,0 +1,250 @@
|
||||
/* ============================================================
|
||||
OmahCitra Store - Aplikasi (jQuery)
|
||||
Fitur: Katalog, Keranjang, Checkout, Pengiriman, Pembayaran
|
||||
============================================================ */
|
||||
$(function () {
|
||||
"use strict";
|
||||
|
||||
// ---------- State ----------
|
||||
const cart = {}; // { id: qty }
|
||||
let activeCat = "Semua";
|
||||
let searchTerm = "";
|
||||
let selectedCourier = null;
|
||||
let selectedPayment = null;
|
||||
|
||||
// ---------- Helpers ----------
|
||||
const rupiah = (n) => "Rp" + Number(n).toLocaleString("id-ID");
|
||||
const $view = (name) => {
|
||||
$(".view").removeClass("active");
|
||||
$("#view-" + name).addClass("active");
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
const toast = (msg) => {
|
||||
const $t = $("#toast").text(msg).addClass("show");
|
||||
clearTimeout(toast._t);
|
||||
toast._t = setTimeout(() => $t.removeClass("show"), 1800);
|
||||
};
|
||||
|
||||
// ---------- Category nav ----------
|
||||
function buildCats() {
|
||||
const cats = ["Semua", ...new Set(PRODUCTS.map(p => p.category))];
|
||||
const $nav = $("#catNav").empty();
|
||||
cats.forEach(c => {
|
||||
$("<div>")
|
||||
.addClass("cat-pill" + (c === activeCat ? " active" : ""))
|
||||
.text(c)
|
||||
.on("click", function () {
|
||||
activeCat = c;
|
||||
buildCats();
|
||||
renderCatalog();
|
||||
})
|
||||
.appendTo($nav);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Catalog ----------
|
||||
function renderCatalog() {
|
||||
const grid = $("#productGrid").empty();
|
||||
const term = searchTerm.toLowerCase();
|
||||
const list = PRODUCTS.filter(p => {
|
||||
const okCat = activeCat === "Semua" || p.category === activeCat;
|
||||
const okSearch = !term ||
|
||||
p.name.toLowerCase().includes(term) ||
|
||||
p.category.toLowerCase().includes(term) ||
|
||||
p.desc.toLowerCase().includes(term);
|
||||
return okCat && okSearch;
|
||||
});
|
||||
|
||||
$("#emptyNote").toggle(list.length === 0);
|
||||
|
||||
list.forEach(p => {
|
||||
const card = $(`
|
||||
<div class="product-card">
|
||||
<div class="pc-thumb">${p.emoji}</div>
|
||||
<div class="pc-body">
|
||||
<span class="pc-cat">${p.category}${p.badge ? `<span class="pc-badge">${p.badge}</span>` : ""}</span>
|
||||
<div class="pc-name">${p.name}</div>
|
||||
<div class="pc-rating">★ ${p.rating} · ${p.sold.toLocaleString("id-ID")} terjual</div>
|
||||
<div class="pc-price">${rupiah(p.price)}</div>
|
||||
<button class="pc-btn" data-id="${p.id}">+ Keranjang</button>
|
||||
</div>
|
||||
</div>`);
|
||||
card.find(".pc-btn").on("click", () => addToCart(p.id));
|
||||
card.appendTo(grid);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Cart ops ----------
|
||||
function addToCart(id) {
|
||||
cart[id] = (cart[id] || 0) + 1;
|
||||
updateCartCount();
|
||||
toast("Ditambahkan ke keranjang");
|
||||
}
|
||||
function setQty(id, qty) {
|
||||
if (qty <= 0) delete cart[id];
|
||||
else cart[id] = qty;
|
||||
updateCartCount();
|
||||
renderCart();
|
||||
}
|
||||
function updateCartCount() {
|
||||
const n = Object.values(cart).reduce((a, b) => a + b, 0);
|
||||
$("#cartCount").text(n);
|
||||
}
|
||||
function cartSubtotal() {
|
||||
return Object.entries(cart).reduce((sum, [id, q]) => {
|
||||
const p = PRODUCTS.find(x => x.id === id);
|
||||
return sum + (p ? p.price * q : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ---------- Render cart ----------
|
||||
function renderCart() {
|
||||
const $list = $("#cartItems").empty();
|
||||
const ids = Object.keys(cart);
|
||||
|
||||
if (ids.length === 0) {
|
||||
$("#cartEmpty").show();
|
||||
$("#cartSummary").hide();
|
||||
return;
|
||||
}
|
||||
$("#cartEmpty").hide();
|
||||
$("#cartSummary").show();
|
||||
|
||||
ids.forEach(id => {
|
||||
const p = PRODUCTS.find(x => x.id === id);
|
||||
const qty = cart[id];
|
||||
const row = $(`
|
||||
<div class="cart-row">
|
||||
<div class="cart-emoji">${p.emoji}</div>
|
||||
<div class="cart-info">
|
||||
<h4>${p.name}</h4>
|
||||
<small>${rupiah(p.price)} / pcs</small>
|
||||
</div>
|
||||
<div class="qty">
|
||||
<button class="dec">−</button>
|
||||
<span>${qty}</span>
|
||||
<button class="inc">+</button>
|
||||
</div>
|
||||
<div class="price">${rupiah(p.price * qty)}</div>
|
||||
<button class="rm" title="Hapus">🗑️</button>
|
||||
</div>`);
|
||||
row.find(".inc").on("click", () => setQty(id, qty + 1));
|
||||
row.find(".dec").on("click", () => setQty(id, qty - 1));
|
||||
row.find(".rm").on("click", () => { delete cart[id]; updateCartCount(); renderCart(); toast("Dihapus"); });
|
||||
row.appendTo($list);
|
||||
});
|
||||
|
||||
$("#sumSubtotal").text(rupiah(cartSubtotal()));
|
||||
}
|
||||
|
||||
// ---------- Checkout ----------
|
||||
function buildCheckout() {
|
||||
// couriers
|
||||
const $c = $("#courierOptions").empty();
|
||||
COURIERS.forEach(co => {
|
||||
const opt = $(`
|
||||
<label class="opt">
|
||||
<input type="radio" name="courier" value="${co.id}">
|
||||
<span>${co.name} · ${rupiah(co.fee)}</span>
|
||||
<small>(${co.etd})</small>
|
||||
</label>`);
|
||||
opt.find("input").on("change", () => {
|
||||
selectedCourier = co;
|
||||
$(".opt", $c).removeClass("selected");
|
||||
opt.addClass("selected");
|
||||
updateOrderSummary();
|
||||
});
|
||||
opt.appendTo($c);
|
||||
});
|
||||
|
||||
// payments
|
||||
const $p = $("#paymentOptions").empty();
|
||||
PAYMENTS.forEach(pm => {
|
||||
const opt = $(`
|
||||
<label class="opt">
|
||||
<input type="radio" name="payment" value="${pm.id}">
|
||||
<span>${pm.name}</span>
|
||||
</label>`);
|
||||
opt.find("input").on("change", () => {
|
||||
selectedPayment = pm;
|
||||
$(".opt", $p).removeClass("selected");
|
||||
opt.addClass("selected");
|
||||
});
|
||||
opt.appendTo($p);
|
||||
});
|
||||
|
||||
renderOrderItems();
|
||||
updateOrderSummary();
|
||||
}
|
||||
|
||||
function renderOrderItems() {
|
||||
const $o = $("#orderItems").empty();
|
||||
Object.entries(cart).forEach(([id, q]) => {
|
||||
const p = PRODUCTS.find(x => x.id === id);
|
||||
$(`<div class="order-item"><span>${p.emoji} ${p.name} ×${q}</span><span>${rupiah(p.price * q)}</span></div>`).appendTo($o);
|
||||
});
|
||||
}
|
||||
|
||||
function updateOrderSummary() {
|
||||
const sub = cartSubtotal();
|
||||
const ship = selectedCourier ? selectedCourier.fee : 0;
|
||||
$("#coSubtotal").text(rupiah(sub));
|
||||
$("#coShipping").text(selectedCourier ? rupiah(ship) : "Pilih kurir");
|
||||
$("#coTotal").text(rupiah(sub + ship));
|
||||
}
|
||||
|
||||
// ---------- Place order ----------
|
||||
function placeOrder() {
|
||||
if (Object.keys(cart).length === 0) { toast("Keranjang kosong"); return; }
|
||||
const name = $("#custName").val().trim();
|
||||
const addr = $("#custAddr").val().trim();
|
||||
const phone = $("#custPhone").val().trim();
|
||||
if (!name || !addr || !phone) { toast("Lengkapi data penerima"); return; }
|
||||
if (!selectedCourier) { toast("Pilih kurir pengiriman"); return; }
|
||||
if (!selectedPayment) { toast("Pilih metode pembayaran"); return; }
|
||||
|
||||
const orderNo = "OC" + Date.now().toString().slice(-8);
|
||||
const sub = cartSubtotal();
|
||||
const total = sub + selectedCourier.fee;
|
||||
|
||||
$("#orderNo").text(orderNo);
|
||||
$("#doneDetail").html(`
|
||||
<div><strong>Nama:</strong> ${name}</div>
|
||||
<div><strong>Alamat:</strong> ${addr}</div>
|
||||
<div><strong>HP:</strong> ${phone}</div>
|
||||
<div><strong>Kurir:</strong> ${selectedCourier.name} (${selectedCourier.etd})</div>
|
||||
<div><strong>Pembayaran:</strong> ${selectedPayment.name}</div>
|
||||
<div><strong>Total:</strong> ${rupiah(total)}</div>
|
||||
`);
|
||||
|
||||
// reset
|
||||
for (const k in cart) delete cart[k];
|
||||
updateCartCount();
|
||||
selectedCourier = null; selectedPayment = null;
|
||||
$("#custName,#custAddr,#custPhone").val("");
|
||||
$view("done");
|
||||
}
|
||||
|
||||
// ---------- Events ----------
|
||||
$("#cartBtn").on("click", () => { renderCart(); $view("cart"); });
|
||||
$("#toCheckout").on("click", () => {
|
||||
if (Object.keys(cart).length === 0) { toast("Keranjang kosong"); return; }
|
||||
buildCheckout(); $view("checkout");
|
||||
});
|
||||
$("#placeOrder").on("click", placeOrder);
|
||||
$("#searchInput").on("input", function () { searchTerm = $(this).val(); renderCatalog(); });
|
||||
|
||||
// navigasi via data-view (brand & tombol)
|
||||
$(document).on("click", "[data-view]", function (e) {
|
||||
e.preventDefault();
|
||||
const v = $(this).data("view");
|
||||
if (v === "cart") renderCart();
|
||||
if (v === "checkout") buildCheckout();
|
||||
$view(v);
|
||||
});
|
||||
|
||||
// ---------- Init ----------
|
||||
buildCats();
|
||||
renderCatalog();
|
||||
updateCartCount();
|
||||
});
|
||||
2
js/jquery-3.7.1.min.js
vendored
Normal file
2
js/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
163
js/products.js
Normal file
163
js/products.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/* ============================================================
|
||||
OmahCitra Store - Daftar produk paling laku di Indonesia
|
||||
Data statis (simulasi) - diurutkan berdasar kategori terlaris
|
||||
============================================================ */
|
||||
const PRODUCTS = [
|
||||
// --- Elektronik & Smartphone (kategori paling laku) ---
|
||||
{
|
||||
id: "p01",
|
||||
name: "Samsung Galaxy A55 5G",
|
||||
category: "Elektronik",
|
||||
price: 4299000,
|
||||
rating: 4.7,
|
||||
sold: 12400,
|
||||
badge: "Terlaris",
|
||||
emoji: "📱",
|
||||
desc: "HP 5G RAM 8GB/256GB, kamera 50MP, layar Super AMOLED 120Hz."
|
||||
},
|
||||
{
|
||||
id: "p02",
|
||||
name: "Xiaomi Redmi Note 13 Pro",
|
||||
category: "Elektronik",
|
||||
price: 3299000,
|
||||
rating: 4.6,
|
||||
sold: 9820,
|
||||
badge: "Populer",
|
||||
emoji: "📱",
|
||||
desc: "Snapdragon 7s Gen 2, kamera 200MP, baterai 5000mAh fast charge."
|
||||
},
|
||||
{
|
||||
id: "p03",
|
||||
name: "Apple AirPods Gen 2",
|
||||
category: "Elektronik",
|
||||
price: 1499000,
|
||||
rating: 4.8,
|
||||
sold: 15600,
|
||||
badge: "Terlaris",
|
||||
emoji: "🎧",
|
||||
desc: "Earbuds nirkabel dengan chip H1, audio berkualitas & low latency."
|
||||
},
|
||||
{
|
||||
id: "p04",
|
||||
name: "Charger Adaptor 33W Fast Charge",
|
||||
category: "Elektronik",
|
||||
price: 129000,
|
||||
rating: 4.5,
|
||||
sold: 22100,
|
||||
badge: "",
|
||||
emoji: "🔌",
|
||||
desc: "Adaptor GAN cepat universal, kompatibel banyak merek HP."
|
||||
},
|
||||
|
||||
// --- Fashion & Kecantikan ---
|
||||
{
|
||||
id: "p05",
|
||||
name: "Hoodie Unisex Oversize",
|
||||
category: "Fashion",
|
||||
price: 189000,
|
||||
rating: 4.4,
|
||||
sold: 18900,
|
||||
badge: "Trending",
|
||||
emoji: "👕",
|
||||
desc: "Hoodie katun tebal nyaman, cocok pria & wanita, banyak warna."
|
||||
},
|
||||
{
|
||||
id: "p06",
|
||||
name: "Skincare Serum Vitamin C",
|
||||
category: "Kecantikan",
|
||||
price: 95000,
|
||||
rating: 4.6,
|
||||
sold: 27300,
|
||||
badge: "Terlaris",
|
||||
emoji: "🧴",
|
||||
desc: "Serum wajah brightening 30ml, cerahkan & haluskan kulit."
|
||||
},
|
||||
{
|
||||
id: "p07",
|
||||
name: "Sneakers Casual Pria",
|
||||
category: "Fashion",
|
||||
price: 259000,
|
||||
rating: 4.3,
|
||||
sold: 11200,
|
||||
badge: "",
|
||||
emoji: "👟",
|
||||
desc: "Sepatu sneakers ringan anti-slip, sol empuk untuk daily use."
|
||||
},
|
||||
|
||||
// --- Makanan & Minuman (FMCG paling laku) ---
|
||||
{
|
||||
id: "p08",
|
||||
name: "Indomie Goreng 40 Pcs",
|
||||
category: "Makanan",
|
||||
price: 138000,
|
||||
rating: 4.9,
|
||||
sold: 41200,
|
||||
badge: "Terlaris",
|
||||
emoji: "🍜",
|
||||
desc: "Mi instan goreng kemasan ekonomis, rasa favorit Indonesia."
|
||||
},
|
||||
{
|
||||
id: "p09",
|
||||
name: "Kopi Susu ABC 10 Sachet",
|
||||
category: "Makanan",
|
||||
price: 45000,
|
||||
rating: 4.7,
|
||||
sold: 33800,
|
||||
badge: "",
|
||||
emoji: "☕",
|
||||
desc: "Kopi susu kemasan sachet, praktis & nikmat untuk sahur/ngopi."
|
||||
},
|
||||
{
|
||||
id: "p10",
|
||||
name: "Teh Botol Sosro 350ml (12 botol)",
|
||||
category: "Makanan",
|
||||
price: 72000,
|
||||
rating: 4.8,
|
||||
sold: 29500,
|
||||
badge: "Populer",
|
||||
emoji: "🍵",
|
||||
desc: "Teh hijau siap minum original, segar dan tanpa pengawet berlebih."
|
||||
},
|
||||
|
||||
// --- Rumah Tangga ---
|
||||
{
|
||||
id: "p11",
|
||||
name: "Rice Cooker 1.8L Anti Lengket",
|
||||
category: "Rumah Tangga",
|
||||
price: 265000,
|
||||
rating: 4.5,
|
||||
sold: 14300,
|
||||
badge: "",
|
||||
emoji: "🍚",
|
||||
desc: "Magic com hemat listrik, memasak otomatis & keep warm."
|
||||
},
|
||||
{
|
||||
id: "p12",
|
||||
name: "Deterjen Bubuk 3kg",
|
||||
category: "Rumah Tangga",
|
||||
price: 58000,
|
||||
rating: 4.6,
|
||||
sold: 26700,
|
||||
badge: "Populer",
|
||||
emoji: "🧼",
|
||||
desc: "Deterjen wangi tahan lama, cocok mesin cuci & cuci tangan."
|
||||
}
|
||||
];
|
||||
|
||||
// Opsi kurir / pengiriman
|
||||
const COURIERS = [
|
||||
{ id: "jne", name: "JNE Regular", fee: 15000, etd: "2-3 hari" },
|
||||
{ id: "sicepat", name: "SiCepat BEST", fee: 18000, etd: "1-2 hari" },
|
||||
{ id: "pos", name: "POS Indonesia", fee: 12000, etd: "3-5 hari" },
|
||||
{ id: "gosend", name: "GoSend Same Day", fee: 35000, etd: "Hari ini" }
|
||||
];
|
||||
|
||||
// Opsi metode pembayaran
|
||||
const PAYMENTS = [
|
||||
{ id: "qris", name: "QRIS (All e-wallet & m-banking)" },
|
||||
{ id: "gopay", name: "GoPay" },
|
||||
{ id: "ovo", name: "OVO" },
|
||||
{ id: "dana", name: "DANA" },
|
||||
{ id: "transfer", name: "Transfer Bank (BCA / Mandiri / BRI)" },
|
||||
{ id: "cod", name: "Bayar di Tempat (COD)" }
|
||||
];
|
||||
Reference in New Issue
Block a user