/* ============================================================ MACP Solar โ€” Storefront logic Cart, filtering, search, checkout flow. Backend integration points are marked with >>> BACKEND HOOK <<< ============================================================ */ // ---- build the spinning sun rays on the logo tile ---- document.querySelectorAll('.tile').forEach(function (tile) { for (var i = 0; i < 8; i++) { var r = document.createElement('div'); r.className = 'r'; r.style.transform = 'translateX(-50%) rotate(' + (i * 45) + 'deg)'; tile.appendChild(r); } }); // ---- delivery regions (SADC + East Africa) ---- var SADC = [ { f: '๐Ÿ‡ฆ๐Ÿ‡ด', n: 'Angola' }, { f: '๐Ÿ‡ง๐Ÿ‡ผ', n: 'Botswana' }, { f: '๐Ÿ‡ฐ๐Ÿ‡ฒ', n: 'Comoros' }, { f: '๐Ÿ‡จ๐Ÿ‡ฉ', n: 'DR Congo' }, { f: '๐Ÿ‡ธ๐Ÿ‡ฟ', n: 'Eswatini' }, { f: '๐Ÿ‡ฑ๐Ÿ‡ธ', n: 'Lesotho' }, { f: '๐Ÿ‡ฒ๐Ÿ‡ฌ', n: 'Madagascar' }, { f: '๐Ÿ‡ฒ๐Ÿ‡ผ', n: 'Malawi' }, { f: '๐Ÿ‡ฒ๐Ÿ‡บ', n: 'Mauritius' }, { f: '๐Ÿ‡ฒ๐Ÿ‡ฟ', n: 'Mozambique' }, { f: '๐Ÿ‡ณ๐Ÿ‡ฆ', n: 'Namibia' }, { f: '๐Ÿ‡ธ๐Ÿ‡จ', n: 'Seychelles' }, { f: '๐Ÿ‡ฟ๐Ÿ‡ฆ', n: 'South Africa' }, { f: '๐Ÿ‡น๐Ÿ‡ฟ', n: 'Tanzania' }, { f: '๐Ÿ‡ฟ๐Ÿ‡ฒ', n: 'Zambia' }, { f: '๐Ÿ‡ฟ๐Ÿ‡ผ', n: 'Zimbabwe' } ]; var EAST_AFRICA = [ { f: '๐Ÿ‡ง๐Ÿ‡ฎ', n: 'Burundi' }, { f: '๐Ÿ‡ฐ๐Ÿ‡ช', n: 'Kenya' }, { f: '๐Ÿ‡ท๐Ÿ‡ผ', n: 'Rwanda' }, { f: '๐Ÿ‡ธ๐Ÿ‡ธ', n: 'South Sudan' }, { f: '๐Ÿ‡บ๐Ÿ‡ฌ', n: 'Uganda' }, { f: '๐Ÿ‡ธ๐Ÿ‡ด', n: 'Somalia' }, { f: '๐Ÿ‡ช๐Ÿ‡น', n: 'Ethiopia' }, { f: '๐Ÿ‡ฉ๐Ÿ‡ฏ', n: 'Djibouti' }, { f: '๐Ÿ‡ช๐Ÿ‡ท', n: 'Eritrea' } ]; // merged unique list for the checkout dropdown (sorted alphabetically) var DELIVERY_COUNTRIES = (function () { var seen = {}, out = []; SADC.concat(EAST_AFRICA).forEach(function (c) { if (!seen[c.n]) { seen[c.n] = 1; out.push(c); } }); out.sort(function (a, b) { return a.n.localeCompare(b.n); }); return out; })(); function buildRegionGrids() { function chips(list) { return list.map(function (c) { return '' + c.f + '' + c.n + ''; }).join(''); } var s = document.getElementById('sadcGrid'); var e = document.getElementById('eastGrid'); if (s) s.innerHTML = chips(SADC); if (e) e.innerHTML = chips(EAST_AFRICA); } var state = { tier: 'all', // all | res | com cat: 'all', // category name or 'all' query: '', sort: 'default' }; var cart = {}; // id -> {product, qty} var TIER_MAP = { Residential: 'res', Commercial: 'com' }; function money(n) { return 'R' + n.toLocaleString('en-ZA'); } // ---- category sidebar ---- function buildCatList() { var counts = {}; PRODUCTS.forEach(function (p) { counts[p.category] = (counts[p.category] || 0) + 1; }); var el = document.getElementById('catList'); var html = ''; CATEGORY_ORDER.forEach(function (c) { if (!counts[c]) return; html += ''; }); el.innerHTML = html; } function selectCat(c) { state.cat = c; document.querySelectorAll('#catList button').forEach(function (b) { b.classList.toggle('on', b.dataset.c === c); }); render(); document.getElementById('shop').scrollIntoView({ behavior: 'smooth' }); } function filterTier(t) { state.tier = t; document.querySelectorAll('#tierToggle button').forEach(function (b) { b.classList.toggle('on', b.dataset.t === t); }); render(); } function resetFilters() { state.tier = 'all'; state.cat = 'all'; state.query = ''; document.getElementById('search').value = ''; document.querySelectorAll('#tierToggle button').forEach(function (b) { b.classList.toggle('on', b.dataset.t === 'all'); }); selectCat('all'); window.scrollTo({ top: 0, behavior: 'smooth' }); } // ---- filtering + render ---- function getFiltered() { var q = state.query.toLowerCase().trim(); var list = PRODUCTS.filter(function (p) { if (state.tier !== 'all' && TIER_MAP[p.tier] !== state.tier) return false; if (state.cat !== 'all' && p.category !== state.cat) return false; if (q) { var hay = (p.name + ' ' + p.vendor + ' ' + p.sku + ' ' + p.category).toLowerCase(); if (hay.indexOf(q) === -1) return false; } return true; }); if (state.sort === 'low') list.sort(function (a, b) { return a.price - b.price; }); else if (state.sort === 'high') list.sort(function (a, b) { return b.price - a.price; }); else if (state.sort === 'name') list.sort(function (a, b) { return a.name.localeCompare(b.name); }); return list; } function render() { state.sort = document.getElementById('sortSel').value; var list = getFiltered(); var grid = document.getElementById('grid'); // title + sub var title = state.cat === 'all' ? 'All products' : state.cat; var tierTxt = state.tier === 'res' ? 'Home' : state.tier === 'com' ? 'Business' : ''; document.getElementById('shopTitle').textContent = title; document.getElementById('shopSub').textContent = tierTxt ? tierTxt + ' range' : 'Browse the full MACP Solar range'; document.getElementById('resultCount').textContent = list.length + (list.length === 1 ? ' product' : ' products'); if (!list.length) { grid.innerHTML = '

