bs-widget 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +56 -22
  2. package/dist/bundle.js +128 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,43 +2,77 @@
2
2
 
3
3
  Embeddable Beanstack goal widget bundle built with Rollup.
4
4
 
5
- ## Usage
5
+ ## Quick Start
6
6
 
7
- Add a container element where the widget should render:
7
+ Add a container element where the widget should render, include the script, then initialize the widget.
8
8
 
9
- ```html
10
- <div id="bs-widget"></div>
11
- ```
12
-
13
- Load the bundle (local build):
9
+ ### Option A: CDN (published package)
14
10
 
15
11
  ```html
16
- <script src="dist/bundle.js"></script>
12
+ <div id="bs-widget"></div>
13
+ <script src="https://unpkg.com/bs-widget@1.0.1/dist/bundle.js"></script>
14
+ <script>
15
+ let widget = new BSWidget({
16
+ microsite: 6,
17
+ container: "#bs-widget",
18
+ color: "purple",
19
+ styled: true,
20
+ loadingText: "Loading widget...",
21
+ apiBaseUrl: "https://beanstackedu.beanstack.org/api/v2/microsites_group_statistics/",
22
+ });
23
+ </script>
17
24
  ```
18
25
 
19
- Or load it from a CDN (after publishing to npm):
26
+ You can also use jsDelivr:
20
27
 
21
28
  ```html
22
- <script src="https://unpkg.com/bs-widget@latest/dist/bundle.js"></script>
29
+ <script src="https://cdn.jsdelivr.net/npm/bs-widget@1.0.1/dist/bundle.js"></script>
23
30
  ```
24
31
 
25
- ```html
26
- <script src="https://cdn.jsdelivr.net/npm/bs-widget@latest/dist/bundle.js"></script>
27
- ```
28
-
29
- Initialize the widget (same example as `index.html`):
32
+ ### Option B: Local build
30
33
 
31
34
  ```html
35
+ <div id="bs-widget"></div>
36
+ <script src="dist/bundle.js"></script>
32
37
  <script>
33
38
  let widget = new BSWidget({
34
39
  microsite: 6,
35
40
  container: "#bs-widget",
36
41
  color: "purple",
37
42
  styled: true,
43
+ loadingText: "Loading widget...",
44
+ apiBaseUrl: "https://beanstackedu.beanstack.org/api/v2/microsites_group_statistics/",
38
45
  });
39
46
  </script>
40
47
  ```
41
48
 
49
+ ## Configuration
50
+
51
+ `BSWidget` supports these commonly used options:
52
+
53
+ - `microsite` (number): microsite group id appended to `apiBaseUrl`
54
+ - `container` (string): CSS selector for target mount element
55
+ - `color` (string): base accent color for widget styles
56
+ - `styled` (boolean): inject widget styles when `true`
57
+ - `loadingText` (string): message shown while request is in flight
58
+ - `apiBaseUrl` (string): base endpoint that returns JSON statistics
59
+ - `requestTimeoutMs` (number): optional XHR timeout in milliseconds (`0` disables timeout)
60
+ - `onLoad` (function): optional callback called with fetched `statistic` payload
61
+ - `onError` (function): optional callback called with `{ statusCode, message }`
62
+
63
+ Default `apiBaseUrl`:
64
+
65
+ ```text
66
+ https://beanstackedu.beanstack.org/api/v2/microsites_group_statistics/
67
+ ```
68
+
69
+ If your endpoint redirects to an HTML page instead of returning JSON, the widget cannot render stats from that URL.
70
+
71
+ ### Lifecycle methods
72
+
73
+ - `widget.refresh()` re-fetches and re-renders the widget in place.
74
+ - `widget.destroy()` removes the rendered widget and aborts any in-flight request.
75
+
42
76
  ## Requirements
43
77
 
44
78
  - Node.js 18+
@@ -46,13 +80,13 @@ Initialize the widget (same example as `index.html`):
46
80
 
47
81
  ## Development
