Initial commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
const Cart = (function () {
|
||||
const STORAGE_KEY = "cart";
|
||||
const TAX_RATE = 0.05;
|
||||
let couponDiscount = 0;
|
||||
|
||||
// ─────────────────────────────
|
||||
// Internal
|
||||
// ─────────────────────────────
|
||||
function getCart() {
|
||||
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
|
||||
}
|
||||
|
||||
function saveCart(cart) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(cart));
|
||||
window.dispatchEvent(new Event("cartUpdated"));
|
||||
}
|
||||
|
||||
function findItem(cart, id) {
|
||||
return cart.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
// ─────────────────────────────
|
||||
// Public API
|
||||
// ─────────────────────────────
|
||||
|
||||
function addItem(product, quantity = 1) {
|
||||
const cart = getCart();
|
||||
const existing = findItem(cart, product.id);
|
||||
|
||||
if (existing) {
|
||||
existing.quantity += quantity;
|
||||
} else {
|
||||
cart.push({
|
||||
id: product.id,
|
||||
name: product.basic.name,
|
||||
price: product.pricing.price,
|
||||
image: product.media.mainImage,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
saveCart(cart);
|
||||
}
|
||||
|
||||
function removeItem(id) {
|
||||
const cart = getCart().filter((item) => item.id !== id);
|
||||
saveCart(cart);
|
||||
}
|
||||
|
||||
function updateQuantity(id, quantity) {
|
||||
const cart = getCart();
|
||||
const item = findItem(cart, id);
|
||||
if (!item) return;
|
||||
|
||||
item.quantity = Math.max(1, quantity);
|
||||
saveCart(cart);
|
||||
}
|
||||
|
||||
function clear() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
window.dispatchEvent(new Event("cartUpdated"));
|
||||
}
|
||||
|
||||
function applyCoupon(code) {
|
||||
if (code === "MEDI10") {
|
||||
couponDiscount = 0.1;
|
||||
window.dispatchEvent(new Event("cartUpdated"));
|
||||
return true;
|
||||
}
|
||||
|
||||
couponDiscount = 0;
|
||||
window.dispatchEvent(new Event("cartUpdated"));
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTotals() {
|
||||
const cart = getCart();
|
||||
|
||||
const subtotal = cart.reduce(
|
||||
(sum, item) => sum + item.price * item.quantity,
|
||||
0
|
||||
);
|
||||
|
||||
const tax = subtotal * TAX_RATE;
|
||||
const discount = subtotal * couponDiscount;
|
||||
const total = subtotal - discount;
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
tax,
|
||||
discount,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
function getTotalCount() {
|
||||
return getCart().reduce((sum, item) => sum + item.quantity, 0);
|
||||
}
|
||||
|
||||
return {
|
||||
getCart,
|
||||
addItem,
|
||||
removeItem,
|
||||
updateQuantity,
|
||||
clear,
|
||||
applyCoupon,
|
||||
getTotals,
|
||||
getTotalCount,
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,43 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const cartItemsContainer = document.getElementById("checkout-items");
|
||||
const subtotalEl = document.getElementById("checkout-subtotal");
|
||||
const totalEl = document.getElementById("checkout-total");
|
||||
|
||||
const qrWrapper = document.getElementById("qr-wrapper");
|
||||
const form = document.getElementById("checkout-form");
|
||||
|
||||
const cart = Cart.getCart();
|
||||
|
||||
if (cart.length === 0) {
|
||||
cartItemsContainer.innerHTML = "<p>Your cart is empty.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
let subtotal = 0;
|
||||
|
||||
cart.forEach((item) => {
|
||||
subtotal += item.price * item.quantity;
|
||||
|
||||
const row = document.createElement("div");
|
||||
row.className = "summary-row";
|
||||
row.innerHTML = `
|
||||
<span>${item.name} × ${item.quantity}</span>
|
||||
<span>₹${(item.price * item.quantity).toFixed(2)}</span>
|
||||
`;
|
||||
cartItemsContainer.appendChild(row);
|
||||
});
|
||||
|
||||
const total = subtotal;
|
||||
|
||||
subtotalEl.textContent = `₹${subtotal.toFixed(2)}`;
|
||||
totalEl.textContent = `₹${total.toFixed(2)}`;
|
||||
|
||||
// Unlock QR on valid form submit
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!form.checkValidity()) return;
|
||||
|
||||
qrWrapper.classList.remove("blurred");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
async function loadComponent(id, file) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const res = await fetch(file);
|
||||
const html = await res.text();
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
async function initComponents() {
|
||||
console.log("init");
|
||||
await Promise.all([
|
||||
loadComponent("topbar-placeholder", "components/topbar.html"),
|
||||
loadComponent("navbar-placeholder", "components/navbar.html"),
|
||||
loadComponent("footer-placeholder", "components/footer.html"),
|
||||
loadComponent("toast-placeholder", "components/toast.html"),
|
||||
]);
|
||||
|
||||
// ✅ DOM is fully ready now — safe to run these
|
||||
setActiveNavLink();
|
||||
updateCartCount();
|
||||
|
||||
// ✅ Tell cart.js it can now render
|
||||
document.dispatchEvent(new Event("components:ready"));
|
||||
|
||||
const menuToggle = document.getElementById("menu-toggle");
|
||||
const mobileMenu = document.getElementById("mobile-menu");
|
||||
|
||||
if (!menuToggle || !mobileMenu) return;
|
||||
|
||||
menuToggle.addEventListener("click", () => {
|
||||
mobileMenu.classList.toggle("active");
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveNavLink() {
|
||||
const page = window.location.pathname.split("/").pop() || "index.html";
|
||||
document.querySelectorAll(".nav-links a").forEach((a) => {
|
||||
a.parentElement.classList.toggle("active", a.getAttribute("href") === page);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCartCount() {
|
||||
const cart = JSON.parse(localStorage.getItem("cart")) || [];
|
||||
const total = cart.reduce((s, i) => s + i.quantity, 0);
|
||||
const el = document.getElementById("nav-cart-count");
|
||||
if (el) el.textContent = total;
|
||||
}
|
||||
|
||||
initComponents();
|
||||
@@ -0,0 +1,300 @@
|
||||
function renderRelatedProducts(product) {
|
||||
const container = document.querySelector(".related-grid");
|
||||
if (!container || !product.relatedProductIds) return;
|
||||
|
||||
const relatedProducts = product.relatedProductIds
|
||||
.map((id) => PRODUCTS.find((p) => p.id === id))
|
||||
.filter(Boolean);
|
||||
|
||||
container.innerHTML = relatedProducts
|
||||
.map(
|
||||
(p) => `
|
||||
<div class="product-card">
|
||||
<div class="product-img">
|
||||
${
|
||||
p.pricing.discountPercent
|
||||
? `<div class="product-disc">-${p.pricing.discountPercent}%</div>`
|
||||
: ""
|
||||
}
|
||||
<img
|
||||
src="${p.media.mainImage}"
|
||||
alt="${p.basic.name}"
|
||||
width="200"
|
||||
height="200"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
draggable="false" />
|
||||
</div>
|
||||
<div class="product-body">
|
||||
<div class="product-name">${p.basic.name}</div>
|
||||
<div class="product-rating">★★★★★</div>
|
||||
<div class="product-price">
|
||||
<span class="rp-now">₹${p.pricing.price}</span>
|
||||
${
|
||||
p.pricing.originalPrice
|
||||
? `<span class="rp-old">₹${p.pricing.originalPrice}</span>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
<a href="product.html?id=${
|
||||
p.id
|
||||
}" target="_blank" class="product-add-btn">
|
||||
View Product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderProduct(productId) {
|
||||
const product = PRODUCTS.find((p) => p.id === productId);
|
||||
|
||||
if (!product) {
|
||||
console.error("Product not found:", productId);
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// BASIC INFO
|
||||
// =========================
|
||||
const { basic, pricing, media, rating } = product;
|
||||
|
||||
// Title
|
||||
document.title = product.seo?.title || basic.name;
|
||||
document.querySelector(".product-title").textContent = basic.name;
|
||||
|
||||
// Breadcrumb
|
||||
const breadcrumbCurrent = document.querySelector(".breadcrumb-bar .current");
|
||||
if (breadcrumbCurrent)
|
||||
breadcrumbCurrent.textContent = basic.shortName || basic.name;
|
||||
|
||||
// Image
|
||||
const mainImg = document.querySelector(".gallery-main img");
|
||||
if (mainImg && media?.mainImage) {
|
||||
mainImg.src = media.mainImage;
|
||||
mainImg.alt = basic.name;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// PRICING
|
||||
// =========================
|
||||
document.querySelector(".price-current").textContent = `₹${pricing.price}`;
|
||||
document.querySelector(".price-original").textContent = pricing.originalPrice
|
||||
? `₹${pricing.originalPrice}`
|
||||
: "";
|
||||
|
||||
document.querySelector(".price-badge").textContent = pricing.discountPercent
|
||||
? `-${pricing.discountPercent}%`
|
||||
: "";
|
||||
|
||||
// Stock
|
||||
const stockStatus = document.querySelector(".stock-status");
|
||||
if (stockStatus) {
|
||||
stockStatus.innerHTML = `
|
||||
<span class="stock-dot"></span>
|
||||
${pricing.stockStatus} — Ships within ${pricing.shipsWithin}
|
||||
`;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// RATING
|
||||
// =========================
|
||||
if (rating) {
|
||||
document.querySelector(".rating-count").textContent =
|
||||
rating.average.toFixed(1);
|
||||
document.querySelector(
|
||||
".review-link"
|
||||
).textContent = `${rating.totalReviews} Reviews`;
|
||||
|
||||
const soldText = document.querySelector(".rating-row span[style]");
|
||||
if (soldText) soldText.textContent = `${rating.sold} sold`;
|
||||
|
||||
// Reviews tab count
|
||||
const reviewTabBtn = document.querySelectorAll(".tab-nav-btn")[2];
|
||||
if (reviewTabBtn)
|
||||
reviewTabBtn.textContent = `Reviews (${rating.totalReviews})`;
|
||||
|
||||
// Big rating
|
||||
const scoreEl = document.querySelector(".rating-big .score");
|
||||
if (scoreEl) scoreEl.textContent = rating.average.toFixed(1);
|
||||
|
||||
const totalEl = document.querySelector(".rating-big .total");
|
||||
if (totalEl)
|
||||
totalEl.textContent = `Based on ${rating.totalReviews} reviews`;
|
||||
|
||||
// Rating bars
|
||||
const bars = document.querySelectorAll(".rating-bar-row");
|
||||
bars.forEach((bar) => {
|
||||
const label = bar.querySelector(".bar-label").textContent.trim()[0];
|
||||
const count = rating.breakdown[label] || 0;
|
||||
const percent = rating.totalReviews
|
||||
? (count / rating.totalReviews) * 100
|
||||
: 0;
|
||||
|
||||
bar.querySelector(".bar-fill").style.width = `${percent}%`;
|
||||
bar.querySelector(".bar-count").textContent = count;
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// SUMMARY
|
||||
// =========================
|
||||
document.querySelector(".product-short-desc").textContent =
|
||||
product.summary.shortDescription;
|
||||
|
||||
// Benefit pills
|
||||
const pillsContainer = document.querySelector(".benefit-pills");
|
||||
if (pillsContainer) {
|
||||
pillsContainer.innerHTML = product.summary.benefitPills
|
||||
.map((p) => `<span class="benefit-pill">${p.icon} ${p.text}</span>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Meta
|
||||
document.querySelector(".meta-row span:last-child").textContent = basic.sku;
|
||||
document
|
||||
.querySelectorAll(".meta-row")[1]
|
||||
.querySelector("span:last-child").textContent = basic.weight;
|
||||
document
|
||||
.querySelectorAll(".meta-row")[2]
|
||||
.querySelector("span:last-child").textContent = `₹${pricing.price}`;
|
||||
|
||||
// Categories
|
||||
const categoryContainer = document
|
||||
.querySelectorAll(".meta-row")[3]
|
||||
.querySelector("div");
|
||||
categoryContainer.innerHTML = basic.category
|
||||
.map((c) => `<span class="meta-tag">${c}</span>`)
|
||||
.join("");
|
||||
|
||||
// Tags
|
||||
const tagContainer = document
|
||||
.querySelectorAll(".meta-row")[4]
|
||||
.querySelector("div");
|
||||
tagContainer.innerHTML = basic.tags
|
||||
.map((t) => `<span class="meta-tag">${t}</span>`)
|
||||
.join("");
|
||||
|
||||
// Ideal for
|
||||
document
|
||||
.querySelectorAll(".meta-row")[5]
|
||||
.querySelector("span:last-child").textContent = basic.idealFor;
|
||||
|
||||
// =========================
|
||||
// DESCRIPTION TAB
|
||||
// =========================
|
||||
const descTab = product.descriptionTab;
|
||||
|
||||
document.querySelector(".desc-lead").textContent = descTab.lead;
|
||||
|
||||
const descBody = document.querySelector(".desc-body");
|
||||
descBody.innerHTML = descTab.paragraphs.map((p) => `<p>${p}</p>`).join("");
|
||||
|
||||
// Features
|
||||
const featuresContainer = document.querySelector(".desc-features");
|
||||
featuresContainer.innerHTML = descTab.features
|
||||
.map(
|
||||
(f) => `
|
||||
<div class="desc-feature">
|
||||
<div class="feat-icon">${f.icon}</div>
|
||||
<div class="feat-text">
|
||||
<strong>${f.title}</strong>
|
||||
<span>${f.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// How To Use
|
||||
const howSteps = document.querySelector(".how-steps");
|
||||
howSteps.innerHTML = descTab.howToUse
|
||||
.map(
|
||||
(step, i) => `
|
||||
<div class="how-step">
|
||||
<div class="step-num">${i + 1}</div>
|
||||
${step}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// =========================
|
||||
// INGREDIENTS TAB
|
||||
// =========================
|
||||
const ingTab = product.ingredientsTab;
|
||||
|
||||
document
|
||||
.querySelectorAll(".tab-pane")[1]
|
||||
.querySelector(".desc-lead").textContent = ingTab.lead;
|
||||
|
||||
const ingGrid = document.querySelector(".ingredients-grid");
|
||||
ingGrid.innerHTML = ingTab.ingredients
|
||||
.map(
|
||||
(i) => `
|
||||
<div class="ingredient-card">
|
||||
<div class="ing-icon">${i.icon}</div>
|
||||
<div class="ing-name">${i.name}</div>
|
||||
<div class="ing-role">${i.role}</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
document.querySelector(
|
||||
".full-ingredients"
|
||||
).innerHTML = `<strong>Full Ingredient List:</strong> ${ingTab.fullIngredients}`;
|
||||
|
||||
// =========================
|
||||
// REVIEWS TAB
|
||||
// =========================
|
||||
const reviewList = document.querySelector(".review-list");
|
||||
reviewList.innerHTML = product.reviews
|
||||
.map(
|
||||
(r) => `
|
||||
<div class="review-card">
|
||||
<div class="review-header">
|
||||
<div class="reviewer-info">
|
||||
<div class="reviewer-avatar">${r.avatarLetter}</div>
|
||||
<div>
|
||||
<div class="reviewer-name">${r.name}</div>
|
||||
<div class="reviewer-date">${r.date}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-stars">${"★".repeat(r.rating)}${"☆".repeat(
|
||||
5 - r.rating
|
||||
)}</div>
|
||||
</div>
|
||||
<div class="review-title">${r.title}</div>
|
||||
<div class="review-text">${r.text}</div>
|
||||
${
|
||||
r.verified
|
||||
? `<div class="verified-badge">✓ Verified Purchase</div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
// =========================
|
||||
// SHIPPING TAB
|
||||
// =========================
|
||||
const shippingTab = product.shipping;
|
||||
const shippingLead = document
|
||||
.querySelectorAll(".tab-pane")[3]
|
||||
.querySelector(".desc-lead");
|
||||
shippingLead.textContent = shippingTab.lead;
|
||||
|
||||
const shippingBody = document
|
||||
.querySelectorAll(".tab-pane")[3]
|
||||
.querySelector(".desc-body");
|
||||
|
||||
shippingBody.innerHTML = shippingTab.policies
|
||||
.map((p) => `<p><strong>${p.title}:</strong> ${p.text}</p>`)
|
||||
.join("");
|
||||
|
||||
renderRelatedProducts(product);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user