No products match your filters.

'; return; } grid.innerHTML = list.map(function (p) { var tcls = p.tier === 'Residential' ? 'tier-res' : 'tier-com'; var inCart = cart[p.id]; return '' + '
' + '
' + '' + p.tier + '' + '' + esc(p.name) + '' + '
' + '
' + '
' + esc(p.vendor) + '
' + '
' + esc(p.name) + '
' + '
' + esc(p.sku) + '
' + '
' + '
' + money(p.price) + '
' + '' + '
' + '
' + '
'; }).join(''); } function esc(s) { return (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function jsq(s) { return (s || '').replace(/'/g, "\\'"); } // ---- search ---- document.getElementById('search').addEventListener('input', function (e) { state.query = e.target.value; render(); }); // ---- cart ---- function findProduct(id) { return PRODUCTS.find(function (p) { return p.id === id; }); } function addToCart(id) { var p = findProduct(id); if (!p) return; if (cart[id]) cart[id].qty++; else cart[id] = { product: p, qty: 1 }; updateCartUI(); render(); showToast('Added: ' + p.name); } function changeQty(id, delta) { if (!cart[id]) return; cart[id].qty += delta; if (cart[id].qty <= 0) delete cart[id]; updateCartUI(); render(); } function removeItem(id) { delete cart[id]; updateCartUI(); render(); } function cartArray() { return Object.keys(cart).map(function (k) { return cart[k]; }); } function cartCount() { return cartArray().reduce(function (s, i) { return s + i.qty; }, 0); } function cartSubtotal() { return cartArray().reduce(function (s, i) { return s + i.product.price * i.qty; }, 0); } function shippingCost(sub) { return 0; } // International delivery quoted separately after order function updateCartUI() { var count = cartCount(); var badge = document.getElementById('cartCount'); badge.textContent = count; badge.style.display = count ? 'flex' : 'none'; var items = document.getElementById('ditems'); var arr = cartArray(); if (!arr.length) { items.innerHTML = '

