1/** 2 * src/public/js/course-page.js â the shared helper module for the courses UI. 3 * 4 * ââ â ï¸ THE TABBED COURSE PAGE THAT USED TO LIVE HERE IS GONE âââââââââ 5 * RETIRED 2026-08-03 (L21 stage 2). Alex, of the six-tab editor: "the old 6 * six-tab course editor, I don't like it at all. Unless it has something in it 7 * that we haven't covered with the Manage editor or through the new onboarding 8 * flow, we shouldn't need it⦠Make sure that everything's covered and then you 9 * can retire all the old stuff because there's no need to have it on the 10 * platform." 11 * 12 * What replaced it: the guide covers Outline (the plan editor), Customize (the 13 * Look step) and Offers (Go live's product step); the Manage board's list 14 * covers Lesson checks, Members, Community & cohorts, 1:1 sessions, 15 * Certificates and Course settings. Both live in course-creation-wizard.js. 16 * 17 * ââ â ï¸ SO WHY DOES THIS FILE STILL EXIST? ââââââââââââââââââââââââââââ 18 * Because it was two things, and only one of them was a tab strip. The other 19 * is this: the shared helpers â `api`, `esc`, `toast`, `fmtDate`, `timeAgo`, 20 * `money`, `emptyBlock`, `soonBlock`, `tile`, `bar`, `setMsg` â used by TWELVE 21 * files across roughly 154 call sites, including every module the Manage board 22 * mounts. They exist precisely so the courses modules stopped carrying their 23 * own fetch wrapper each; deleting them with the shell would have broken the 24 * destinations the retirement was built on. 25 * 26 * The name is kept because twelve files say `App.CoursePage.api` and renaming 27 * it would be a large diff whose only product is a different word. 28 *
29 * â ï¸ DO NOT ADD SCREENS HERE. This is a helper module now. Anything that draws 30 * a course surface belongs in the wizard's manage board or in its own module. 31 */ 32(function () { 33 'use strict'; 34 35 var App = (window.App = window.App || {}); 36 37 // ââ Shared helpers (one copy, used by every tab module) âââââââââââââ 38 39 function token() { try { return localStorage.getItem('cb_token'); } catch (_) { return null; } } 40 41 async function api(path, opts) { 42 opts = opts || {}; 43 var t = token(); 44 var headers = t ? { 'Authorization': 'Bearer ' + t } : {}; 45 var init = { credentials: 'same-origin', headers: headers, method: opts.method || 'GET' }; 46 if (opts.body !== undefined) { 47 headers['Content-Type'] = 'application/json'; 48 init.body = JSON.stringify(opts.body); 49 } 50 var r = await fetch('/api' + path, init); 51 var j = null; 52 try { j = await r.json(); } catch (_) { j = {}; } 53 if (!r.ok) { 54 var e = new Error('HTTP ' + r.status + ' ' + path + (j && j.error ? ' ' + j.error : '')); 55 e.status = r.status; e.code = j && j.error; e.payload = j; 56 throw e; 57 } 58 return j; 59 } 60 61 function esc(s) { 62 return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { 63 return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; 64 }); 65 } 66 67 var _toastEl = null, _toastT = null; 68 function toast(text) { 69 try { 70 if (!_toastEl) { 71 _toastEl = document.createElement('div'); 72 _toastEl.setAttribute('data-cb-toast', ''); 73 _toastEl.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);' + 74 'background:var(--surface);color:var(--text);padding:10px 18px;border-radius:8px;' + 75 'font-size:13.5px;font-weight:500;border:1px solid var(--border);' + 76 'box-shadow:0 8px 24px var(--border-strong);z-index:100010;opacity:0;' + 77 'transition:opacity .15s;pointer-events:none;max-width:88vw;text-align:center'; 78 document.body.appendChild(_toastEl); 79 } 80 _toastEl.textContent = String(text); 81 _toastEl.style.opacity = '1'; 82 clearTimeout(_toastT); 83 _toastT = setTimeout(function () { _toastEl.style.opacity = '0'; }, 3200); 84 } catch (_) {} 85 } 86 87 function fmtDate(d) { 88 if (!d) return ''; 89 try { return App.Dates.short(d); } 90 catch (_) { return ''; } 91 } 92 93 /** "3 days ago" / "today". Empty string for a missing date â the caller 94 * decides what "never" reads like in its own context. */ 95 function timeAgo(d) { 96 if (!d) return ''; 97 try { 98 var days = Math.floor((Date.now() - new Date(d).getTime()) / 86400000); 99 if (days <= 0) return 'today'; 100 if (days === 1) return 'yesterday'; 101 if (days < 31) return days + ' days ago'; 102 var months = Math.round(days / 30); 103 if (months < 24) return months + ' month' + (months === 1 ? '' : 's') + ' ago'; 104 return Math.round(months / 12) + ' years ago'; 105 } catch (_) { return ''; } 106 } 107 108 var CURRENCY = { gbp: '£', usd: '$', eur: 'â¬' }; 109 /** Minor units â a display price. Currency symbol where we know one, the 110 * ISO code where we do not â never a wrong symbol. */ 111 function _group(text) { 112 var parts = String(text).split('.'); 113 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); 114 return parts.join('.'); 115 } 116 function money(minor, currency) { 117 var cur = String(currency || '').toLowerCase(); 118 var sym = CURRENCY[cur]; 119 var amount = (Number(minor) || 0) / 100; 120 /* ð» GROUPED, 2026-08-27. One was added here on 2026-08-26 and reverted the 121 same day, because this helper feeds the coach's screens and the buyer's 122 runtimes it has to agree with had none â "£1,497" beside "£1497.00" on 123 one row. Alex approved the platform-wide change: *"the thousand 124 separator, yeah definitely put that in."* All five moved together; see 125 `_deriveMainPriceLabel` in html-sanitiser.js. */ 126 var text = _group(amount.toFixed(amount % 1 === 0 ? 0 : 2)); 127 return sym ? sym + text : text + ' ' + cur.toUpperCase(); 128 } 129 130 function emptyBlock(icon, title, blurb) { 131 return '<div class="empty-block"><div style="font-size:28px">' + icon + '</div>' + 132 '<div style="font-weight:600;margin:6px 0 2px;color:var(--text)">' + esc(title) + '</div>' + 133 '<div class="cpg-section-sub" style="margin:0 auto;max-width:380px">' + esc(blurb) + '</div></div>'; 134 } 135 136 /** 137 * The honest empty state. Used wherever a tab has a place for something 138 * that is genuinely NOT built yet â the blueprint's rule is to name what 139 * will live there rather than render a fake control that does nothing. 140 */ 141 function soonBlock(icon, title, blurb) { 142 return '<div class="cpg-soon"><div class="t">' + icon + ' ' + esc(title) + '</div>' + blurb + '</div>'; 143 } 144 145 function tile(label, value, hint) { 146 return '<div class="cpg-tile"><div class="label">' + esc(label) + '</div>' + 147 '<div class="value">' + value + '</div>' + 148 (hint ? '<div class="hint">' + esc(hint) + '</div>' : '') + '</div>'; 149 } 150 151 function bar(n, d, done) { 152 var pct = Number(d) > 0 ? Math.round((Number(n) / Number(d)) * 100) : 0; 153 return '<div class="cpg-barline"><div class="cpg-bar' + (done ? ' done' : '') + '">' + 154 '<i style="width:' + pct + '%"></i></div><span class="pct">' + pct + '%</span></div>'; 155 } 156 157 /** A message line under a form. `el` is the <p class="cpg-msg">. */ 158 function setMsg(el, text, kind) { 159 if (!el) return; 160 el.className = 'cpg-msg' + (kind ? ' ' + kind : ''); 161 el.textContent = text || ''; 162 el.style.display = text ? '' : 'none'; 163 } 164
165 /* The helper surface, and nothing else. `render` and `TABS` were removed with 166 the shell â `render` had exactly one caller (subpages.js's course editor, 167 deleted in the same change) and `TABS` had none outside this file. */ 168 App.CoursePage = { 169 api: api, esc: esc, toast: toast, 170 fmtDate: fmtDate, timeAgo: timeAgo, money: money, 171 emptyBlock: emptyBlock, soonBlock: soonBlock, tile: tile, bar: bar, setMsg: setMsg, 172 }; 173})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.