48
82
 
49
- - Install dependencies:
83
+ Install dependencies:
50
84
 
51
85
  ```bash
52
86
  yarn install
53
87
  ```
54
88
 
55
- - Start local preview with watch support (recommended):
89
+ Start local preview with watch support (recommended):
56
90
 
57
91
  ```bash
58
92
  yarn dev
@@ -60,19 +94,19 @@ yarn dev
60
94
 
61
95
  This runs Rollup in watch mode and serves the project at `http://localhost:8080`.
62
96
 
63
- - Start local preview and auto-open in browser:
97
+ Start local preview and auto-open in browser:
64
98
 
65
99
  ```bash
66
100
  yarn dev:open
67
101
  ```
68
102
 
69
- - Start only Rollup in watch mode:
103
+ Start only Rollup in watch mode:
70
104
 
71
105
  ```bash
72
106
  yarn start
73
107
  ```
74
108
 
75
- - Build once:
109
+ Build once:
76
110
 
77
111
  ```bash
78
112
  yarn build
@@ -80,7 +114,7 @@ yarn build
80
114
 
81
115
  ## Testing
82
116
 
83
- - Run smoke test (build + verification):
117
+ Run smoke test (build + verification):
84
118
 
85
119
  ```bash
86
120
  yarn test
@@ -90,7 +124,7 @@ The smoke test checks that `dist/bundle.js` is generated and includes the `BSWid
90
124
 
91
125
  ## Release checks
92
126
 
93
- - Run all checks before publishing:
127
+ Run all checks before publishing:
94
128
 
95
129
  ```bash
96
130
  yarn verify
