Help
RSS
API
Feed
Maltego
Contact
Domain > www.essentialstationeries.com
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2025-08-26
13.249.91.45
(
ClassC
)
2026-02-05
3.169.173.40
(
ClassC
)
Port 80
HTTP/1.1 301 Moved PermanentlyServer: CloudFrontDate: Thu, 05 Feb 2026 19:06:28 GMTContent-Type: text/htmlContent-Length: 167Connection: keep-aliveLocation: https://www.essentialstationeries.com/X-Cache: Redirect from cloudfrontVia: 1.1 a454a679efa1e16833b77cb6af61e11c.cloudfront.net (CloudFront)X-Amz-Cf-Pop: HIO52-P4X-Amz-Cf-Id: vZLh5fDoFjcPCkdcltbWlDjXNrSuDTNBaJy3lmuAuUKwKxQWif-Y8g html>head>title>301 Moved Permanently/title>/head>body>center>h1>301 Moved Permanently/h1>/center>hr>center>CloudFront/center>/body>/html>
Port 443
HTTP/1.1 200 OKContent-Type: text/htmlContent-Length: 43242Connection: keep-aliveLast-Modified: Sun, 11 Jan 2026 06:19:02 GMTx-amz-server-side-encryption: AES256x-amz-version-id: Wypa81jqBbCOur49My2.xdZiCICL5vK3Accept-Ranges: bytesServer: AmazonS3Date: Thu, 05 Feb 2026 19:06:30 GMTETag: ec347cbcb992b224cf7894b62de9d3c6X-Cache: RefreshHit from cloudfrontVia: 1.1 e3d057b3e6efdd15e49b433f7704a6c8.cloudfront.net (CloudFront)X-Amz-Cf-Pop: HIO52-P4X-Amz-Cf-Id: G22bYycMzpWfmCwQrh3sqCEc27XSiziU7WDnCowZFDfFndR7-9lNYw !DOCTYPE html> html langen>head> meta charsetutf-8 /> meta nameviewport contentwidthdevice-width, initial-scale1 /> title>School Stationery Packs | Essential Stationery Co. – South Africa/title> meta namedescription contentAffordable, hassle-free school stationery packs per grade. Order online with peace of mind; labelled packs delivered to your school before term. /> link relcanonical hrefhttps://www.essentialstationeries.com/ /> meta namerobots contentindex,follow /> meta propertyog:type contentwebsite /> meta propertyog:title contentSchool Stationery Packs | Essential Stationery Co. /> meta propertyog:description contentAffordable, hassle-free stationery packs per grade with labelled delivery. /> meta propertyog:url contenthttps://www.essentialstationeries.com/ /> meta propertyog:image contenthttps://www.essentialstationeries.com/assets/social-card.jpg /> link relicon href/assets/favicon.ico> meta nametheme-color content#0E3887> style> :root{ --blue-900:#0E3887;--blue-700:#2A4C9B;--blue-050:#F5F8FF;--ink:#0E3887;--yellow:#FFD34B; --paper:#FFFFFF;--muted:#5B6C9E;--border:#E7ECFF;--accent:#10BBD4;--journey-top:64px } html,body{margin:0;padding:0;background:var(--blue-050);color:var(--ink); font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;line-height:1.5} *{box-sizing:border-box} a{color:var(--blue-900);text-decoration:none} a:hover{text-decoration:underline} .muted{color:var(--muted)} .container{max-width:1100px;margin-inline:auto;padding:24px} .grid{display:grid;gap:24px} .grid-2{grid-template-columns:repeat(2,minmax(0,1fr))} @media (max-width:900px){.grid-2{grid-template-columns:1fr}} /* Topbar */ .topbar{position:sticky;top:0;z-index:40;background:#fff;border-bottom:1px solid var(--border)} .brand{display:flex;align-items:center;gap:12px;font-weight:800} .brand-mark{width:36px;height:36px;border-radius:9px;background:var(--blue-900); display:grid;place-items:center;color:#fff} .top-actions{display:flex;gap:clamp(6px,2vw,12px);align-items:center;flex-wrap:wrap} .btn{display:inline-flex;align-items:center;gap:10px; padding:clamp(8px,1.6vw,12px) clamp(12px,2.4vw,18px); border-radius:999px;border:2px solid transparent;font-weight:700;cursor:pointer;min-height:44px; font-size:clamp(14px,2.8vw,16px)} .btn-primary{background:var(--yellow);color:#2b2b2b} .btn-outline{background:transparent;border-color:var(--blue-900);color:var(--blue-900)} .btn-accent{background:var(--accent);color:#fff} .btn:disabled{opacity:.5;cursor:not-allowed} input,select{padding:12px;border:1px solid #E1E8FF;border-radius:10px;font-size:16px;background:#fff;height:48px} /* Journey tracker */ .journey{position:sticky;top:var(--journey-top);z-index:35;background:#fff;border-bottom:1px solid var(--border)} .progress{height:10px;border-radius:999px;background:#EEF2FF;overflow:hidden} .progress i{display:block;height:100%;width:10%;background:linear-gradient(90deg,#FFD34B,#74E0A5);transition:width .35s} .stepsRow{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:8px;font-size:13px;color:var(--muted);text-align:center} .stepsRow b{color:var(--blue-900)} /* Hero + sections */ .hero{background:linear-gradient(180deg,#fff,#F7FAFF);border-bottom:1px solid var(--border)} .hero h1{font-size:clamp(28px,4vw,42px);line-height:1.15;margin:0 0 10px;font-weight:900;color:var(--blue-900)} .hero p{font-size:clamp(15px,2.2vw,18px);color:var(--muted);max-width:60ch} .section{padding:40px 0} .section h2{font-size:clamp(22px,3vw,30px);margin:0 0 8px;color:var(--blue-900)} .section p.lead{color:var(--muted);max-width:68ch;margin:0 0 24px} .card{background:#fff;border:1px solid var(--border);border-radius:16px;padding:20px;box-shadow:0 6px 18px rgba(14,56,135,.06)} .info{background:#F5FAFF;border:1px solid #CFE3FF;color:#27457A;border-radius:12px;padding:10px 12px;font-size:14px} .finder-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));align-items:end} /* Responsive pack grid: phone1, tablet2, desktop3 */ #packsGrid{display:grid;gap:16px} @media (min-width:1025px){ #packsGrid{grid-template-columns:repeat(3,minmax(0,1fr))} } @media (min-width:641px) and (max-width:1024px){ #packsGrid{grid-template-columns:repeat(2,minmax(0,1fr))} } @media (max-width:640px){ #packsGrid{grid-template-columns:1fr} } /* CTA + footer */ .cta{background:#0E3887;color:#fff;border-radius:16px;padding:26px;display:flex;align-items:center;justify-content:space-between;gap:16px} footer{background:#0f2f74;color:#dfe7ff;margin-top:40px} .footer-grid{display:grid;gap:18px;grid-template-columns:1.2fr .8fr} @media (max-width:800px){.footer-grid{grid-template-columns:1fr}} /* Modal / sheet */ .sheet{position:fixed;inset:0;background:rgba(0,0,0,.4);display:none;align-items:center;justify-content:center;z-index:50} .sheet.open{display:flex} .sheet-card{background:#fff;width:min(980px,96vw);max-height:92vh;overflow:auto;border-radius:18px;border:1px solid var(--border);box-shadow:0 20px 50px rgba(14,56,135,.2);padding:18px} .sheet-head{display:flex;justify-content:space-between;gap:12px;align-items:center;border-bottom:1px solid var(--border);padding-bottom:10px;margin-bottom:12px} .pill{display:inline-flex;gap:8px;align-items:center;border:1px dashed var(--border);border-radius:999px;padding:6px 10px;color:#27457A} .close-x{border:none;background:transparent;font-size:22px;cursor:pointer} .items-tbl{width:100%;border-collapse:collapse} .items-tbl th,.items-tbl td{padding:10px 8px;border-bottom:1px solid #EEF2FF;text-align:left;vertical-align:middle} .items-tbl th:last-child,.items-tbl td:last-child{text-align:right} .qty-input{width:86px;height:40px;text-align:center;border-radius:10px;border:1px solid #E1E8FF} @media (max-width:640px){.qty-input{width:74px}} .totals{display:grid;gap:8px;margin-top:12px} .totals-row{display:flex;justify-content:space-between;align-items:center} .warn{color:#B45309;background:#FFF7ED;border:1px solid #FDE68A;padding:8px;border-radius:10px} .toast{position:fixed;left:50%;transform:translateX(-50%);bottom:20px;background:#212944;color:#fff;border-radius:999px;padding:10px 14px;box-shadow:0 10px 22px rgba(0,0,0,.2);opacity:0;pointer-events:none;transition:opacity .25s;z-index:60} .toast.show{opacity:1} /* Offer radio cards */ .offerGroup{display:grid;gap:10px;margin-top:8px} .offerCard{border:2px solid #dbe3ff;border-radius:14px;padding:12px;display:grid;gap:6px;background:#fff} .offerCardaria-checkedtrue{border-color:#0E3887;box-shadow:0 0 0 2px rgba(14,56,135,.12)} .offerHead{display:flex;gap:10px;align-items:center} .offerHead input{width:18px;height:18px} .calcRow{font-size:14px;color:#27457A} /* --- additions (offers-only) --- */ .offerCard.is-disabled{opacity:.55;filter:grayscale(1);pointer-events:none} .expires{font-size:12px;color:var(--muted)} /style>/head>body> !-- Top bar --> div classtopbar> div classcontainer styledisplay:flex;justify-content:space-between;align-items:center;gap:18px;> div classbrand> span classbrand-mark aria-hiddentrue> svg viewBox0 0 24 24 fillnone strokecurrentColor stroke-width2 stroke-linecapround stroke-linejoinround>path dM4 19.5A2.5 2.5 0 0 1 6.5 17H20/>path dM4 4v15.5A2.5 2.5 0 0 1 6.5 22H20V6a2 2 0 0 0-2-2H6/>/svg> /span> span>Essential Stationery Co./span> /div> div classtop-actions> a classbtn btn-outline hrefcompare.html aria-labelCompare our prices>Compare prices/a> a classbtn btn-outline hrefpartner.html>Partner with us/a> a classbtn btn-primary href#order-finder>Place order now/a> /div> /div> /div> !-- Journey tracker --> div classjourney aria-hiddenfalse> div classcontainer> div classprogress roleprogressbar aria-valuemin0 aria-valuemax100 aria-valuenow10> i idprogressFill stylewidth:10%>/i> /div> div classstepsRow> span>b>1/b> Find school/span>span>b>2/b> View packs/span>span>b>3/b> Confirm/Modify/span>span>b>4/b> Your details/span>span>b>5/b> Place order/span> /div> /div> /div> !-- HERO --> header classhero idhow> div classcontainer> h1>Affordable, Hassle-Free School Stationery/h1> p>Standardised packs per grade. Easy online ordering. Labelled packs delivered to your school strong>before term starts/strong>. We donate strong>10% of our profits/strong> back to your school./p> div classinfo stylemargin-top:10px>Save your time, avoid queues, and get exactly what teachers asked for—checked, packed and labelled./div> div styledisplay:flex;gap:12px;flex-wrap:wrap;margin-top:16px> a classbtn btn-primary href#order-finder>Place order now/a> a classbtn btn-outline hrefpartner.html>Partner with us/a> /div> /div> /header> !-- ORDER SECTION --> section classsection idorder-finder-section> div classcontainer> div classgrid grid-2> div classcard idorder-finder> h3 stylemargin-top:0>Place order now/h3> p classlead>Choose your province, school and grade./p> div classfinder-grid> select idprovSel>option value>Select Province…/option> option>Gauteng/option>option>Western Cape/option>option>KwaZulu-Natal/option> option>Eastern Cape/option>option>Limpopo/option>option>Mpumalanga/option> option>North West/option>option>Northern Cape/option>option>Free State/option> /select> select idschoolSel disabled>option value>Select School…/option>/select> select idgradeSel disabled>option value>Select Grade…/option>/select> button idviewBtn classbtn btn-primary disabled>View list now/button> /div> div styledisplay:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:12px> span classmuted>List not here yet? Simply/span> a classbtn btn-accent hrefparent_upload.html>UPLOAD HERE/a> span classmuted>and we’ll notify you once your list is added./span> /div> div idorderFinderStatus classmuted aria-livepolite stylemargin-top:8px>/div> /div> !-- Quick Search --> div classcard> h3 stylemargin-top:0>Prefer to search?/h3> p classlead>Look up your school by name and view its packs./p> form idschoolSearchForm classgrid stylegrid-template-columns:1fr auto;gap:12px;margin-top:10px> input idschoolSearchInput required placeholderSearch your school name… /> button classbtn btn-primary typesubmit>Search/button> /form> div idschoolResults stylemargin-top:12px>/div> div stylemargin-top:10px;font-size:14px;color:var(--muted)>Cut-off for 2025 orders: strong>30 November 2025/strong>/div> /div> /div> /div> /section> !-- Packs area --> section classsection idorder> div classcontainer> h2>Example packs/h2> p classlead>These are demo placeholders. Your school’s packs match teacher lists./p> div idpacksGrid>/div> /div> /section> div classcontainer> div classcard cta> div> h3 stylemargin:0>We donate 10% of profits back to your school/h3> p stylemargin:6px 0 0;color:#E6ECFF>Paid after delivery with a simple reconciliation report./p> /div> div styledisplay:flex;gap:10px;flex-wrap:wrap> a classbtn btn-primary href#order-finder>Place order now/a> a classbtn btn-outline hrefpartner.html>Partner with us/a> /div> /div> /div> footer> div classcontainer footer-grid> div> h3 stylemargin:0 0 6px>Essential Stationery Co./h3> div>Preparing Learners, Supporting Schools./div> div stylemargin-top:12px>Cut-off for 2025: strong>30 November 2025/strong>/div> /div> div> div stylefont-weight:700;margin-bottom:6px>Contact/div> div>📞 067 604 4989/div> div>✉️ a hrefmailto:sales@essentialstationeries.com>sales@essentialstationeries.com/a>/div> /div> /div> /footer> !-- Pack Sheet (modal) --> div idpackSheet classsheet aria-hiddentrue> div classsheet-card roledialog aria-modaltrue aria-labelledbysheetTitle> div classsheet-head> div> div classpill idpackMeta>Loading…/div> h3 idsheetTitle stylemargin:.25rem 0 0>/h3> /div> button classclose-x idsheetClose aria-labelClose>✕/button> /div> !-- Note at top of view --> div classinfo idpackNote stylemargin:6px 0 14px> You can modify quantities or remove items as long as your total stays at/above b>R1 000/b>. If you customise the pack, you b>forfeit free personalisation on the pencil case/b> and b>any two optional uniform items/b>. /div> div iditemsWrap> table classitems-tbl iditemsTbl> thead> tr> th stylewidth:70%>Item/th> th stylewidth:130px>Qty/th> th>/th> /tr> /thead> tbody>/tbody> /table> div classtotals> div classtotals-row stylefont-size:20px>span>Total/span> strong idgrand>R 0.00/strong>/div> div idminWarn classwarn styledisplay:none>Your total is below the minimum order of strong>R 1 000.00/strong>. Please add items or increase quantities to continue./div> /div> div styledisplay:flex;gap:10px;flex-wrap:wrap;margin-top:12px> button classbtn btn-outline idresetBtn>Reset to default/button> button classbtn btn-primary idcontinueBtn disabled>Continue/button> /div> !-- Details form --> div iddetailsForm classcard stylemargin-top:12px;display:none> div classinfo stylemargin:-6px 0 12px> You may adjust items down to b>R1 000/b>. If you do, you’ll b>forfeit free personalisation/b> and b>two optional uniform items/b>. /div> h3 stylemargin-top:0>Your details/h3> div classgrid stylegrid-template-columns:1fr;gap:12px> input idstudent_name placeholderStudent full name /> input idparent_name placeholderParent/Guardian name /> input idparent_email placeholderEmail /> input idphone placeholderPhone/WhatsApp /> input idlabels placeholderName for labels (optional) /> /div> !-- Offer selection --> div stylemargin-top:14px;padding:12px;border:1px dashed var(--border);border-radius:12px;background:#FAFCFF> div stylefont-weight:700;margin-bottom:6px>Choose your payment option/div> div classofferGroup idofferGroup> div classofferCard data-codeSCHOOL50 aria-checkedtrue tabindex0> div classofferHead> input typeradio nameoffer idoffer50 valueSCHOOL50 checked /> label foroffer50 stylefont-weight:700;cursor:pointer>50% now to the school, settle the rest monthly (until Jan 2026)/label> /div> div classcalcRow idoffer50Calc>Deposit: R 0.00 • Remaining: R 0.00 • Monthly estimate: R 0.00/div> div stylemargin-top:8px> label>Months to settle remaining 50% select idsettleMonths stylewidth:100%>/select> /label> /div> div classmuted idsettleInfo stylemargin-top:4px>/div> /div> !-- additions (offers-only) --> div classofferCard data-codeEARLY50 aria-checkedfalse tabindex0> div classofferHead> input typeradio nameoffer idofferEarly50 valueEARLY50 /> label forofferEarly50 stylefont-weight:700;cursor:pointer>50% OFF — Early action/label> /div> div classcalcRow idofferEarly50Calc>Pay now: R 0.00 (we cover the rest)/div> div classexpires>Active until strong>10 Oct 2025/strong>/div> /div> div classofferCard data-codeFULL25 aria-checkedfalse tabindex0> div classofferHead> input typeradio nameoffer idofferFull25 valueFULL25 /> label forofferFull25 stylefont-weight:700;cursor:pointer>25% OFF — Pay in full/label> /div> div classcalcRow idofferFull25Calc>Pay now: R 0.00/div> div classexpires>Active until strong>31 Oct 2025/strong>/div> /div> div classofferCard data-codeFULL10 aria-checkedfalse tabindex0> div classofferHead> input typeradio nameoffer idofferFull10 valueFULL10 /> label forofferFull10 stylefont-weight:700;cursor:pointer>10% OFF — Last call/label> /div> div classcalcRow idofferFull10Calc>Pay now: R 0.00/div> div classexpires>Active until strong>30 Jan 2026/strong>/div> /div> !-- Declaration (applies to all options) --> div classofferCard data-codeAGREE aria-checkedfalse tabindex-1> label styledisplay:flex;gap:10px;align-items:flex-start;margin:0> input typecheckbox idagree50 /> span> I understand that my order will only be processed once the school confirms receipt of strong>50% of the pack value/strong>, and I will send strong>proof of payment within 7 days/strong> to a hrefmailto:sales@essentialstationeries.com>sales@essentialstationeries.com/a> or WhatsApp strong>067 604 4989/strong>. /span> /label> /div> div classofferCard is-disabled data-codeFULL_NOW aria-checkedfalse aria-disabledtrue tabindex-1> div classofferHead> input typeradio nameoffer idofferFull valueFULL_NOW disabled /> label forofferFull stylefont-weight:700;cursor:not-allowed>Pay in full online (coming soon)/label> /div> div classcalcRow idofferFullCalc>Amount now: R 0.00/div> div classmuted>We’ll record your choice; payment links will be shared once online payments are live./div> /div> /div> /div> div stylemargin-top:12px>Pack total: strong idformTotal>R 0.00/strong>/div> div stylemargin-top:12px> button classbtn btn-primary idpaybtn disabled>Place order/button> /div> /div> /div> /div> /div> div idtoast classtoast>Your total is below R 1 000./div> script> const API https://5hp88zox23.execute-api.af-south-1.amazonaws.com/prod; const MIN_TOTAL 100000; // R1000 in cents const CUTOFF_YEAR 2026, CUTOFF_MONTH_INDEX 0; // January 2026 let currentSchool { id: null, name: }; let currentPack null; function fixJourneyTop(){ const tb document.querySelector(.topbar); const h tb ? tb.offsetHeight : 64; document.documentElement.style.setProperty(--journey-top, h + px); } window.addEventListener(load, fixJourneyTop); window.addEventListener(resize, fixJourneyTop); const progressFill document.getElementById(progressFill); const PROG { start:10, packs:35, modify:65, details:85, placed:100 }; function setProgress(p){ progressFill.style.width p + %; document.querySelector(.progress)?.setAttribute(aria-valuenow, String(p)); sessionStorage.setItem(esc_prog, String(p)); } setProgress(Number(sessionStorage.getItem(esc_prog)||PROG.start)); (function(){ const params new URLSearchParams(location.search); const q params.get(q); if (q) { const input document.getElementById(schoolSearchInput); const form document.getElementById(schoolSearchForm); input.value q; setTimeout(()>form.dispatchEvent(new Event(submit,{cancelable:true})),0); } })(); async function api(path, opts{}){ const res await fetch(API + path, {headers:{Content-Type:application/json}, ...opts}); if (!res.ok) throw new Error(await res.text().catch(()>res.statusText)); return res.json(); } const fmtR cents > R + (Math.round(cents)/100).toFixed(2); const byName (a,b) > (a.name||).localeCompare(b.name||); const numbersOnly s > Number((s||).replace(/^\d/g,)) || 0; const toast (mYour total is below R 1 000.) > { const tdocument.getElementById(toast); t.textContentm; t.classList.add(show); clearTimeout(toast._t); toast._tsetTimeout(()>t.classList.remove(show),2200); }; // Search document.getElementById(schoolSearchForm)?.addEventListener(submit, async (e) > { e.preventDefault(); const q (document.getElementById(schoolSearchInput).value||).trim(); if (!q) return; const box document.getElementById(schoolResults); box.innerHTML div classcard>Searching…/div>; try { const {results} await api(`/schools?query${encodeURIComponent(q)}`); if (!results.length) { box.innerHTML div classcard>No schools found yet./div>; return; } box.innerHTML results.map(s > ` div classcard stylemargin-top:10px> div stylefont-weight:800>${s.name}/div> div classmuted>${(s.city||)}${s.province? , + s.province : }/div> div stylemargin-top:8px>button classbtn btn-outline data-school${s.school_id} data-name${s.name}>See packs/button>/div> /div>`).join(); box.querySelectorAll(buttondata-school).forEach(btn > btn.addEventListener(click, () > { showPacks(btn.dataset.school, btn.dataset.name); setProgress(PROG.packs); document.getElementById(order).scrollIntoView({behavior:smooth}); }) ); } catch(e){ box.innerHTML `div classcard>Error: ${e.message}/div>`; } }); // Finder const provSel document.getElementById(provSel); const schoolSel document.getElementById(schoolSel); const gradeSel document.getElementById(gradeSel); const viewBtn document.getElementById(viewBtn); const statusBox document.getElementById(orderFinderStatus); let schoolsCache ; let packsCache ; const syncBtn () > viewBtn.disabled !(provSel.value && schoolSel.value && gradeSel.value); provSel?.addEventListener(change, async () > { schoolSel.innerHTML `option value>Select School…/option>`; gradeSel.innerHTML `option value>Select Grade…/option>`; schoolSel.disabled gradeSel.disabled true; syncBtn(); if (!provSel.value) return; statusBox.textContent Loading schools…; try { let data await api(`/schools/find_school?province${encodeURIComponent(provSel.value)}`); if (!data || !Array.isArray(data.results)) { data await api(`/schools?province${encodeURIComponent(provSel.value)}`); } schoolsCache data.results||; schoolsCache.forEach(s > { const odocument.createElement(option); o.values.school_id; o.textContents.name; schoolSel.appendChild(o); }); schoolSel.disabled !schoolsCache.length; statusBox.textContent schoolsCache.length ? : No schools in this province yet.; } catch(e){ statusBox.textContent Error loading schools: + e.message; } syncBtn(); }); schoolSel?.addEventListener(change, async () > { gradeSel.innerHTML `option value>Select Grade…/option>`; gradeSel.disabled true; syncBtn(); const schoolId schoolSel.value; if (!schoolId) return; statusBox.textContent Loading grades…; try { const {packs} await api(`/schools/${encodeURIComponent(schoolId)}/packs`); packsCache packs; const grades ...new Set(packs.map(p > String(p.grade))).sort(); grades.forEach(g > { const odocument.createElement(option); o.valueg; o.textContentg; gradeSel.appendChild(o); }); gradeSel.disabled !grades.length; statusBox.textContent grades.length ? : No grades published for this school.; } catch(e){ statusBox.textContent Error loading grades: + e.message; } syncBtn(); }); gradeSel?.addEventListener(change, syncBtn); viewBtn?.addEventListener(click, async () > { const schoolId schoolSel.value; const schoolName (schoolsCache.find(s > s.school_idschoolId)||{}).name || Selected school; const grade gradeSel.value; await showPacks(schoolId, schoolName, grade); setProgress(PROG.packs); document.getElementById(order).scrollIntoView({behavior:smooth}); }); // Packs grid (minimal cards) async function showPacks(school_id, school_name, onlyGradenull) { currentSchool { id: school_id, name: school_name || }; const grid document.getElementById(packsGrid); grid.innerHTML div classcard>Loading packs…/div>; try{ const {packs} await api(`/schools/${encodeURIComponent(school_id)}/packs`); const filtered onlyGrade ? packs.filter(p > String(p.grade)String(onlyGrade)) : packs; if (!filtered.length){ grid.innerHTMLdiv classcard>No packs for this selection./div>; return; } grid.innerHTML filtered.map(p > ` article classcard> h3>${(p.name || p.pack_label || Pack)}/h3> p classmuted stylemargin:.35rem 0>Grade ${p.grade}/p> div classmuted stylemargin:.25rem 0>Click to see full item list/div> button classbtn btn-primary data-order data-pack${p.pack_id} data-packname${(p.name||p.pack_label||Pack).replace(//g,")}>View pack/button> /article>`).join(); grid.querySelectorAll(buttondata-order).forEach(btn > btn.addEventListener(click, ()>{ openPackSheet(btn.dataset.pack, btn.dataset.packname); setProgress(PROG.modify); }) ); }catch(e){ grid.innerHTML `div classcard>Error: ${e.message}/div>`; } } // Sheet logic const sheetdocument.getElementById(packSheet), sheetClosedocument.getElementById(sheetClose); const itemsTbldocument.querySelector(#itemsTbl tbody); const packMetadocument.getElementById(packMeta); const grandEldocument.getElementById(grand); const minWarndocument.getElementById(minWarn); const resetBtndocument.getElementById(resetBtn); const continueBtndocument.getElementById(continueBtn); const detailsFormdocument.getElementById(detailsForm); const formTotaldocument.getElementById(formTotal); const agree50document.getElementById(agree50); const settleMonthsSeldocument.getElementById(settleMonths); const settleInfodocument.getElementById(settleInfo); const paybtndocument.getElementById(paybtn); const offerGroupdocument.getElementById(offerGroup); let defaultQtys; let wasAboveMinfalse; function openSheet(){ sheet.classList.add(open); sheet.setAttribute(aria-hidden,false); } function closeSheet(){ sheet.classList.remove(open); sheet.setAttribute(aria-hidden,true); detailsForm.style.displaynone; } sheetClose.addEventListener(click, closeSheet); sheet.addEventListener(click, e>{ if(e.targetsheet) closeSheet(); }); async function openPackSheet(pack_id, packNameFallback){ itemsTbl.innerHTML `tr>td colspan3>Loading pack…/td>/tr>`; packMeta.textContentLoading…; document.getElementById(sheetTitle).textContent; detailsForm.style.displaynone; continueBtn.disabledtrue; openSheet(); try{ const detail await api(`/packs/${encodeURIComponent(pack_id)}`); const items (detail.items||).map(i>({ sku:i.sku||, name:(i.name||i.sku||Item), qty:Number(i.qty||0), unit_price_cents:Number(i.unit_price_cents||0) })).sort(byName); currentPack { pack_id:detail.pack_id, name:(detail.name||packNameFallback||Pack), grade:detail.grade||, overhead_pct:Number(detail.overhead_pct||0), margin_pct:Number(detail.margin_pct||0), items }; defaultQtys items.map(i>i.qty); document.getElementById(sheetTitle).textContentcurrentPack.name; packMeta.textContent `Grade ${currentPack.grade}`; renderRows(); recalcTotals(); }catch(e){ itemsTbl.innerHTML `tr>td colspan3>Failed to load this pack./td>/tr>`; } } function renderRows(){ itemsTbl.innerHTML currentPack.items.map((it, idx)>` tr data-idx${idx}> td>div stylefont-weight:700>${it.name}/div>/td> td>input classqty-input typenumber min0 step1 value${it.qty} data-idx${idx} />/td> td>button classbtn btn-outline data-remove${idx} titleRemove>Remove/button>/td> /tr>`).join(); itemsTbl.querySelectorAll(input.qty-input).forEach(inp>{ inp.addEventListener(input, (e)>{ const i Number(e.target.dataset.idx); const val Math.max(0, Math.floor(Number(e.target.value||0))); currentPack.itemsi.qty val; recalcTotals(true); }); }); itemsTbl.querySelectorAll(buttondata-remove).forEach(btn>{ btn.addEventListener(click, ()>{ const i Number(btn.dataset.remove); currentPack.itemsi.qty 0; itemsTbl.querySelector(`inputdata-idx${i}`).value 0; recalcTotals(true); }); }); } function recalcTotals(fromChangefalse){ const sub currentPack.items.reduce((s,i)> s + i.qty * i.unit_price_cents, 0); const oh Math.round(sub * (currentPack.overhead_pct/100)); const mg Math.round((sub + oh) * (currentPack.margin_pct/100)); const tot sub + oh + mg; grandEl.textContent fmtR(tot); formTotal.textContentfmtR(tot); const meetsMin tot > MIN_TOTAL; minWarn.style.display meetsMin ? none : block; continueBtn.disabled !meetsMin; if (fromChange) { if (!meetsMin && wasAboveMin) toast(Your total is below R 1 000.); if (meetsMin && !wasAboveMin) toast(Nice! You’ve reached the minimum — you can continue.); } wasAboveMin meetsMin; if (!continueBtn.disabled && detailsForm.style.display ! none) { populateSettlementMonths(); updateOfferCalcs(); validateReadyToPlace(); } } resetBtn.addEventListener(click, ()>{ currentPack.items.forEach((i,idx)> i.qty defaultQtysidx); renderRows(); recalcTotals(); }); continueBtn.addEventListener(click, ()>{ detailsForm.style.displayblock; setProgress(PROG.details); populateSettlementMonths(); updateOfferCalcs(); validateReadyToPlace(); detailsForm.scrollIntoView({behavior:smooth,block:start}); }); function monthsUntilCutoff(){ const nownew Date(); const cutoffnew Date(Date.UTC(CUTOFF_YEAR,CUTOFF_MONTH_INDEX,31)); let m(cutoff.getUTCFullYear()-now.getUTCFullYear())*12 + (cutoff.getUTCMonth()-now.getUTCMonth()); return Math.max(1,m); } function populateSettlementMonths(){ const maxmonthsUntilCutoff(); const prevNumber(settleMonthsSel.value||0); settleMonthsSel.innerHTML Array.from({length:max},(_,i)>`option value${i+1}>${i+1}/option>`).join(); settleMonthsSel.value String(prev && prevmax ? prev : Math.min(3,max)); } function selectedOfferCode(){ return (document.querySelector(inputnameoffer:checked)?.value)||SCHOOL50; } /* --- additions (offers-only) --- */ const OFFER_DEADLINES { EARLY50: new Date(2025, 9, 10, 23, 59, 59), // 10 Oct 2025 FULL25: new Date(2025, 9, 31, 23, 59, 59), // 31 Oct 2025 FULL10: new Date(2026,1, 30, 23, 59, 59) // 30 Jan 2026 }; function offerActive(code){ const d OFFER_DEADLINEScode; return !d || new Date() d; } function enforceOfferAvailability(){ document.querySelectorAll(#offerGroup .offerCard).forEach(card>{ const code card.getAttribute(data-code); const input card.querySelector(inputtyperadio); if (code FULL_NOW){ if (input){ input.disabled true; } card.classList.add(is-disabled); return; } if (EARLY50,FULL25,FULL10.includes(code)){ const active offerActive(code); if (input){ input.disabled !active; } card.classList.toggle(is-disabled, !active); if (!active && input && input.checked){ document.getElementById(offer50).checked true; } } }); } function updateOfferCalcs(){ const totalCents numbersOnly(grandEl.textContent); // Offer A: 50% now to school const months Number(settleMonthsSel.value||1); const deposit50 Math.round(totalCents * 0.5); const remain50 totalCents - deposit50; const perMonth Math.ceil(Math.max(0, remain50) / Math.max(1, months)); document.getElementById(offer50Calc).textContent `Deposit: ${fmtR(deposit50)} • Remaining: ${fmtR(remain50)} • Monthly estimate: ${fmtR(perMonth)}`; document.getElementById(settleInfo).textContent `Estimate: 50% now via school, remaining ${months} × ${fmtR(perMonth)} (to January 2026).`; // Offer B: limited-time offers const early50Now Math.round(totalCents * 0.50); const full25Now Math.round(totalCents * 0.75); const full10Now Math.round(totalCents * 0.90); const elEarly document.getElementById(offerEarly50Calc); const el25 document.getElementById(offerFull25Calc); const el10 document.getElementById(offerFull10Calc); if (elEarly) elEarly.textContent `Pay now: ${fmtR(early50Now)} (we cover the rest)`; if (el25) el25.textContent `Pay now: ${fmtR(full25Now)}`; if (el10) el10.textContent `Pay now: ${fmtR(full10Now)}`; // Coming soon (greyed) const fullNow document.getElementById(offerFullCalc); if (fullNow) fullNow.textContent `Amount now: ${fmtR(totalCents)}`; // Visual selected state offerGroup.querySelectorAll(.offerCard).forEach(card>{ const code card.getAttribute(data-code); card.setAttribute(aria-checked, String(code selectedOfferCode())); }); // Enable/disable months only for SCHOOL50; declaration always enabled/required const is50 selectedOfferCode()SCHOOL50; settleMonthsSel.disabled !is50; // Disable expired offers enforceOfferAvailability(); } // Validate on radio change *and* any click in the offers area offerGroup.addEventListener(change, ()>{ updateOfferCalcs(); validateReadyToPlace(); }); offerGroup.addEventListener(click, (e)>{ const card e.target.closest(.offerCard); const radio card?.querySelector(inputtyperadio); if (radio) { radio.checked true; } // make the whole card effectively selectable updateOfferCalcs(); validateReadyToPlace(); }); settleMonthsSel.addEventListener(change, ()>{ updateOfferCalcs(); validateReadyToPlace(); }); agree50.addEventListener(change, validateReadyToPlace); function validateReadyToPlace(){ const ok id > (document.getElementById(id).value||).trim().length>1; const okEmail ()> /\S+@\S+\.\S+/.test((document.getElementById(parent_email).value||).trim()); const okPhone ()> (document.getElementById(phone).value||).trim().length>7; const minOk (numbersOnly(grandEl.textContent) > MIN_TOTAL); const agreed agree50.checked; const schoolOk !!currentSchool.id; const ready (ok(student_name) && ok(parent_name) && okEmail() && okPhone() && minOk && agreed && schoolOk); paybtn.disabled !ready; } student_name,parent_name,parent_email,phone,labels.forEach(id>{ document.getElementById(id).addEventListener(input, validateReadyToPlace); }); // Place order paybtn.addEventListener(click, async ()>{ if (!currentSchool.id){ alert(Please select your school again.); return; } const totalCents numbersOnly(grandEl.textContent); const offerCode selectedOfferCode(); const months Number(settleMonthsSel.value||1); let offer; if (offerCode SCHOOL50){ offer { code: SCHOOL50, label: 50% now to school, remainder monthly to Jan 2026, deposit_cents: Math.round(totalCents * 0.5), remaining_cents: totalCents - Math.round(totalCents * 0.5), months_to_settle: months, monthly_estimate_cents: Math.ceil((totalCents - Math.round(totalCents * 0.5)) / Math.max(1, months)) }; } else if (offerCode EARLY50){ offer { code:EARLY50, label:50% OFF — pay 50% to the school by 10 Oct 2025 (we cover the rest), discount_pct:50, pay_now_cents: Math.round(totalCents*0.50), remaining_cents:0, months_to_settle:0, monthly_estimate_cents:0, deadline:2025-10-10 }; } else if (offerCode FULL25){ offer { code:FULL25, label:25% OFF — pay 75% in full by 31 Oct 2025, discount_pct:25, pay_now_cents: Math.round(totalCents*0.75), remaining_cents:0, months_to_settle:0, monthly_estimate_cents:0, deadline:2025-10-31 }; } else if (offerCode FULL10){ offer { code:FULL10, label:10% OFF — pay 90% in full by 30 Nov 2025, discount_pct:10, pay_now_cents: Math.round(totalCents*0.90), remaining_cents:0, months_to_settle:0, monthly_estimate_cents:0, deadline:2025-11-30 }; } else { offer { code: FULL_NOW, label: Pay in full online (coming soon), deposit_cents: totalCents, remaining_cents: 0, months_to_settle: 0, monthly_estimate_cents: 0 }; } const payload { school_id: currentSchool.id, school_name: currentSchool.name, pack_id: currentPack.pack_id, pack_name: currentPack.name, grade: currentPack.grade, items: currentPack.items .filter(i > i.qty > 0) .map(i > ({ sku: i.sku, qty: i.qty, // send exactly what the parent saw unit_price_cents: Number(i.unit_price_cents || 0) })), // send the same percentages the UI used, so server math matches pack_overhead_pct: Number(currentPack.overhead_pct || 0), pack_margin_pct: Number(currentPack.margin_pct || 0), // >>> END ADDITIONS displayed_total_cents: totalCents, student_name: document.getElementById(student_name).value.trim(), parent_name: document.getElementById(parent_name).value.trim(), parent_email: document.getElementById(parent_email).value.trim(), phone: document.getElementById(phone).value.trim(), personalisation: { labels: document.getElementById(labels).value.trim() }, payment_offer: offer }; try{ const r await api(/orders, {method:POST, body:JSON.stringify(payload)}); setProgress(PROG.placed); alert(`Order placed! ${r.order_id}\nPlease pay 50% to the school and send POP to 067 604 4989 or sales@essentialstationeries.com.`); closeSheet(); }catch(e){ alert(Order failed: + e.message); } }); // Placeholder cards (for landing) (function renderPlaceholders(){ const grid document.getElementById(packsGrid); grid.innerHTML ` article classcard>h3>Grade R Pack/h3>p classmuted>Curated starter items, teacher-approved./p>/article> article classcard>h3>Grade 1 Pack/h3>p classmuted>Everything needed, labelled and ready./p>/article> article classcard>h3>Grade 2 Pack/h3>p classmuted>Teacher list matched, no guesswork./p>/article> `; })(); /* Auto-open pack & order form when arriving with deep-link params */ window.addEventListener(load, async () > { const p new URLSearchParams(location.search); const openPack p.get(open_pack); const schoolId p.get(school_id); const schoolNm p.get(school_name) || ; const grade p.get(grade); if (openPack && schoolId && typeof showPacks function && typeof openPackSheet function) { try { await showPacks(schoolId, schoolNm, grade); setTimeout(() > { openPackSheet(openPack, Pack); document.getElementById(order)?.scrollIntoView({behavior:smooth}); }, 250); } catch(e) { console.warn(Deep-link open failed:, e); } } }); /script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]