@kelviq/js-promotions-ui 0.0.1

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.
package/index.html ADDED
@@ -0,0 +1,485 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>KQ Promotions UI — Demo</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ body {
14
+ font-family: system-ui, sans-serif;
15
+ background: #f5f5f5;
16
+ color: #222;
17
+ }
18
+ h1 {
19
+ font-size: 1.5rem;
20
+ margin-bottom: 1rem;
21
+ }
22
+ h2 {
23
+ font-size: 1.1rem;
24
+ margin-bottom: 0.75rem;
25
+ color: #444;
26
+ }
27
+
28
+ .page {
29
+ max-width: 900px;
30
+ margin: 0 auto;
31
+ padding: 2rem 1rem;
32
+ }
33
+
34
+ /* Section */
35
+ .section {
36
+ background: #fff;
37
+ border: 1px solid #e0e0e0;
38
+ border-radius: 8px;
39
+ padding: 1.5rem;
40
+ margin-bottom: 1.5rem;
41
+ }
42
+ .section-title {
43
+ font-size: 0.7rem;
44
+ font-weight: 700;
45
+ letter-spacing: 0.08em;
46
+ text-transform: uppercase;
47
+ color: #888;
48
+ margin-bottom: 0.5rem;
49
+ }
50
+
51
+ /* Cards */
52
+ .cards {
53
+ display: flex;
54
+ gap: 1rem;
55
+ flex-wrap: wrap;
56
+ }
57
+ .card {
58
+ background: #f9f9f9;
59
+ border: 1px solid #e0e0e0;
60
+ border-radius: 6px;
61
+ padding: 1rem;
62
+ min-width: 160px;
63
+ }
64
+ .card-label {
65
+ font-size: 0.75rem;
66
+ color: #888;
67
+ margin-bottom: 0.25rem;
68
+ }
69
+ .card-name {
70
+ font-weight: 600;
71
+ margin-bottom: 0.5rem;
72
+ }
73
+
74
+ /* Price display */
75
+ .price {
76
+ font-size: 1.8rem;
77
+ font-weight: 700;
78
+ line-height: 1;
79
+ }
80
+ .price sup {
81
+ font-size: 1rem;
82
+ vertical-align: super;
83
+ }
84
+ .price sub {
85
+ font-size: 0.9rem;
86
+ }
87
+ .original-price {
88
+ font-size: 1rem;
89
+ color: #999;
90
+ text-decoration: line-through;
91
+ margin-top: 0.25rem;
92
+ }
93
+ .discount-badge {
94
+ display: inline-block;
95
+ background: #e53e3e;
96
+ color: #fff;
97
+ font-size: 0.7rem;
98
+ font-weight: 700;
99
+ padding: 2px 6px;
100
+ border-radius: 4px;
101
+ margin-top: 0.4rem;
102
+ }
103
+ .coupon {
104
+ font-size: 0.8rem;
105
+ margin-top: 0.4rem;
106
+ color: #555;
107
+ }
108
+ .coupon code {
109
+ background: #fffbea;
110
+ border: 1px dashed #ccc;
111
+ padding: 2px 6px;
112
+ border-radius: 4px;
113
+ font-weight: 600;
114
+ }
115
+
116
+ /* Button */
117
+ button {
118
+ background: #3182ce;
119
+ color: #fff;
120
+ border: none;
121
+ border-radius: 5px;
122
+ padding: 0.5rem 1rem;
123
+ cursor: pointer;
124
+ font-size: 0.85rem;
125
+ }
126
+ button:hover {
127
+ background: #2b6cb0;
128
+ }
129
+
130
+ /* Output box */
131
+ pre {
132
+ background: #1a1a2e;
133
+ color: #a8dadc;
134
+ border-radius: 6px;
135
+ padding: 1rem;
136
+ font-size: 0.78rem;
137
+ overflow-x: auto;
138
+ margin-top: 0.75rem;
139
+ white-space: pre-wrap;
140
+ }
141
+
142
+ /* Table */
143
+ table {
144
+ width: 100%;
145
+ border-collapse: collapse;
146
+ font-size: 0.85rem;
147
+ }
148
+ th {
149
+ text-align: left;
150
+ font-size: 0.7rem;
151
+ text-transform: uppercase;
152
+ letter-spacing: 0.05em;
153
+ color: #888;
154
+ padding: 0.4rem 0.5rem;
155
+ border-bottom: 1px solid #eee;
156
+ }
157
+ td {
158
+ padding: 0.5rem;
159
+ border-bottom: 1px solid #f0f0f0;
160
+ }
161
+ td:first-child {
162
+ font-weight: 600;
163
+ }
164
+
165
+ /* kq-discount-applied body class demo */
166
+ body.kq-discount-applied .discount-badge {
167
+ display: inline-block;
168
+ }
169
+ .discount-badge {
170
+ display: none;
171
+ }
172
+ .kq-original-price-display {
173
+ display: none;
174
+ }
175
+ body.kq-discount-applied .kq-original-price-display {
176
+ display: block;
177
+ }
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <div class="page">
182
+ <h1>KQ Promotions UI — Demo</h1>
183
+
184
+ <!-- ─── 1. BANNER ──────────────────────────────────────────────── -->
185
+ <div class="section">
186
+ <div class="section-title">1 · Banner Widget</div>
187
+ <h2>Auto-rendered via <code>widgets[type=BANNER]</code></h2>
188
+ <p style="font-size: 0.85rem; color: #666">
189
+ The banner is injected automatically by the SDK after
190
+ <code>init()</code>. Look above the page for the banner if the API
191
+ returns one.
192
+ </p>
193
+ </div>
194
+
195
+ <!-- ─── 2. AUTO PRICE (data-kq-price) ─────────────────────────── -->
196
+ <div class="section">
197
+ <div class="section-title">
198
+ 2 · Auto Price Update — <code>data-kq-price</code>
199
+ </div>
200
+ <h2>SDK updates these elements automatically on init</h2>
201
+ <div class="cards">
202
+ <!-- Basic: price only -->
203
+ <div class="card" data-kq-price="9.99">
204
+ <div class="card-label">Basic</div>
205
+ <div class="card-name">Starter</div>
206
+ <div class="price">
207
+ <sup data-kq-currency-symbol></sup
208
+ ><span data-kq-price-integer></span>
209
+ </div>
210
+ <div class="discount-badge">Sale!</div>
211
+ </div>
212
+
213
+ <!-- Pro: price + original + decimal -->
214
+ <div class="card">
215
+ <div class="card-label">Pro</div>
216
+ <div class="card-name">Growth</div>
217
+ <div class="price" data-kq-price="49" data-kq-show-decimal="true">
218
+ <sup data-kq-currency-symbol></sup
219
+ ><span data-kq-price-integer></span
220
+ ><span data-kq-price-decimal-separator></span
221
+ ><span data-kq-price-decimal></span>
222
+ </div>
223
+ <div
224
+ class="original-price kq-original-price-display"
225
+ data-kq-original-price-display="79"
226
+ data-kq-rel="growth-price"
227
+ >
228
+ <span data-kq-currency-symbol></span
229
+ ><span data-kq-price-formatted></span>
230
+ </div>
231
+ <div class="discount-badge">Limited offer</div>
232
+ </div>
233
+
234
+ <!-- Enterprise: formatted price + currency code -->
235
+ <div class="card">
236
+ <div class="card-label">Enterprise</div>
237
+ <div class="card-name">Scale</div>
238
+ <div
239
+ class="price"
240
+ data-kq-price="199"
241
+ data-kq-currency-display="code"
242
+ >
243
+ <span data-kq-price-formatted></span>
244
+ </div>
245
+ <div
246
+ class="original-price kq-original-price-display"
247
+ data-kq-original-price-display="299"
248
+ data-kq-currency-display="code"
249
+ >
250
+ <span data-kq-price-formatted></span>
251
+ </div>
252
+ <div class="discount-badge">Sale!</div>
253
+ </div>
254
+
255
+ <!-- Lifetime: no decimal -->
256
+ <div class="card" data-kq-price="499" data-kq-show-decimal="false">
257
+ <div class="card-label">Lifetime</div>
258
+ <div class="card-name">Forever</div>
259
+ <div class="price">
260
+ <sup data-kq-currency-symbol></sup
261
+ ><span data-kq-price-integer></span>
262
+ </div>
263
+ <div class="discount-badge">Best value</div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <!-- ─── 3. CURRENCY DISPLAY VARIANTS ──────────────────────────── -->
269
+ <div class="section">
270
+ <div class="section-title">3 · Currency Display Variants</div>
271
+ <h2>Same price, different <code>data-kq-currency-display</code></h2>
272
+ <table>
273
+ <tr>
274
+ <th>Option</th>
275
+ <th>Formatted Output</th>
276
+ </tr>
277
+ <tr>
278
+ <td>symbol (default)</td>
279
+ <td data-kq-price="99" data-kq-currency-display="symbol">
280
+ <span data-kq-price-formatted></span>
281
+ </td>
282
+ </tr>
283
+ <tr>
284
+ <td>code</td>
285
+ <td data-kq-price="99" data-kq-currency-display="code">
286
+ <span data-kq-price-formatted></span>
287
+ </td>
288
+ </tr>
289
+ <tr>
290
+ <td>name</td>
291
+ <td data-kq-price="99" data-kq-currency-display="name">
292
+ <span data-kq-price-formatted></span>
293
+ </td>
294
+ </tr>
295
+ </table>
296
+ </div>
297
+
298
+ <!-- ─── 4. getUpdatedPrice (JS API) ───────────────────────────── -->
299
+ <div class="section">
300
+ <div class="section-title">
301
+ 4 · <code>getUpdatedPrice</code> — JS API
302
+ </div>
303
+ <h2>Fetch a computed price programmatically</h2>
304
+ <button onclick="testGetUpdatedPrice()">
305
+ Run getUpdatedPrice(100)
306
+ </button>
307
+ <pre id="get-price-output">— click the button —</pre>
308
+ </div>
309
+
310
+ <!-- ─── 5. updatePriceElement (JS API) ───────────────────────── -->
311
+ <div class="section">
312
+ <div class="section-title">
313
+ 5 · <code>updatePriceElement</code> — JS API
314
+ </div>
315
+ <h2>Update a specific element by reference</h2>
316
+ <div class="card" style="margin-bottom: 0.75rem">
317
+ <div class="card-label">Target element</div>
318
+ <div
319
+ id="update-element-target"
320
+ data-kq-price="250"
321
+ data-kq-show-decimal="true"
322
+ >
323
+ <div class="price">
324
+ <sup data-kq-currency-symbol></sup
325
+ ><span data-kq-price-integer></span
326
+ ><span data-kq-price-decimal-separator></span
327
+ ><span data-kq-price-decimal></span>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ <button onclick="testUpdatePriceElement()">
332
+ Call updatePriceElement
333
+ </button>
334
+ <pre id="update-element-output">— click the button —</pre>
335
+ </div>
336
+
337
+ <!-- ─── 6. updatePrice with template (JS API) ─────────────────── -->
338
+ <div class="section">
339
+ <div class="section-title">
340
+ 6 · <code>updatePrice</code> — Batch with Template
341
+ </div>
342
+ <h2>Inject rendered price into arbitrary HTML</h2>
343
+ <div
344
+ id="template-output-1"
345
+ class="card"
346
+ style="margin-bottom: 0.5rem; min-height: 40px"
347
+ ></div>
348
+ <div
349
+ id="template-output-2"
350
+ class="card"
351
+ style="margin-bottom: 0.75rem; min-height: 40px"
352
+ ></div>
353
+ <button onclick="testUpdatePrice()">Run updatePrice batch</button>
354
+ <pre id="update-price-output">— click the button —</pre>
355
+ </div>
356
+
357
+ <!-- ─── 7. API Response ────────────────────────────────────────── -->
358
+ <div class="section">
359
+ <div class="section-title">7 · Raw API Response</div>
360
+ <h2>
361
+ Full <code>apiResponse</code> from last
362
+ <code>getUpdatedPrice</code> call
363
+ </h2>
364
+ <pre id="api-response-output">— run section 4 first —</pre>
365
+ </div>
366
+ </div>
367
+
368
+ <!-- Set up queue before SDK module loads (module scripts are deferred) -->
369
+ <script>
370
+ window.KQPromotionUISDK =
371
+ window.KQPromotionUISDK ||
372
+ function () {
373
+ (window.KQPromotionUISDK.q = window.KQPromotionUISDK.q || []).push(
374
+ Array.prototype.slice.call(arguments)
375
+ );
376
+ };
377
+ </script>
378
+
379
+ <!-- Load SDK directly from source — Vite handles TS + HMR -->
380
+ <script type="module" src="./src/index.ts"></script>
381
+
382
+ <script>
383
+ function resolvePdSDKFunction(e) {
384
+ var args = Array.prototype.slice.call(arguments, 1);
385
+ return new Promise(function (resolve, reject) {
386
+ !(function poll() {
387
+ window.KQPromotionUISDK &&
388
+ typeof window.KQPromotionUISDK[e] === "function"
389
+ ? window.KQPromotionUISDK[e]
390
+ .apply(null, args)
391
+ .then(resolve)
392
+ .catch(reject)
393
+ : setTimeout(poll, 50);
394
+ })();
395
+ });
396
+ }
397
+
398
+ window.KQPromotionUI = {
399
+ init: function (e) {
400
+ KQPromotionUISDK("init", e);
401
+ },
402
+ getUpdatedPrice: function (e, t) {
403
+ return resolvePdSDKFunction("getUpdatedPrice", e, t);
404
+ },
405
+ updatePriceElement: function (e, t) {
406
+ return resolvePdSDKFunction("updatePriceElement", e, t);
407
+ },
408
+ updatePrice: function (e) {
409
+ return resolvePdSDKFunction("updatePrice", e);
410
+ },
411
+ };
412
+
413
+ KQPromotionUI.init({
414
+ promotionId: "ade58436-0278-45cc-b3a4-3df2d62d340e",
415
+ accessToken:
416
+ "client-463ea556-ae15-4ae3-8728-cfa13f8f8086:58b13c32-4919-419b-8fc9-f6dc0ef64c11",
417
+ showBanner: true,
418
+ baseCurrencyCode: "USD",
419
+ baseCurrencySymbol: "$",
420
+ showDecimal: false,
421
+ });
422
+
423
+ // ── Section 4: getUpdatedPrice ──
424
+ function testGetUpdatedPrice() {
425
+ document.getElementById("get-price-output").textContent = "Loading…";
426
+ KQPromotionUI.getUpdatedPrice(100, { showDecimal: true })
427
+ .then(function (result) {
428
+ document.getElementById("get-price-output").textContent =
429
+ JSON.stringify(result, null, 2);
430
+ document.getElementById("api-response-output").textContent =
431
+ JSON.stringify(result.apiResponse, null, 2);
432
+ })
433
+ .catch(function (err) {
434
+ document.getElementById("get-price-output").textContent =
435
+ "Error: " + err;
436
+ });
437
+ }
438
+
439
+ // ── Section 5: updatePriceElement ──
440
+ function testUpdatePriceElement() {
441
+ var el = document.getElementById("update-element-target");
442
+ document.getElementById("update-element-output").textContent =
443
+ "Loading…";
444
+ KQPromotionUI.updatePriceElement(el, { showDecimal: true })
445
+ .then(function (result) {
446
+ document.getElementById("update-element-output").textContent =
447
+ JSON.stringify(result, null, 2);
448
+ })
449
+ .catch(function (err) {
450
+ document.getElementById("update-element-output").textContent =
451
+ "Error: " + err;
452
+ });
453
+ }
454
+
455
+ // ── Section 6: updatePrice with template ──
456
+ function testUpdatePrice() {
457
+ document.getElementById("update-price-output").textContent = "Loading…";
458
+ KQPromotionUI.updatePrice([
459
+ {
460
+ element: document.getElementById("template-output-1"),
461
+ price: 29,
462
+ options: { showDecimal: false },
463
+ template:
464
+ "<div class='card-label'>Monthly</div><div class='price'>{{formattedPrice}} <small style='font-size:0.7rem;color:#888'>/mo</small></div>",
465
+ },
466
+ {
467
+ element: document.getElementById("template-output-2"),
468
+ price: 199,
469
+ options: { showDecimal: true },
470
+ template:
471
+ "<div class='card-label'>Annual</div><div class='price'>{{formattedPrice}} <small style='font-size:0.7rem;color:#888'>/yr</small></div><div class='coupon'>Code: <code>{{currencyCode}}</code></div>",
472
+ },
473
+ ])
474
+ .then(function (results) {
475
+ document.getElementById("update-price-output").textContent =
476
+ JSON.stringify(results, null, 2);
477
+ })
478
+ .catch(function (err) {
479
+ document.getElementById("update-price-output").textContent =
480
+ "Error: " + err;
481
+ });
482
+ }
483
+ </script>
484
+ </body>
485
+ </html>
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@kelviq/js-promotions-ui",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "module": "./dist/js-promotions-ui.js",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/js-promotions-ui.js"
10
+ },
11
+ "./dist/": {
12
+ "import": "./dist/"
13
+ }
14
+ },
15
+ "types": "./dist/index.d.ts",
16
+ "scripts": {
17
+ "dev": "vite --host",
18
+ "build": "rimraf build/**/* && tsc && vite build && dts-bundle-generator --config ./dts-bundle-generator.config.ts && copyfiles ./package.json build",
19
+ "minify:public-script": "terser src/public-script.js -o build/dist/public-script.min.js --compress --mangle",
20
+ "test": "vitest",
21
+ "commit": "cz",
22
+ "test:coverage": "vitest --coverage",
23
+ "lint:scripts": "eslint . --ext .ts",
24
+ "lint:styles": "stylelint ./**/*.{css,scss}",
25
+ "format:scripts": "prettier . --write",
26
+ "format:styles": "stylelint ./**/*.{css,scss} --fix",
27
+ "format": "npm run format:scripts && npm run format:styles",
28
+ "prepare": "husky && echo 'npx lint-staged' > .husky/pre-commit && git add .husky/pre-commit && husky install",
29
+ "uninstall-husky": "npm uninstall husky --no-save && git config --unset core.hooksPath && npx rimraf .husky",
30
+ "prepublishOnly": "npm run build",
31
+ "publish:beta": "bash scripts/publish.sh beta",
32
+ "publish:stable": "bash scripts/publish.sh stable"
33
+ },
34
+ "config": {
35
+ "commitizen": {
36
+ "path": "cz-conventional-changelog"
37
+ }
38
+ },
39
+ "lint-staged": {
40
+ "*.{js,ts,tsx,jsx}": [
41
+ "prettier --write",
42
+ "eslint --fix"
43
+ ]
44
+ },
45
+ "devDependencies": {
46
+ "@commitlint/cli": "^19.8.1",
47
+ "@commitlint/config-conventional": "^19.8.1",
48
+ "@types/jsdom": "^21.1.7",
49
+ "@types/node": "^22.0.0",
50
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
51
+ "@typescript-eslint/parser": "^7.18.0",
52
+ "@vitest/coverage-v8": "^2.0.4",
53
+ "commitizen": "^4.3.1",
54
+ "copyfiles": "^2.4.1",
55
+ "cz-conventional-changelog": "^3.3.0",
56
+ "dts-bundle-generator": "^9.5.1",
57
+ "eslint": "^8.57.1",
58
+ "eslint-config-prettier": "^9.1.0",
59
+ "eslint-plugin-prettier": "^5.2.1",
60
+ "husky": "^8.0.0",
61
+ "jsdom": "^26.1.0",
62
+ "lint-staged": "^15.5.2",
63
+ "postcss": "^8.4.40",
64
+ "postcss-scss": "^4.0.9",
65
+ "prettier": "^3.5.3",
66
+ "rimraf": "^6.0.1",
67
+ "stylelint": "^16.8.1",
68
+ "stylelint-config-recommended": "^14.0.1",
69
+ "stylelint-config-sass-guidelines": "^12.0.0",
70
+ "stylelint-order": "^6.0.4",
71
+ "stylelint-prettier": "^5.0.2",
72
+ "terser": "^5.43.0",
73
+ "ts-node": "^10.9.2",
74
+ "typescript": "^5.3.3",
75
+ "vite": "^5.3.5",
76
+ "vite-plugin-static-copy": "^3.0.2",
77
+ "vitest": "^2.0.4"
78
+ }
79
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ── Usage ──
5
+ TAG="${1:-}"
6
+ if [[ "$TAG" != "beta" && "$TAG" != "stable" ]]; then
7
+ echo "Usage: bash scripts/publish.sh <beta|stable>"
8
+ exit 1
9
+ fi
10
+
11
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
12
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
13
+
14
+ # ── Step 1: Verify npm identity ──
15
+ echo ""
16
+ echo "Checking npm login status..."
17
+ NPM_USER=$(npm whoami 2>/dev/null) || {
18
+ echo "Error: You are not logged in to npm."
19
+ echo "Run 'npm login' first."
20
+ exit 1
21
+ }
22
+
23
+ # ── Step 2: Check git state ──
24
+ if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
25
+ echo ""
26
+ echo "Warning: You have uncommitted changes."
27
+ read -r -p "Continue anyway? (y/N) " git_confirm
28
+ if [[ "$git_confirm" != "y" && "$git_confirm" != "Y" ]]; then
29
+ echo "Aborted."
30
+ exit 1
31
+ fi
32
+ fi
33
+
34
+ # ── Step 3: Confirm publish details ──
35
+ echo ""
36
+ echo "Publishing $PACKAGE_NAME"
37
+ echo " Version : $PACKAGE_VERSION"
38
+ echo " Tag : $TAG"
39
+ echo " npm user: $NPM_USER"
40
+ echo ""
41
+ read -r -p "Proceed? (y/N) " confirm
42
+ if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
43
+ echo "Aborted."
44
+ exit 0
45
+ fi
46
+
47
+ # ── Step 4: Quality checks ──
48
+ echo ""
49
+ echo "Running tests..."
50
+ npm run test -- --run
51
+
52
+ echo ""
53
+ echo "Building..."
54
+ npm run build
55
+
56
+ # ── Step 5: Publish ──
57
+ echo ""
58
+ if [[ "$TAG" == "beta" ]]; then
59
+ npm publish --tag beta --access public
60
+ else
61
+ npm publish --access public
62
+ fi
63
+
64
+ # ── Done ──
65
+ echo ""
66
+ echo "Published $PACKAGE_NAME@$PACKAGE_VERSION ($TAG)"
67
+ echo "https://www.npmjs.com/package/$PACKAGE_NAME"
@@ -0,0 +1,6 @@
1
+ export function findRelatedElements(element: HTMLElement) {
2
+ const rel = element.getAttribute("data-kq-rel");
3
+ if (!rel) return null;
4
+
5
+ return document.querySelectorAll(`[data-kq-rel="${rel}"]`);
6
+ }
@@ -0,0 +1,9 @@
1
+ // src/globals.d.ts
2
+ declare global {
3
+ interface Window {
4
+ myCustomProperty: string;
5
+ initializeApp: () => void;
6
+ }
7
+ }
8
+
9
+ export {};