(function () { // Load the marketplace design system + page styles if (!document.querySelector('link[href*="/markets/style.css"]')) { var l = document.createElement('link'); l.rel = 'stylesheet'; l.href = '/markets/style.css'; document.head.appendChild(l); } if (!document.getElementById('mss-css')) { var s = document.createElement('style'); s.id = 'mss-css'; s.textContent = ` /* ===== ROY4D Bidding pages (mss) - premium UI ===== */ .mkt-mss-body { background: var(--mkt-bg) !important; } #mss-app { min-height: 100dvh; min-height: 100vh; display: flex; flex-direction: column; background: linear-gradient(180deg, var(--mkt-bg) 0%, var(--mkt-bg-2) 100%); } .mkt-mss-top { position: sticky; top: 0; z-index: 30; display: flex; align-items: center; gap: .5rem; padding: calc(env(safe-area-inset-top, 0px) + .7rem) .85rem .7rem; background: var(--mkt-header); -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6); border-bottom: 1px solid var(--mkt-border); } .mkt-mss-title { flex: 1 1 auto; text-align: center; font-size: 1.1rem; font-weight: 800; color: var(--mkt-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .mkt-mss-main { flex: 1 1 auto; width: 100%; max-width: 720px; margin: 0 auto; padding: 1rem .85rem calc(104px + env(safe-area-inset-bottom, 0px)); } .mkt-mss-foot { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; padding: .6rem .85rem calc(env(safe-area-inset-bottom, 0px) + .6rem); background: var(--mkt-dock); -webkit-backdrop-filter: blur(18px) saturate(1.5); backdrop-filter: blur(18px) saturate(1.5); border-top: 1px solid var(--mkt-border); } #Amaka .mkt-btn { margin-bottom: 0; } .mkt-mss-checkout { position: absolute; bottom: calc(100% + .5rem); left: .75rem; right: .75rem; border-radius: 16px; } .mkt-mss-checkout:empty { display: none; } .mkt-mss-view { display: flex; flex-direction: column; gap: 1rem; } .mkt-pdct-hero { position: relative; overflow: hidden; border-radius: 24px; padding: 1.1rem 1.1rem 1rem; background: var(--mkt-card); border: 1px solid var(--mkt-border); box-shadow: var(--mkt-shadow); } .mkt-pdct-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-bottom: .6rem; } .mkt-pdct-badge { display: inline-flex; align-items: center; gap: .3rem; font-size: .62rem; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: #fff; background: var(--mkt-grad); padding: .3rem .62rem; border-radius: 999px; } .mkt-pdct-count { display: inline-flex; align-items: center; gap: .35rem; font-size: .74rem; font-weight: 700; color: var(--mkt-danger); background: var(--mkt-pink-soft); padding: .3rem .6rem; border-radius: 999px; } .mkt-pdct-name { font-size: 1.2rem; font-weight: 800; color: var(--mkt-text); margin: 0 0 .9rem; line-height: 1.25; } .mkt-pdct-imgwrap { display: grid; place-items: center; min-height: 220px; border-radius: 16px; overflow: hidden; background: var(--mkt-card-2); border: 1px solid var(--mkt-border); } .mkt-pdct-img { max-width: 100%; max-height: 280px; object-fit: contain; } .mkt-pdct-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; margin-top: .9rem; } .mkt-pdct-chip { display: flex; flex-direction: column; gap: .15rem; padding: .6rem .7rem; border-radius: 14px; background: var(--mkt-card-2); border: 1px solid var(--mkt-border); text-align: center; } .mkt-pdct-chip span { font-size: .6rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--mkt-muted); } .mkt-pdct-chip b { font-size: .95rem; font-weight: 800; color: var(--mkt-text); } .mkt-pdct-chip b s { color: var(--mkt-muted); } .mkt-pdct-actions { display: grid; grid-template-columns: repeat(3, 1fr); gap: .5rem; margin-top: .8rem; } .mkt-pill { display: inline-flex; align-items: center; justify-content: center; gap: .3rem; padding: .55rem .4rem; border-radius: 12px; border: 1px solid var(--mkt-border); background: var(--mkt-card-2); color: var(--mkt-text); font-size: .78rem; font-weight: 700; cursor: pointer; transition: transform var(--mkt-transition), border-color var(--mkt-transition), background var(--mkt-transition); } .mkt-pill:hover { border-color: var(--mkt-accent); background: var(--mkt-accent-soft); } .mkt-pill:active { transform: scale(.96); } .mkt-bids { border-radius: 20px; overflow: hidden; background: var(--mkt-card); border: 1px solid var(--mkt-border); box-shadow: var(--mkt-shadow); } .mkt-bids-head { display: flex; align-items: center; justify-content: space-between; padding: .85rem 1rem; background: var(--mkt-card-2); border-bottom: 1px solid var(--mkt-border); } .mkt-bids-head h3 { font-size: 1rem; font-weight: 800; margin: 0; color: var(--mkt-text); } .mkt-bids-live { display: inline-flex; align-items: center; gap: .35rem; font-size: .64rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--mkt-green); } .mkt-bids-live::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--mkt-green); animation: mktPulse 1.6s ease-in-out infinite; } .mkt-bids-area { padding: .4rem .6rem .6rem; min-height: 80px; } .mkt-bids-empty { padding: 1.5rem 1rem; text-align: center; color: var(--mkt-muted); font-size: .88rem; } #hedex { list-style: none; margin: 0; padding: 0; } #hedex .mkt-bid { display: flex; align-items: center; gap: .6rem; padding: .65rem .8rem; margin-bottom: .45rem; border: 1px solid var(--mkt-border); border-radius: 12px; background: var(--mkt-card-2); color: var(--mkt-text); font-size: .85rem; animation: mktFadeUp .3s ease both; } .mkt-bid-ava { display: grid; place-items: center; width: 34px; height: 34px; flex: none; border-radius: 50%; font-size: .8rem; font-weight: 800; color: #fff; background: var(--mkt-grad); } .mkt-bid-name { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; line-height: 1.2; } .mkt-bid-name b { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .mkt-bid-name code { align-self: flex-start; background: var(--mkt-accent-soft); color: var(--mkt-accent); padding: .05rem .4rem; border-radius: 6px; font-size: .68rem; margin-top: .1rem; } .mkt-bid-ref { flex: none; font-size: .7rem; font-weight: 700; color: var(--mkt-muted); background: var(--mkt-card); border: 1px solid var(--mkt-border); padding: .25rem .5rem; border-radius: 8px; } @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } } `; document.head.appendChild(s); } function applyTheme() { try { document.documentElement.dataset.mktTheme = (window.theme === 'dark') ? 'dark' : 'light'; } catch (e) {} } applyTheme(); document.addEventListener('DOMContentLoaded', applyTheme); if (window.themeListeners) window.themeListeners['mktMss'] = applyTheme; })(); class Mission { constructor(args = null) { this.args = args; this.config = { products: { tv: { dn: 'Smart TV\'s', bp: 100, gpd: 10 }, ha: { dn: 'Home Appliances', bp: 150, gpd: 12 }, as: { dn: 'Audio Systems', bp: 100, gpd: 50 }, lp: { dn: 'Laptops', bp: 150, gpd: 100 }, sp: { dn: 'SmartPhones', bp: 150, gpd: 20 } } } this.create(); this.spinner = ``; self.onnetrestoreEvents = self.onnetrestoreEvents ?? []; this.cpdet = {} } init(c) { try { this.config = { ...this.config, ...c } } catch { console.log('Invalid initialization object'); } this.start(); } sharePdct() { Client.share({ title: mss.pdct?.pname ?? currPdct.dn, text: `@${CALC.parseMoney(currPdct.bp, true)}/-`, url: `${AC.env}${location.pathname.slice(1)}?venture=[0]` }); sessionStorage.exportImage = mss.pdct?.imgs[0] ?? ''; } create() { let x = `

`; $('body').addClass('mkt-mss-body').append(x); } #checkStockSize(dot) { let eop = new Date(), cpd = Date.now(), tdiff; eop.setFullYear(eop.getFullYear() + 1); eop.setMonth(0); eop.setDate(1); tdiff = eop.getTime() - cpd; let estss = Math.ceil(tdiff / (1000 * 60 * 60 * 24)) * dot; $('#estss').text(estss); return estss; } start() { self.currPdct = this.config.products[this.config.cp]; let view = `
⚡ Today's Offer

${currPdct.dn}

-
Bid price Ksh ${currPdct.bp}
Retail
Units left

Latest Bids

Live
Be the first to place your bid here
`; $('main').html(view); $('#ct').text(currPdct.dn); this.checkDrawTime('#timetodraw,.timetodraw'); this.#fetchProduct(); this.#checkStockSize(currPdct.gpd); this.syncBids(); } #fetchProduct() { $.ajax({ url: 'promos/res/.incs/pdts/', data: { fetch: this.config.cp }, dataType: 'json', success: pdct => { this.#parseProduct(pdct) }, error: err => { if (isNetError(err)) return; showToast(err.statusText); } }) } #parseProduct(pdct) { mss.pdct = pdct; $('#pdctName').text(pdct.pname); $('#pdctMprice').text(pdct.price).closest('.mkt-pdct-hero').on('click', function () { mss.viewPdct(pdct); }); $('#pdctImg')[0].src = (pdct.imgs[0]); mss.cpdet = pdct; } viewPdct(pdct) { let m = `

${pdct.pname}


Remaining: ${this.#checkStockSize(currPdct.gpd)}units
Today's offer: ${currPdct.gpd} units
`; Action.confirm({ title: "Product Details", html: m, positive: "Place Bid", negative: "Back" }).then(res => { if (res) return mss.bid('#bidder'); }) } async #createAccRef(amount) { try { return new Promise((resolve, reject) => { let r; try { r = Operator.getSP().venture; } catch (e) { e } if (Number(r) < 0) resolve(r); $.post('promos/.inc/api/createTxnRef', { uid: localStorage.user, amount: amount, strict: false }).then(res => resolve(res)).fail(err => reject(err.statusText)) }).catch(err => showToast(err.message ?? err)) } catch (error) { console.log(error.message); } } #createTimestamp() { let x = new Date(); x.setMinutes(x.getMinutes() - x.getTimezoneOffset()); return x.toJSON().replace(/[^0-9]/g, '').slice(0, -3); } async bid(t) { if (mss.lock) return ActionCentre('*Please confirm the previous transaction to proceed on another bid', 15000, 'Error'); let pdct = this.config.products[this.config.cp]; await this.getPhoneNumber().then(async pn => { let Timestamp = this.#createTimestamp(), Amount = pdct.bp, AccountReference; localStorage.setItem('user', pn); Swal.fire({ html: `
A payment request has been sent to your phone number, please complete the payment to place your bid.
` }).then(confirmed => { if (!confirmed) return; let accRef = mss.#createAccRef(Amount); if (!accRef) return showToast('Failed to create account reference, please try again later'); const pmReq = { AccountReference: accRef, Amount: Amount, TransactionDesc: pdct.dn, PhoneNumber: pn, Timestamp: Timestamp } Epay.reqDeposit(pmReq).then(async res => { if (res && res.status == 200) { mss.lock = true; mss.submitBid(AccountReference); let bidData = { ref: JSON.stringify({ FirstName: localStorage.getItem('user'), MSISDN: pn }), etime: Timestamp, id: AccountReference }; mss.parseonSyncBids([bidData], true); setTimeout(() => { mss.lock = false; showToast('Bid placed successfully ✅'); }, 5000); } else { showToast(res?.message ?? 'Payment request failed, please try again later'); } }).catch(err => { mss.lock = false; if (isNetError(err)) { showToast('Network error: Please check your internet connection'); return; } showToast(err.message ?? err); }); }) }) } getPhoneNumber() { return new Promise((resolve, reject) => { const stored = localStorage.getItem('user') ?? ''; Swal.fire({ title: 'Enter phone number', input: 'tel', inputValue: stored, inputLabel: 'Phone number', inputPlaceholder: 'e.g. 2547XXXXXXXX', showCancelButton: true, confirmButtonText: 'Continue', inputValidator: value => { if (!value) return 'Phone number is required'; // normalize let v = value.toString().replace(/[\s\-\(\)\+]/g, ''); if (/^0(7\d{8})$/.test(v)) v = v.replace(/^0/, '254'); else if (/^7\d{8}$/.test(v)) v = '254' + v; // final format should be 2547XXXXXXXX if (!/^2547\d{8}$/.test(v)) return 'Invalid format. Use 2547XXXXXXXX'; return undefined; } }).then(result => { if (!result.isConfirmed) return reject('Phone entry cancelled'); let v = result.value.toString().replace(/[\s\-\(\)\+]/g, ''); if (/^0(7\d{8})$/.test(v)) v = v.replace(/^0/, '254'); else if (/^7\d{8}$/.test(v)) v = '254' + v; if (!/^2547\d{8}$/.test(v)) return reject('Invalid phone number'); localStorage.setItem('user', v); resolve(v); }).catch(err => reject(err)); }); } checkDrawTime(plc) { function time() { let ctime = new Date(), ctimeOffsetBalancer = ctime.getTimezoneOffset() / 60 + 3; ctime.setHours(ctime.getHours() + ctimeOffsetBalancer) let dtime = new Date(ctime); dtime.setDate(dtime.getDate() + 1); dtime.setHours(0); dtime.setMinutes(0); dtime.setSeconds(0); let dtt = dtime.getTime(), ctt = ctime.getTime(), diffTime = dtt - ctt, hoursin = Math.floor(diffTime / (1000 * 60 * 60)), minutesin = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60)), secsin = Math.floor((diffTime % (1000 * 60)) / 1000); return `${hoursin}H: ${minutesin}M: ${secsin}S`; } if (document.querySelector(plc)) return setInterval(() => { $(plc).html(time()); }, 1000); return time(); } submitBid(bid) { try { $.ajax({ url: AC.env.concat('shops/products/bidding/api/submitBid'), type: 'POST', data: { bid: bid }, success: res => { try { if (res && !isNaN(res)) return showToast('Entered to draw successfully ✅'); throw JSON.parse(res); } catch { showToast(res); } }, error: err => { if (isNetError(err)) return setTimeout(() => mss.submitBid(bid), 5000) } }) } catch (error) { console.log(error.message ?? error) } } parseonSyncBids(bids, instate = false) { try { if (!bids.length) return; let bidView = ``; if (instate) bidView += ``; $('#hedexArea').html(bidView); } else { $('#hedex').append(bidView); } $('#khamara')[0].scrollIntoView(); } catch (error) { console.log(error.message); } } syncBids() { try { if (!mss.bidStream) mss.bidStream = setInterval(mss.syncBids, 10000); if (!document.hasFocus()) return console.log('Stream paused'); if (mss.passcode && mss.passcode != 1) return; $.ajax({ url: AC.env.concat('shops/products/bidding/api/syncBids'), type: 'POST', data: { x: mss.config.cp, pass: mss.pass ?? '' }, beforeSend: e => { mss.passcode = performance.now(); }, complete: e => { mss.passcode = 1; }, dataType: 'json', success: data => { return mss.parseonSyncBids(data, mss.pass ? false : true); }, error: err => { mss.passcode = 1; if (isNetError(err)) return self.onnetrestoreEvents[0] = () => mss.syncBids(); } }); } catch (error) { console.log(error); } } } self.mss = new Mission();