Your cart is empty.
Add some solar gear to get started.

'; document.getElementById('checkoutBtn').disabled = true; } else { items.innerHTML = arr.map(function (it) { var p = it.product; return '
' + '
' + esc(p.name) + '
' + '
' + '
' + esc(p.name) + '
' + '
' + money(p.price) + '
' + '
' + '' + '' + it.qty + '' + '' + '' + '
' + '
'; }).join(''); document.getElementById('checkoutBtn').disabled = false; } var sub = cartSubtotal(); var ship = shippingCost(sub); document.getElementById('cartSub').textContent = money(sub); document.getElementById('cartShip').textContent = sub === 0 ? 'โ€”' : 'Quoted (intl.)'; document.getElementById('cartTotal').textContent = money(sub + ship); } function openCart() { document.getElementById('drawer').classList.add('show'); document.getElementById('scrim').classList.add('show'); } function closeCart() { document.getElementById('drawer').classList.remove('show'); document.getElementById('scrim').classList.remove('show'); } // ---- toast ---- var toastTimer; function showToast(msg) { document.getElementById('toastMsg').textContent = msg; var t = document.getElementById('toast'); t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(function () { t.classList.remove('show'); }, 2200); } /* ============================================================ CHECKOUT ============================================================ */ function openCheckout() { closeCart(); renderCheckoutForm(); document.getElementById('modal').classList.add('show'); } function closeModal() { document.getElementById('modal').classList.remove('show'); } function renderCheckoutForm() { var sub = cartSubtotal(); var ship = shippingCost(sub); var total = sub + ship; var panel = document.getElementById('modalPanel'); panel.innerHTML = '

Checkout

' + cartCount() + ' item(s) ยท secure order

