';
}).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) + '
' +
'
' + 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.