PageSourceSearch

https://app.replicants.ai/js/course-page.js?v=20260927dates2

js replicants.ai collected 2026-10-05 12:30:30 UTC 7,809 bytes, 173 lines download raw bytes

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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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.