package/dist/bundle.js CHANGED
@@ -1232,33 +1232,142 @@ var BSWidget = (function () {
1232
1232
 
1233
1233
  function BSWidget(options) {
1234
1234
  var _this = this;
1235
- var cors = "https://beanstack-cors-anywhere.herokuapp.com/";
1236
- var api = "https://beanstackedu.beanstack.com/api/v2/microsites_group_statistics/";
1235
+ var apiBaseUrl = "https://beanstackedu.beanstack.org/api/v2/microsites_group_statistics/";
1237
1236
  var defaults = {
1238
1237
  microsite: 6,
1239
1238
  container: "#bs-widget",
1240
1239
  color: "#2323FA",
1241
- styled: true
1240
+ styled: true,
1241
+ loadingText: "Loading widget...",
1242
+ apiBaseUrl: apiBaseUrl,
1243
+ requestTimeoutMs: 0,
1244
+ onLoad: null,
1245
+ onError: null
1242
1246
  };
1243
1247
  var heart = '<svg width="20" height="29" viewBox="0 0 20 29" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.10466 9.60099C-1.42936 13.4764 0.777223 19.0596 14.6388 28.4978C14.9823 28.7315 15.4783 28.6482 15.153 28.0563L15.0603 27.8838C14.1577 26.1622 13.1114 22.8931 15.6222 18.7861L15.8194 18.4598C19.007 13.1288 22.0554 5.13492 17.2642 1.50079C12.3347 -2.23819 6.60583 2.63878 6.46388 8.35695L6.3637 8.29836C5.80513 7.98761 3.03132 6.65437 1.10466 9.60099Z"/></svg>';
1244
1248
  var clock = '<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 2.16667C5.3975 2.16667 1.66666 5.89751 1.66666 10.5C1.66666 15.1025 5.3975 18.8333 10 18.8333C14.6025 18.8333 18.3333 15.1025 18.3333 10.5C18.3333 5.89751 14.6025 2.16667 10 2.16667ZM12.155 13.8333L9.41083 11.0892C9.25416 10.9325 9.16666 10.7208 9.16666 10.5V6.33334C9.16666 5.87334 9.54 5.50001 10 5.50001C10.46 5.50001 10.8333 5.87334 10.8333 6.33334V10.155L13.3333 12.655C13.6583 12.98 13.6583 13.5083 13.3333 13.8333C13.0083 14.1583 12.48 14.1583 12.155 13.8333Z"/></svg>';
1245
1249
  var styles = "#bs-widget,.bs-widget-container{display:-webkit-box;display:-ms-flexbox;-webkit-box-orient:vertical;-webkit-box-direction:normal}#bs-widget,.bs-widget-container,.bs-widget-title-row{-webkit-box-direction:normal}#bs-widget{display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:24px;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;font-family:Arial,sans-serif;font-style:normal;--bs-color:red;--bs-light:red;--bs-dark:red}.bs-widget-container{-webkit-box-sizing:border-box;box-sizing:border-box;display:flex;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;padding:32px;gap:28px;position:relative;width:500px;background:#fff;border:2px solid #eaeaea;border-radius:16px}.bs-widget-title-row{width:100%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;gap:4px}.bs-widget-title-row .bs-widget-title-container{display:-webkit-box;display:-ms-flexbox;display:flex;gap:12px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.bs-widget-goal-row,.bs-widget-title-row .bs-widget-endson-container{display:-webkit-box;display:-ms-flexbox;-webkit-box-direction:normal}.bs-widget-title-row .bs-widget-endson-container .bs-widget-clock svg,.bs-widget-title-row .bs-widget-title-container .bs-widget-heart svg{fill:var(--bs-color)}.bs-widget-title-row .bs-widget-title-container .bs-widget-title{font-weight:700;font-size:20px;line-height:25px;color:var(--bs-color)}.bs-widget-title-row .bs-widget-endson-container{display:flex;-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row;gap:8px;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.bs-widget-title-row .bs-widget-endson-container .bs-widget-endson{font-weight:700;font-size:15px;line-height:20px;text-align:right;color:#656565;position:relative;top:-1px}.bs-widget-goal-row{width:100%;display:flex;-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column;gap:12px}.bs-widget-goal-row .bs-widget-goal-text{color:#656565}.bs-widget-goal-row .bs-widget-goal-text strong{font-weight:700;font-size:30px;line-height:34px;color:var(--bs-color)}.bs-widget-goal-row .bs-widget-goal-text span{font-weight:700;font-size:16px}@media screen and (max-width:350px){.bs-widget-title-row .bs-widget-title-container .bs-widget-heart svg{width:12px;height:22px}.bs-widget-title-row .bs-widget-title-container .bs-widget-title{font-size:18px;line-height:22px}.bs-widget-title-row .bs-widget-endson-container .bs-widget-clock{display:none}.bs-widget-title-row .bs-widget-endson-container .bs-widget-endson{font-size:14px;line-height:18px}.bs-widget-goal-row .bs-widget-goal-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;gap:2px}.bs-widget-goal-row .bs-widget-goal-text strong{font-size:24px;line-height:30px}.bs-widget-goal-row .bs-widget-goal-text span{font-size:15px;line-height:18px}}.bs-widget-goal-row .bs-widget-goal{margin-top:4px}.bs-widget-goal-row .bs-widget-goal .bs-widget-goal-bar{height:8px;width:100%;background:var(--bs-light);border-radius:8px;position:relative}.bs-widget-goal-row .bs-widget-goal .bs-widget-goal-progress{background:var(--bs-color);border-radius:8px;height:100%;}.bs-widget-goal-row .bs-widget-goal .bs-widget-goal-indicator{width:8px;height:8px;background:#fff;border:4px solid var(--bs-color);-webkit-box-shadow:0 0 0 4px #fff;box-shadow:0 0 0 4px #fff;border-radius:100%;position:absolute;margin-left:-4px;left:0px;top:0;margin-top:-4px}.bs-widget-last-updated-row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;gap:8px}.bs-widget-last-updated-row .bs-widget-last-updated{font-weight:400;font-size:14px;line-height:18px;color:#707070}.bs-widget-last-updated-row a.bs-widget-visit{font-weight:400;font-size:14px;line-height:18px;text-align:center;-webkit-text-decoration-line:underline;text-decoration-line:underline;color:var(--bs-dark)}@media screen and (max-width:450px){.bs-widget-title-row{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.bs-widget-last-updated-row{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.bs-widget-last-updated-row .bs-widget-last-updated,.bs-widget-last-updated-row a.bs-widget-visit{font-size:13px;line-height:16px}}";
1246
1250
  options = _objectSpread2(_objectSpread2({}, defaults), options);
1251
+ this.containerElement = document.querySelector(options.container);
1252
+ this.loadingElement = null;
1253
+ this.renderedElement = null;
1254
+ this.activeRequest = null;
1255
+ this.invokeCallback = function (callback, payload) {
1256
+ if (typeof callback === "function") {
1257
+ callback(payload, this);
1258
+ }
1259
+ };
1260
+ this.showLoading = function () {
1261
+ if (!this.containerElement || this.loadingElement) {
1262
+ return;
1263
+ }
1264
+ var loadingContainer = document.createElement("div");
1265
+ loadingContainer.setAttribute("class", "bs-widget-container bs-widget-loading");
1266
+ loadingContainer.style.setProperty("--bs-color", tinycolor(options.color).toHexString());
1267
+ loadingContainer.style.setProperty("--bs-dark", tinycolor(options.color).darken().toHexString());
1268
+ loadingContainer.style.setProperty("--bs-light", tinycolor(options.color).lighten(30).toHexString());
1269
+ var style = document.createElement("style");
1270
+ if (options.styled == true) {
1271
+ style.innerHTML = styles;
1272
+ }
1273
+ loadingContainer.appendChild(style);
1274
+ var loadingRow = document.createElement("div");
1275
+ loadingRow.setAttribute("class", "bs-widget-goal-row");
1276
+ loadingContainer.appendChild(loadingRow);
1277
+ var loadingText = document.createElement("div");
1278
+ loadingText.setAttribute("class", "bs-widget-goal-text");
1279
+ loadingText.textContent = options.loadingText;
1280
+ loadingRow.appendChild(loadingText);
1281
+ this.containerElement.appendChild(loadingContainer);
1282
+ this.loadingElement = loadingContainer;
1283
+ };
1284
+ this.hideLoading = function () {
1285
+ if (this.loadingElement && this.loadingElement.parentNode) {
1286
+ this.loadingElement.parentNode.removeChild(this.loadingElement);
1287
+ }
1288
+ this.loadingElement = null;
1289
+ };
1290
+ this.removeRenderedElement = function () {
1291
+ if (this.renderedElement && this.renderedElement.parentNode) {
1292
+ this.renderedElement.parentNode.removeChild(this.renderedElement);
1293
+ }
1294
+ this.renderedElement = null;
1295
+ };
1296
+ this.destroy = function () {
1297
+ if (this.activeRequest) {
1298
+ this.activeRequest.abort();
1299
+ this.activeRequest = null;
1300
+ }
1301
+ this.hideLoading();
1302
+ this.removeRenderedElement();
1303
+ };
1304
+ this.refresh = function () {
1305
+ this.removeRenderedElement();
1306
+ this.loadRequest();
1307
+ };
1247
1308
  this.loadRequest = function () {
1309
+ _this.showLoading();
1248
1310
  var req = new XMLHttpRequest();
1311
+ var requestResolved = false;
1312
+ _this.activeRequest = req;
1313
+ var completeWithSuccess = function completeWithSuccess(responseText) {
1314
+ if (requestResolved) {
1315
+ return;
1316
+ }
1317
+ requestResolved = true;
1318
+ _this.activeRequest = null;
1319
+ _this.hideLoading();
1320
+ var data = JSON.parse(responseText);
1321
+ _this.init(data.statistic);
1322
+ _this.invokeCallback(options.onLoad, data.statistic);
1323
+ };
1324
+ var completeWithError = function completeWithError(statusCode) {
1325
+ if (requestResolved) {
1326
+ return;
1327
+ }
1328
+ requestResolved = true;
1329
+ _this.activeRequest = null;
1330
+ _this.hideLoading();
1331
+ _this.error(statusCode);
1332
+ _this.invokeCallback(options.onError, {
1333
+ statusCode: statusCode,
1334
+ message: _this.getErrorMessage(statusCode)
1335
+ });
1336
+ };
1249
1337
  req.onreadystatechange = function () {
1250
1338
  if (this.readyState === 4 && this.status === 200) {
1251
- var data = JSON.parse(this.responseText);
1252
- _this.init(data.statistic);
1339
+ completeWithSuccess(this.responseText);
1253
1340
  } else if (this.readyState === 4 && this.status != 200) {
1254
- _this.error(this.status);
1341
+ completeWithError(this.status);
1255
1342
  }
1256
1343
  };
1257
- req.open("GET", cors + api + options.microsite);
1258
- req.setRequestHeader("Content-Type", "application/json");
1344
+ req.onerror = function () {
1345
+ completeWithError(0);
1346
+ };
1347
+ req.ontimeout = function () {
1348
+ completeWithError(408);
1349
+ };
1350
+ var requestUrl = options.apiBaseUrl + options.microsite;
1351
+ req.open("GET", requestUrl);
1259
1352
  req.setRequestHeader("Accept", "application/json");
1353
+ req.setRequestHeader("Content-Type", "application/json");
1354
+ if (options.requestTimeoutMs > 0) {
1355
+ req.timeout = options.requestTimeoutMs;
1356
+ }
1260
1357
  req.send();
1261
1358
  };
1359
+ this.getErrorMessage = function (errorCode) {
1360
+ if (errorCode === 0) {
1361
+ return "Request blocked by CORS or network policy.";
1362
+ }
1363
+ if (errorCode === 403) {
1364
+ return "Access forbidden by the data source.";
1365
+ }
1366
+ if (errorCode === 408) {
1367
+ return "Request timed out.";
1368
+ }
1369
+ return "Request failed with status " + errorCode + ".";
1370
+ };
1262
1371
  this.init = function (data) {
1263
1372
  var endDate = new Date(data.end_date).toLocaleDateString("en-us", {
1264
1373
  year: "numeric",
@@ -1349,20 +1458,28 @@ var BSWidget = (function () {
1349
1458
  // visitLink.innerHTML = "Visit our Beanstack site";
1350
1459
  // lastUpdatedRow.appendChild(visitLink);
1351
1460
 
1352
- document.querySelector(options.container).appendChild(container);
1461
+ if (_this.containerElement) {
1462
+ _this.removeRenderedElement();
1463
+ _this.containerElement.appendChild(container);
1464
+ _this.renderedElement = container;
1465
+ }
1353
1466
  };
1354
1467
  this.error = function (error) {
1355
1468
  var container = document.createElement("div");
1356
1469
  container.setAttribute("class", "bs-widget-container");
1357
1470
  container.classList.add("error");
1358
- container.innerHTML = error;
1471
+ container.textContent = _this.getErrorMessage(error);
1359
1472
  container.style.setProperty("--bs-color", tinycolor(options.color).toHexString());
1360
1473
  container.style.setProperty("--bs-dark", tinycolor(options.color).darken().toHexString());
1361
1474
  container.style.setProperty("--bs-light", tinycolor(options.color).lighten(30).toHexString());
1362
1475
  var style = document.createElement("style");
1363
1476
  style.innerHTML = styles;
1364
1477
  container.appendChild(style);
1365
- document.querySelector(options.container).appendChild(container);
1478
+ if (_this.containerElement) {
1479
+ _this.removeRenderedElement();
1480
+ _this.containerElement.appendChild(container);
1481
+ _this.renderedElement = container;
1482
+ }
1366
1483
  };
1367
1484
  this.loadRequest();
1368
1485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bs-widget",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "dist/bundle.js",
6
6
  "type": "module",