' + '
' + '
' + '
' + '
Subtotal' + money(sub) + '
' + '
DeliveryQuoted after order (intl.)
' + '
Total' + money(total) + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
๐Ÿ“ฆ We deliver to SADC and East African countries. South African customers can pay online; international customers must place orders via email for a shipping quote.
' + '
Demo checkout. No real payment is taken. Connect a payment provider ' + '(Stripe / Paystack / PayFast) at the processPayment() hook in app.js to go live.
' + '
' + '' + '
' + '
'; } // Switch between online pay (South Africa) and email order (international) function onCountryChange() { var sel = document.getElementById('f_country'); var action = document.getElementById('checkoutAction'); if (!sel || !action) return; var isSA = sel.value.indexOf('South Africa') > -1; var total = cartSubtotal(); if (isSA) { action.innerHTML = ''; } else { action.innerHTML = '
' + '๐ŸŒ International order. Orders outside South Africa are placed by email so we can confirm shipping and a delivery quote.
' + ''; } } // Build a pre-filled email containing the cart for international customers function emailOrder() { var sel = document.getElementById('f_country'); var country = sel ? sel.value : ''; if (!country) { showToast('Please select your country'); return; } var lines = cartArray().map(function (i) { return '- ' + i.qty + ' x ' + i.product.name + ' (' + i.product.sku + ') @ ' + money(i.product.price); }).join('\n'); var body = 'Hello MACP Solar,\n\nI would like to place an international order.\n\n' + 'Name: ' + val('f_first') + ' ' + val('f_last') + '\n' + 'Email: ' + val('f_email') + '\n' + 'Phone: ' + val('f_phone') + '\n' + 'Delivery address: ' + val('f_addr') + ', ' + val('f_city') + ' ' + val('f_zip') + '\n' + 'Country: ' + country + '\n\n' + 'Items:\n' + lines + '\n\n' + 'Subtotal: ' + money(cartSubtotal()) + '\n' + 'Please send me a quote including international delivery. Thank you.'; var subject = 'International Solar Order โ€” ' + country; window.location.href = 'mailto:orders@macpsolar.com?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body); } /* LIVE PAYMENT Calls our Vercel serverless functions, which talk to the payment provider (Stripe or Paystack) using the secret key. The browser never sees the key. The provider is decided server-side via /api/config and the PAYMENT_PROVIDER env var. */ var PAY_PROVIDER = null; // resolved on load via /api/config async function loadProvider() { try { var r = await fetch('/api/config'); var d = await r.json(); PAY_PROVIDER = d.provider; } catch (e) { PAY_PROVIDER = 'none'; } } async function startPayment(order) { if (!PAY_PROVIDER) await loadProvider(); var endpoint = PAY_PROVIDER === 'stripe' ? '/api/stripe-initialize' : '/api/initialize-payment'; var res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: order.items, customer: order.customer }), }); var data = await res.json(); // Stripe returns { url }, Paystack returns { authorization_url } var redirect = data.url || data.authorization_url; if (!res.ok || !redirect) { throw new Error(data.error || 'Could not start payment.'); } window.location.href = redirect; // -> provider's hosted checkout } // When the provider redirects back with ?verify=1, confirm server-side. async function checkPaymentReturn() { var params = new URLSearchParams(window.location.search); if (!params.get('verify')) return; var provider = params.get('provider') === 'stripe' ? 'stripe' : 'paystack'; var verifyUrl, reference; if (provider === 'stripe') { var sid = params.get('session_id'); if (!sid) return; verifyUrl = '/api/stripe-verify?session_id=' + encodeURIComponent(sid); } else { reference = params.get('reference') || params.get('trxref'); if (!reference) return; verifyUrl = '/api/verify-payment?reference=' + encodeURIComponent(reference); } var data; try { var res = await fetch(verifyUrl); data = await res.json(); } catch (e) { data = { ok: false }; } // clean the URL window.history.replaceState({}, '', window.location.pathname); document.getElementById('modal').classList.add('show'); if (data && data.ok) { cart = {}; updateCartUI(); render(); document.getElementById('modalPanel').innerHTML = '
' + '
' + '

Payment received!

' + '

Thank you โ€” your order is confirmed and a receipt is on its way. ' + 'We\'ll be in touch about delivery shortly.

' + '
Ref: ' + esc(data.reference || reference || '') + '
' + '
' + '
'; } else { document.getElementById('modalPanel').innerHTML = '
' + '

Payment not completed

' + '

We couldn\'t confirm your payment. If money was deducted it will be reversed. ' + 'Please try again or email orders@macpsolar.com.

' + '
' + '
'; } } async function placeOrder() { var customer = { first: val('f_first'), last: val('f_last'), email: val('f_email'), phone: val('f_phone'), address: val('f_addr'), city: val('f_city'), zip: val('f_zip'), country: val('f_country') }; if (!customer.first || !customer.email || !customer.address || !customer.country) { showToast('Please fill in name, email, address and country'); return; } var order = { items: cartArray().map(function (i) { return { sku: i.product.sku, name: i.product.name, qty: i.qty, price: i.product.price }; }), subtotal: cartSubtotal(), shipping: shippingCost(cartSubtotal()), customer: customer }; var btn = event.target; btn.disabled = true; btn.textContent = 'Redirecting to secure paymentโ€ฆ'; try { await startPayment(order); // browser redirects to Paystack; nothing below runs on success } catch (err) { btn.disabled = false; btn.textContent = 'Try again'; showToast(err.message || 'Payment could not start'); } } function val(id) { var e = document.getElementById(id); return e ? e.value.trim() : ''; } // close modal on backdrop click document.getElementById('modal').addEventListener('click', function (e) { if (e.target === this) closeModal(); }); // ---- init ---- buildCatList(); buildRegionGrids(); updateCartUI(); render(); loadProvider(); checkPaymentReturn();