@liquidcommercedev/rmn-sdk 1.5.0-beta.2 → 1.5.0-beta.21

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 (38) hide show
  1. package/dist/index.cjs +2728 -953
  2. package/dist/index.esm.js +2729 -954
  3. package/dist/types/common/helpers/event-type.helper.d.ts +8 -0
  4. package/dist/types/common/helpers/extract-deep.helper.d.ts +28 -0
  5. package/dist/types/common/helpers/index.d.ts +5 -0
  6. package/dist/types/common/helpers/utils.helper.d.ts +35 -0
  7. package/dist/types/enums.d.ts +8 -1
  8. package/dist/types/modules/element/component/skeleton/index.d.ts +2 -0
  9. package/dist/types/modules/element/component/skeleton/skeleton.component.d.ts +3 -0
  10. package/dist/types/modules/element/component/skeleton/skeleton.interface.d.ts +13 -0
  11. package/dist/types/modules/element/component/skeleton/skeleton.template.d.ts +2 -0
  12. package/dist/types/modules/element/element.constant.d.ts +3 -0
  13. package/dist/types/modules/element/element.interface.d.ts +5 -2
  14. package/dist/types/modules/element/element.service.d.ts +11 -0
  15. package/dist/types/modules/element/template/helper.d.ts +2 -1
  16. package/dist/types/modules/event/event.interface.d.ts +11 -32
  17. package/dist/types/modules/event/event.service.d.ts +11 -27
  18. package/dist/types/modules/event/index.d.ts +0 -1
  19. package/dist/types/modules/{event/helpers → helper-service}/index.d.ts +1 -0
  20. package/dist/types/modules/{event/helpers → helper-service}/intersection.service.d.ts +1 -1
  21. package/dist/types/modules/helper-service/localstorage.service.d.ts +77 -0
  22. package/dist/types/modules/{event/pubsub.d.ts → helper-service/pubsub.service.d.ts} +8 -8
  23. package/dist/types/modules/{event/helpers → helper-service}/resize.service.d.ts +1 -1
  24. package/dist/types/modules/monitor/index.d.ts +2 -0
  25. package/dist/types/modules/monitor/monitor.enums.d.ts +4 -0
  26. package/dist/types/modules/monitor/monitor.interface.d.ts +16 -0
  27. package/dist/types/modules/monitor/monitor.service.d.ts +12 -0
  28. package/dist/types/modules/monitor/monitors/datalayer.monitor.d.ts +12 -0
  29. package/dist/types/modules/selection/selection.interface.d.ts +10 -2
  30. package/dist/types/modules/selection/selection.service.d.ts +1 -0
  31. package/dist/types/rmn-client.d.ts +17 -25
  32. package/dist/types/rmn-client.helper.d.ts +36 -0
  33. package/dist/types/types.d.ts +5 -4
  34. package/package.json +1 -1
  35. package/umd/liquidcommerce-rmn-sdk.min.js +1 -1
  36. package/dist/types/modules/element/component/utils.d.ts +0 -1
  37. package/dist/types/modules/event/helpers/localstorage.service.d.ts +0 -26
  38. /package/dist/types/{static.constant.d.ts → example.constant.d.ts} +0 -0
package/dist/index.esm.js CHANGED
@@ -1,6 +1,7 @@
1
1
  var RMN_SPOT_TYPE;
2
2
  (function (RMN_SPOT_TYPE) {
3
3
  // Reserve Bar Spot Types
4
+ RMN_SPOT_TYPE["RB_HOMEPAGE_HERO"] = "rbHomepageHero";
4
5
  RMN_SPOT_TYPE["RB_HOMEPAGE_HERO_THREE_TILE"] = "rbHomepageHeroThreeTile";
5
6
  RMN_SPOT_TYPE["RB_HOMEPAGE_HERO_TWO_TILE"] = "rbHomepageHeroTwoTile";
6
7
  RMN_SPOT_TYPE["RB_HOMEPAGE_HERO_FULL_IMAGE"] = "rbHomepageHeroFullImage";
@@ -52,14 +53,21 @@ var RMN_FILTER_PROPERTIES;
52
53
  RMN_FILTER_PROPERTIES["PUBLISHERS"] = "publishers";
53
54
  RMN_FILTER_PROPERTIES["SECTION"] = "section";
54
55
  })(RMN_FILTER_PROPERTIES || (RMN_FILTER_PROPERTIES = {}));
56
+ var RMN_EVENT;
57
+ (function (RMN_EVENT) {
58
+ RMN_EVENT["LIFECYCLE_STATE"] = "LIFECYCLE_STATE";
59
+ RMN_EVENT["SPOT_EVENT"] = "SPOT_EVENT";
60
+ })(RMN_EVENT || (RMN_EVENT = {}));
55
61
  var RMN_SPOT_EVENT;
56
62
  (function (RMN_SPOT_EVENT) {
57
- RMN_SPOT_EVENT["LIFECYCLE_STATE"] = "LIFECYCLE_STATE";
58
63
  RMN_SPOT_EVENT["IMPRESSION"] = "IMPRESSION";
59
64
  RMN_SPOT_EVENT["CLICK"] = "CLICK";
60
65
  RMN_SPOT_EVENT["PURCHASE"] = "PURCHASE";
61
66
  RMN_SPOT_EVENT["ADD_TO_CART"] = "ADD_TO_CART";
67
+ RMN_SPOT_EVENT["REMOVE_FROM_CART"] = "REMOVE_FROM_CART";
68
+ RMN_SPOT_EVENT["ADD_TO_CART_FROM_DETAILS"] = "ADD_TO_CART_FROM_DETAILS";
62
69
  RMN_SPOT_EVENT["ADD_TO_WISHLIST"] = "ADD_TO_WISHLIST";
70
+ RMN_SPOT_EVENT["EXPAND_PRODUCT"] = "EXPAND_PRODUCT";
63
71
  RMN_SPOT_EVENT["BUY_NOW"] = "BUY_NOW";
64
72
  })(RMN_SPOT_EVENT || (RMN_SPOT_EVENT = {}));
65
73
  var RMN_ENV;
@@ -6032,6 +6040,241 @@ axios.HttpStatusCode = HttpStatusCode;
6032
6040
 
6033
6041
  axios.default = axios;
6034
6042
 
6043
+ /**
6044
+ * Keyword patterns for each RMN_SPOT_EVENT.
6045
+ * Each event type has required keywords that must be present and optional ones.
6046
+ *
6047
+ * Note: The order of checks matters - more specific patterns must be checked before general ones
6048
+ * to avoid incorrect matches.
6049
+ */
6050
+ const EVENT_KEYWORDS = {
6051
+ [RMN_SPOT_EVENT.ADD_TO_CART_FROM_DETAILS]: {
6052
+ required: ['add', 'cart'],
6053
+ optional: ['details', 'detail', 'single', 'profile', 'page', 'pdp'],
6054
+ },
6055
+ [RMN_SPOT_EVENT.BUY_NOW]: {
6056
+ required: ['buy', 'now'],
6057
+ },
6058
+ [RMN_SPOT_EVENT.EXPAND_PRODUCT]: {
6059
+ required: ['product'],
6060
+ optional: ['details', 'expand', 'modal', 'popup', 'quickview', 'view'],
6061
+ },
6062
+ [RMN_SPOT_EVENT.ADD_TO_WISHLIST]: {
6063
+ required: ['add', 'wishlist'],
6064
+ },
6065
+ [RMN_SPOT_EVENT.REMOVE_FROM_CART]: {
6066
+ required: ['remove', 'cart'],
6067
+ },
6068
+ [RMN_SPOT_EVENT.PURCHASE]: {
6069
+ required: ['purchase'],
6070
+ },
6071
+ [RMN_SPOT_EVENT.ADD_TO_CART]: {
6072
+ required: ['add', 'cart'],
6073
+ },
6074
+ };
6075
+ /**
6076
+ * Normalizes an event name by converting to lowercase, removing special characters,
6077
+ * and splitting into words.
6078
+ */
6079
+ function normalizeEventName(event) {
6080
+ return event
6081
+ .toLowerCase()
6082
+ .replace(/[^a-z0-9\s]+/g, ' ') // Replace special chars with spaces
6083
+ .split(/\s+/) // Split on whitespace
6084
+ .filter(Boolean); // Remove empty strings
6085
+ }
6086
+ /**
6087
+ * Checks if a word matches a keyword, considering word boundaries.
6088
+ * This prevents partial word matches (e.g., "card" shouldn't match "cart").
6089
+ */
6090
+ function wordMatchesKeyword(word, keyword) {
6091
+ // Create a RegExp that matches the keyword as a whole word
6092
+ const keywordRegex = new RegExp(`^${keyword}s?$|${keyword}s?\\W|\\W${keyword}s?$|\\W${keyword}s?\\W`);
6093
+ return keywordRegex.test(` ${word} `); // Add spaces to handle word boundaries
6094
+ }
6095
+ /**
6096
+ * Checks if all required keywords and at least one optional keyword (if specified) are present.
6097
+ */
6098
+ function matchesKeywordPattern(words, required, optional) {
6099
+ // Check if all required keywords are present as whole words
6100
+ const hasAllRequired = required.every((keyword) => words.some((word) => wordMatchesKeyword(word, keyword)));
6101
+ if (!hasAllRequired) {
6102
+ return false;
6103
+ }
6104
+ // If no optional keywords are specified, return true
6105
+ if (!(optional === null || optional === void 0 ? void 0 : optional.length)) {
6106
+ return true;
6107
+ }
6108
+ // If optional keywords exist, check if at least one matches as a whole word
6109
+ return optional.some((keyword) => words.some((word) => wordMatchesKeyword(word, keyword)));
6110
+ }
6111
+ /**
6112
+ * Determines the event type from a raw event string by checking for keyword patterns.
6113
+ *
6114
+ * @param {string} [event] - The raw event string to evaluate
6115
+ * @returns {RMN_SPOT_EVENT | null} - The corresponding RMN_SPOT_EVENT or null if no match
6116
+ */
6117
+ function getEventTypeFromRawEvent(event) {
6118
+ if (!(event === null || event === void 0 ? void 0 : event.trim())) {
6119
+ return null;
6120
+ }
6121
+ const words = normalizeEventName(event);
6122
+ // Use Object.entries to maintain the exact order defined in EVENT_KEYWORDS
6123
+ for (const [eventType, { required, optional }] of Object.entries(EVENT_KEYWORDS)) {
6124
+ if (matchesKeywordPattern(words, required, optional)) {
6125
+ return eventType;
6126
+ }
6127
+ }
6128
+ return null;
6129
+ }
6130
+
6131
+ // Configuration object with target field names
6132
+ const extractorConfig = {
6133
+ ids: [
6134
+ // Ps: The function handles all the variations of keywords that end with "id" or "ids"
6135
+ // Universal product identifiers
6136
+ 'gtin',
6137
+ 'gtin8',
6138
+ 'gtin12',
6139
+ 'gtin13',
6140
+ 'gtin14',
6141
+ 'mpn',
6142
+ 'sku',
6143
+ 'upc',
6144
+ 'ean',
6145
+ 'isbn',
6146
+ 'isbn10',
6147
+ 'isbn13',
6148
+ 'asin',
6149
+ // Product codes and references
6150
+ 'coupon',
6151
+ 'barcode',
6152
+ 'product_code',
6153
+ 'part_number',
6154
+ 'model_number',
6155
+ 'item_variant',
6156
+ 'item_number',
6157
+ 'article_number',
6158
+ 'reference',
6159
+ ],
6160
+ price: [
6161
+ 'price',
6162
+ 'unitPrice',
6163
+ 'cost',
6164
+ 'current_price',
6165
+ 'sale_price',
6166
+ 'price_value',
6167
+ 'sale_price_value',
6168
+ 'regular_price',
6169
+ 'discount_price',
6170
+ 'unit_price',
6171
+ 'original_price',
6172
+ 'final_price',
6173
+ 'retail_price',
6174
+ ],
6175
+ };
6176
+ /**
6177
+ * Extracts deep values from an object based on specified target type
6178
+ * @param data - The source data object to extract values from
6179
+ * @param target - The type of values to extract ('ids' or 'price')
6180
+ * @param options - Optional configuration for the extraction process
6181
+ * @returns Array of extracted values or a single value if onlyFirst is true
6182
+ */
6183
+ function extractDeepValues(data, target, options = {}) {
6184
+ const {
6185
+ // eslint-disable-next-line @typescript-eslint/naming-convention
6186
+ onlyFirst = false, shouldIncludeZero = false, } = options;
6187
+ const values = [];
6188
+ const targetProperties = new Set(extractorConfig[target].map((name) => name.toLowerCase()));
6189
+ /**
6190
+ * Checks if a property name matches the target criteria
6191
+ */
6192
+ const isTargetField = (key) => {
6193
+ const normalizedKey = key.toLowerCase();
6194
+ const hasTarget = targetProperties.has(normalizedKey);
6195
+ if (target === 'ids') {
6196
+ return normalizedKey.endsWith('id') || normalizedKey.endsWith('ids') || hasTarget;
6197
+ }
6198
+ return hasTarget;
6199
+ };
6200
+ /**
6201
+ * Validates and normalizes extracted values
6202
+ */
6203
+ const validateValue = (value) => {
6204
+ if (typeof value === 'string') {
6205
+ return value.trim().length > 0;
6206
+ }
6207
+ if (typeof value === 'number') {
6208
+ return !isNaN(value) && (shouldIncludeZero || value !== 0);
6209
+ }
6210
+ return false;
6211
+ };
6212
+ /**
6213
+ * Processes a value and extracts matching fields
6214
+ */
6215
+ const processValue = (value, currentKey) => {
6216
+ // Early exit conditions
6217
+ if (value == null || (onlyFirst && values.length > 0))
6218
+ return;
6219
+ // Process current value if it matches target criteria
6220
+ if (currentKey && isTargetField(currentKey)) {
6221
+ if (Array.isArray(value)) {
6222
+ const validValues = value.filter(validateValue);
6223
+ values.push(...validValues);
6224
+ }
6225
+ else if (validateValue(value)) {
6226
+ values.push(value);
6227
+ }
6228
+ return;
6229
+ }
6230
+ // Recursive processing for nested structures
6231
+ if (Array.isArray(value)) {
6232
+ value.forEach((item) => processValue(item));
6233
+ }
6234
+ else if (typeof value === 'object') {
6235
+ Object.entries(value).forEach(([key, val]) => processValue(val, key));
6236
+ }
6237
+ };
6238
+ processValue(data);
6239
+ // Return based on options
6240
+ if (values.length === 0)
6241
+ return undefined;
6242
+ return onlyFirst ? values[0] : values;
6243
+ }
6244
+ /**
6245
+ * Cleans and normalizes an array of product IDs by:
6246
+ * 1. Converting all IDs to strings
6247
+ * 2. Trimming whitespace
6248
+ * 3. Removing leading zeros while preserving single "0"
6249
+ * 4. Filtering out empty/invalid values
6250
+ *
6251
+ * @param productIds - Array of product IDs that can be either strings or numbers
6252
+ * @returns Array of cleaned string IDs
6253
+ *
6254
+ * @example
6255
+ * cleanProductIds(["001", " 123 ", 456, "0"]) // ["1", "123", "456", "0"]
6256
+ */
6257
+ function cleanProductIds(productIds) {
6258
+ if (!Array.isArray(productIds)) {
6259
+ return [];
6260
+ }
6261
+ return productIds
6262
+ .map((id) => {
6263
+ // Guard against null/undefined
6264
+ if (id == null) {
6265
+ return '';
6266
+ }
6267
+ // Convert to string and trim
6268
+ const stringId = String(id).trim();
6269
+ if (!stringId) {
6270
+ return '';
6271
+ }
6272
+ // Remove leading zeros while preserving single "0"
6273
+ return stringId === '0' ? '0' : stringId.replace(/^0+/, '');
6274
+ })
6275
+ .filter((id) => id !== ''); // Remove empty strings
6276
+ }
6277
+
6035
6278
  class SingletonManager {
6036
6279
  /**
6037
6280
  * Retrieves an instance of the specified class using the provided instance creator function.
@@ -6197,126 +6440,406 @@ class ObjectHelper {
6197
6440
  }
6198
6441
  }
6199
6442
 
6200
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
6201
-
6202
- function getAugmentedNamespace(n) {
6203
- if (n.__esModule) return n;
6204
- var f = n.default;
6205
- if (typeof f == "function") {
6206
- var a = function a () {
6207
- if (this instanceof a) {
6208
- return Reflect.construct(f, arguments, this.constructor);
6209
- }
6210
- return f.apply(this, arguments);
6211
- };
6212
- a.prototype = f.prototype;
6213
- } else a = {};
6214
- Object.defineProperty(a, '__esModule', {value: true});
6215
- Object.keys(n).forEach(function (k) {
6216
- var d = Object.getOwnPropertyDescriptor(n, k);
6217
- Object.defineProperty(a, k, d.get ? d : {
6218
- enumerable: true,
6219
- get: function () {
6220
- return n[k];
6221
- }
6222
- });
6223
- });
6224
- return a;
6443
+ // Fallback method using fetch if sendBeacon isn't available
6444
+ async function fallbackEventFire(url) {
6445
+ try {
6446
+ const racePromise = Promise.race([
6447
+ // Promise #1: The fetch request
6448
+ fetch(url, {
6449
+ method: 'POST',
6450
+ keepalive: true,
6451
+ }),
6452
+ // Promise #2: The timeout
6453
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 2000)),
6454
+ ]);
6455
+ /**
6456
+ * Prevent requests from hanging indefinitely
6457
+ * Improve user experience by failing fast
6458
+ * Handle slow network conditions gracefully
6459
+ * Ensure resources are freed up in a timely manner
6460
+ */
6461
+ const response = await racePromise;
6462
+ return response.ok;
6463
+ }
6464
+ catch (_a) {
6465
+ return false;
6466
+ }
6225
6467
  }
6226
-
6227
- var cryptoJs = {exports: {}};
6228
-
6229
- function commonjsRequire(path) {
6230
- throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
6468
+ /**
6469
+ * Helper function to decode base64 string and parse JSON
6470
+ *
6471
+ * @param {string} base64String - The base64 encoded JSON string
6472
+ * @returns {T | null} - Decoded and parsed object or null if invalid
6473
+ */
6474
+ function decodeBase64Json(base64String) {
6475
+ try {
6476
+ return JSON.parse(atob(base64String));
6477
+ }
6478
+ catch (_a) {
6479
+ return null;
6480
+ }
6481
+ }
6482
+ /**
6483
+ * Extracts and decodes a URL from a base64-encoded query parameter.
6484
+ *
6485
+ * @param {string} url - The URL containing the base64-encoded query parameter.
6486
+ * @returns {string | null} - The decoded URL or null if not found or invalid.
6487
+ * @throws {Error} - If URL is malformed or payload is invalid.
6488
+ */
6489
+ function getRedirectUrlFromPayload(url) {
6490
+ if (!url)
6491
+ return null;
6492
+ try {
6493
+ // Extract initial payload
6494
+ const payload = new URL(url).searchParams.get('p');
6495
+ if (!payload)
6496
+ return null;
6497
+ // Decode first layer
6498
+ const decodedData = decodeBase64Json(payload);
6499
+ if (!(decodedData === null || decodedData === void 0 ? void 0 : decodedData.u))
6500
+ return null;
6501
+ // Extract URL from nested query
6502
+ const eventPayload = new URLSearchParams(decodedData.u).get('e');
6503
+ if (!eventPayload)
6504
+ return null;
6505
+ // Decode second layer
6506
+ const eventData = decodeBase64Json(eventPayload);
6507
+ return (eventData === null || eventData === void 0 ? void 0 : eventData.ur) || null;
6508
+ }
6509
+ catch (_a) {
6510
+ console.warn('RmnSdk: Failed to extract redirect URL from payload.');
6511
+ return null;
6512
+ }
6513
+ }
6514
+ /**
6515
+ * Fires an event using the navigator.sendBeacon method or a fallback method if sendBeacon is not available.
6516
+ * If the event is a click event and a redirect URL is found, it redirects the user to that URL.
6517
+ *
6518
+ * @param {IFireEventParams} params - The parameters for firing the event.
6519
+ * @param {RMN_SPOT_EVENT} params.event - The event type.
6520
+ * @param {string} params.eventUrl - The URL to which the event is sent.
6521
+ * @returns {Promise<void>} - A promise that resolves when the event is fired.
6522
+ */
6523
+ async function fireEvent({ event, eventUrl }) {
6524
+ var _a;
6525
+ try {
6526
+ const didFireEvent = ((_a = navigator === null || navigator === void 0 ? void 0 : navigator.sendBeacon) === null || _a === void 0 ? void 0 : _a.call(navigator, eventUrl)) || (await fallbackEventFire(eventUrl));
6527
+ if (!didFireEvent) {
6528
+ return;
6529
+ }
6530
+ if (event === RMN_SPOT_EVENT.CLICK) {
6531
+ const redirectUrl = getRedirectUrlFromPayload(eventUrl);
6532
+ if (redirectUrl) {
6533
+ window.location.href = redirectUrl;
6534
+ }
6535
+ }
6536
+ }
6537
+ catch (_b) {
6538
+ // Handle errors silently
6539
+ }
6540
+ }
6541
+ function calculateScaleFactor(elementScale) {
6542
+ // Step 1: Apply square root for non-linear scaling
6543
+ // This creates a more gradual scaling effect, especially for larger changes
6544
+ // For example:
6545
+ // - elementScale of 0.25 (1/4 size) becomes 0.5
6546
+ // - elementScale of 1 (unchanged) remains 1
6547
+ // - elementScale of 4 (4x size) becomes 2
6548
+ const baseFactor = Math.sqrt(elementScale);
6549
+ // Step 2: Apply additional dampening to further soften the scaling effect
6550
+ // The dampening factor (0.5) can be adjusted:
6551
+ // - Lower values (closer to 0) make scaling more subtle
6552
+ // - Higher values (closer to 1) make scaling more pronounced
6553
+ const dampening = 0.35;
6554
+ // Calculate the scaleFactor:
6555
+ // 1. (baseFactor - 1) represents the change from the original size
6556
+ // 2. Multiply by dampening to reduce the effect
6557
+ // 3. Add 1 to center the scaling around the original size
6558
+ // For example, if baseFactor is 2:
6559
+ // scaleFactor = 1 + (2 - 1) * 0.5 = 1.5
6560
+ const scaleFactor = 1 + (baseFactor - 1) * dampening;
6561
+ // Step 3: Define the allowed range for the scale factor
6562
+ // This ensures that the font size never changes too drastically
6563
+ const minScale = 0.35; // Font will never be smaller than 50% of original
6564
+ const maxScale = 1.5; // Font will never be larger than 150% of original
6565
+ // Step 4: Clamp the scale factor to the defined range
6566
+ // Math.min ensures the value doesn't exceed maxScale
6567
+ // Math.max ensures the value isn't less than minScale
6568
+ return Math.max(minScale, Math.min(maxScale, scaleFactor));
6569
+ }
6570
+ /**
6571
+ * Converts an object to a query string.
6572
+ *
6573
+ * @param {Record<string, string|number|undefined|null>} obj - The object to be converted to a query string.
6574
+ * @returns {string} - The query string.
6575
+ */
6576
+ function objectToQueryParams(obj) {
6577
+ return Object.entries(obj !== null && obj !== void 0 ? obj : {})
6578
+ .map(([key, value]) => {
6579
+ if (value == null) {
6580
+ return '';
6581
+ }
6582
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
6583
+ })
6584
+ .filter(Boolean)
6585
+ .join('&');
6231
6586
  }
6232
6587
 
6233
- var core = {exports: {}};
6234
-
6235
- var _polyfillNode_crypto = {};
6236
-
6237
- var _polyfillNode_crypto$1 = /*#__PURE__*/Object.freeze({
6238
- __proto__: null,
6239
- default: _polyfillNode_crypto
6240
- });
6241
-
6242
- var require$$0 = /*@__PURE__*/getAugmentedNamespace(_polyfillNode_crypto$1);
6243
-
6244
- var hasRequiredCore;
6245
-
6246
- function requireCore () {
6247
- if (hasRequiredCore) return core.exports;
6248
- hasRequiredCore = 1;
6249
- (function (module, exports) {
6250
- (function (root, factory) {
6251
- {
6252
- // CommonJS
6253
- module.exports = factory();
6254
- }
6255
- }(commonjsGlobal, function () {
6256
-
6257
- /*globals window, global, require*/
6258
-
6259
- /**
6260
- * CryptoJS core components.
6261
- */
6262
- var CryptoJS = CryptoJS || (function (Math, undefined$1) {
6263
-
6264
- var crypto;
6265
-
6266
- // Native crypto from window (Browser)
6267
- if (typeof window !== 'undefined' && window.crypto) {
6268
- crypto = window.crypto;
6269
- }
6270
-
6271
- // Native crypto in web worker (Browser)
6272
- if (typeof self !== 'undefined' && self.crypto) {
6273
- crypto = self.crypto;
6274
- }
6275
-
6276
- // Native crypto from worker
6277
- if (typeof globalThis !== 'undefined' && globalThis.crypto) {
6278
- crypto = globalThis.crypto;
6279
- }
6280
-
6281
- // Native (experimental IE 11) crypto from window (Browser)
6282
- if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
6283
- crypto = window.msCrypto;
6284
- }
6285
-
6286
- // Native crypto from global (NodeJS)
6287
- if (!crypto && typeof commonjsGlobal !== 'undefined' && commonjsGlobal.crypto) {
6288
- crypto = commonjsGlobal.crypto;
6289
- }
6290
-
6291
- // Native crypto import via require (NodeJS)
6292
- if (!crypto && typeof commonjsRequire === 'function') {
6293
- try {
6294
- crypto = require$$0;
6295
- } catch (err) {}
6296
- }
6297
-
6298
- /*
6299
- * Cryptographically secure pseudorandom number generator
6300
- *
6301
- * As Math.random() is cryptographically not safe to use
6302
- */
6303
- var cryptoSecureRandomInt = function () {
6304
- if (crypto) {
6305
- // Use getRandomValues method (Browser)
6306
- if (typeof crypto.getRandomValues === 'function') {
6307
- try {
6308
- return crypto.getRandomValues(new Uint32Array(1))[0];
6309
- } catch (err) {}
6310
- }
6311
-
6312
- // Use randomBytes method (NodeJS)
6313
- if (typeof crypto.randomBytes === 'function') {
6314
- try {
6315
- return crypto.randomBytes(4).readInt32LE();
6316
- } catch (err) {}
6317
- }
6318
- }
6319
-
6588
+ class UniqueIdGenerator {
6589
+ /**
6590
+ * Initialize the generator with a node ID
6591
+ * @param nodeId Number between 0-1023 to identify this instance
6592
+ */
6593
+ static initialize(nodeId = Math.floor(Math.random() * 1024)) {
6594
+ if (nodeId < 0 || nodeId >= 1 << this.nodeBits) {
6595
+ throw new Error(`Node ID must be between 0 and ${(1 << this.nodeBits) - 1}`);
6596
+ }
6597
+ this.nodeId = nodeId;
6598
+ }
6599
+ /**
6600
+ * Convert a number to base32 string with specified length
6601
+ */
6602
+ static toBase32(num, length) {
6603
+ let result = '';
6604
+ while (num > 0) {
6605
+ result = this.base32Chars[Number(num % BigInt(32))] + result;
6606
+ // @ts-expect-error - TS doesn't support bigint division
6607
+ num = num / 32n;
6608
+ }
6609
+ return result.padStart(length, '0');
6610
+ }
6611
+ /**
6612
+ * Generate a cryptographically secure random number
6613
+ */
6614
+ static getSecureRandom() {
6615
+ if (typeof crypto !== 'undefined') {
6616
+ const buffer = new Uint32Array(1);
6617
+ crypto.getRandomValues(buffer);
6618
+ return buffer[0];
6619
+ }
6620
+ return Math.floor(Math.random() * 0xffffffff);
6621
+ }
6622
+ /**
6623
+ * Wait until next millisecond
6624
+ */
6625
+ static waitNextMillis(lastTimestamp) {
6626
+ let timestamp = Date.now();
6627
+ while (timestamp <= lastTimestamp) {
6628
+ timestamp = Date.now();
6629
+ }
6630
+ return timestamp;
6631
+ }
6632
+ /**
6633
+ * Generates a highly unique ID with the following format:
6634
+ * TTTTTTTTTTCCCCNNNNNRRRR
6635
+ * T: Timestamp (10 chars)
6636
+ * C: Counter (4 chars)
6637
+ * N: Node ID (5 chars)
6638
+ * R: Random (4 chars)
6639
+ *
6640
+ * Total length: 23 characters, always uppercase alphanumeric
6641
+ */
6642
+ static generate() {
6643
+ if (this.nodeId === undefined) {
6644
+ this.initialize();
6645
+ }
6646
+ let timestamp = Date.now() - this.epoch;
6647
+ // Handle clock moving backwards or same millisecond
6648
+ if (timestamp < this.lastTimestamp) {
6649
+ throw new Error('Clock moved backwards. Refusing to generate ID.');
6650
+ }
6651
+ if (timestamp === this.lastTimestamp) {
6652
+ this.sequence = (this.sequence + 1) & ((1 << this.sequenceBits) - 1);
6653
+ if (this.sequence === 0) {
6654
+ timestamp = this.waitNextMillis(this.lastTimestamp);
6655
+ }
6656
+ }
6657
+ else {
6658
+ this.sequence = 0;
6659
+ }
6660
+ this.lastTimestamp = timestamp;
6661
+ // Generate random component
6662
+ const random = this.getSecureRandom() & 0xffff; // 16 bits of randomness
6663
+ // Combine all components into a BigInt
6664
+ // const id =
6665
+ // (BigInt(timestamp) << BigInt(this.nodeBits + this.sequenceBits + 16)) |
6666
+ // (BigInt(this.nodeId) << BigInt(this.sequenceBits + 16)) |
6667
+ // (BigInt(this.sequence) << BigInt(16)) |
6668
+ // BigInt(random);
6669
+ // Convert to base32 representation
6670
+ const timeComponent = this.toBase32(BigInt(timestamp), 10);
6671
+ const counterComponent = this.toBase32(BigInt(this.sequence), 4);
6672
+ const nodeComponent = this.toBase32(BigInt(this.nodeId), 5);
6673
+ const randomComponent = this.toBase32(BigInt(random), 4);
6674
+ return `${timeComponent}${counterComponent}${nodeComponent}${randomComponent}`;
6675
+ }
6676
+ /**
6677
+ * Validates if a string matches the expected ID format
6678
+ */
6679
+ static isValid(id) {
6680
+ if (!/^[0-9A-HJ-NP-Z]{23}$/.test(id))
6681
+ return false;
6682
+ try {
6683
+ const timeComponent = id.slice(0, 10);
6684
+ const timestamp = this.decodeBase32(timeComponent);
6685
+ const now = Date.now() - this.epoch;
6686
+ return timestamp >= 0 && timestamp <= now;
6687
+ }
6688
+ catch (_a) {
6689
+ return false;
6690
+ }
6691
+ }
6692
+ /**
6693
+ * Decode base32 string to number
6694
+ */
6695
+ static decodeBase32(str) {
6696
+ let result = 0;
6697
+ for (const char of str) {
6698
+ result = result * 32 + this.base32Chars.indexOf(char);
6699
+ }
6700
+ return result;
6701
+ }
6702
+ /**
6703
+ * Extract timestamp from ID
6704
+ */
6705
+ static getTimestamp(id) {
6706
+ if (!this.isValid(id))
6707
+ throw new Error('Invalid ID format');
6708
+ const timeComponent = id.slice(0, 10);
6709
+ const timestamp = this.decodeBase32(timeComponent);
6710
+ return new Date(timestamp + this.epoch);
6711
+ }
6712
+ }
6713
+ // Constants for bit manipulation
6714
+ UniqueIdGenerator.epoch = 1577836800000; // 2020-01-01 as epoch
6715
+ UniqueIdGenerator.nodeBits = 10;
6716
+ UniqueIdGenerator.sequenceBits = 12;
6717
+ // Instance variables
6718
+ UniqueIdGenerator.lastTimestamp = -1;
6719
+ UniqueIdGenerator.sequence = 0;
6720
+ // Character set for base32 encoding (excluding similar looking characters)
6721
+ UniqueIdGenerator.base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
6722
+
6723
+ var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
6724
+
6725
+ function getAugmentedNamespace(n) {
6726
+ if (n.__esModule) return n;
6727
+ var f = n.default;
6728
+ if (typeof f == "function") {
6729
+ var a = function a () {
6730
+ if (this instanceof a) {
6731
+ return Reflect.construct(f, arguments, this.constructor);
6732
+ }
6733
+ return f.apply(this, arguments);
6734
+ };
6735
+ a.prototype = f.prototype;
6736
+ } else a = {};
6737
+ Object.defineProperty(a, '__esModule', {value: true});
6738
+ Object.keys(n).forEach(function (k) {
6739
+ var d = Object.getOwnPropertyDescriptor(n, k);
6740
+ Object.defineProperty(a, k, d.get ? d : {
6741
+ enumerable: true,
6742
+ get: function () {
6743
+ return n[k];
6744
+ }
6745
+ });
6746
+ });
6747
+ return a;
6748
+ }
6749
+
6750
+ var cryptoJs = {exports: {}};
6751
+
6752
+ function commonjsRequire(path) {
6753
+ throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
6754
+ }
6755
+
6756
+ var core = {exports: {}};
6757
+
6758
+ var _polyfillNode_crypto = {};
6759
+
6760
+ var _polyfillNode_crypto$1 = /*#__PURE__*/Object.freeze({
6761
+ __proto__: null,
6762
+ default: _polyfillNode_crypto
6763
+ });
6764
+
6765
+ var require$$0 = /*@__PURE__*/getAugmentedNamespace(_polyfillNode_crypto$1);
6766
+
6767
+ var hasRequiredCore;
6768
+
6769
+ function requireCore () {
6770
+ if (hasRequiredCore) return core.exports;
6771
+ hasRequiredCore = 1;
6772
+ (function (module, exports) {
6773
+ (function (root, factory) {
6774
+ {
6775
+ // CommonJS
6776
+ module.exports = factory();
6777
+ }
6778
+ }(commonjsGlobal, function () {
6779
+
6780
+ /*globals window, global, require*/
6781
+
6782
+ /**
6783
+ * CryptoJS core components.
6784
+ */
6785
+ var CryptoJS = CryptoJS || (function (Math, undefined$1) {
6786
+
6787
+ var crypto;
6788
+
6789
+ // Native crypto from window (Browser)
6790
+ if (typeof window !== 'undefined' && window.crypto) {
6791
+ crypto = window.crypto;
6792
+ }
6793
+
6794
+ // Native crypto in web worker (Browser)
6795
+ if (typeof self !== 'undefined' && self.crypto) {
6796
+ crypto = self.crypto;
6797
+ }
6798
+
6799
+ // Native crypto from worker
6800
+ if (typeof globalThis !== 'undefined' && globalThis.crypto) {
6801
+ crypto = globalThis.crypto;
6802
+ }
6803
+
6804
+ // Native (experimental IE 11) crypto from window (Browser)
6805
+ if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
6806
+ crypto = window.msCrypto;
6807
+ }
6808
+
6809
+ // Native crypto from global (NodeJS)
6810
+ if (!crypto && typeof commonjsGlobal !== 'undefined' && commonjsGlobal.crypto) {
6811
+ crypto = commonjsGlobal.crypto;
6812
+ }
6813
+
6814
+ // Native crypto import via require (NodeJS)
6815
+ if (!crypto && typeof commonjsRequire === 'function') {
6816
+ try {
6817
+ crypto = require$$0;
6818
+ } catch (err) {}
6819
+ }
6820
+
6821
+ /*
6822
+ * Cryptographically secure pseudorandom number generator
6823
+ *
6824
+ * As Math.random() is cryptographically not safe to use
6825
+ */
6826
+ var cryptoSecureRandomInt = function () {
6827
+ if (crypto) {
6828
+ // Use getRandomValues method (Browser)
6829
+ if (typeof crypto.getRandomValues === 'function') {
6830
+ try {
6831
+ return crypto.getRandomValues(new Uint32Array(1))[0];
6832
+ } catch (err) {}
6833
+ }
6834
+
6835
+ // Use randomBytes method (NodeJS)
6836
+ if (typeof crypto.randomBytes === 'function') {
6837
+ try {
6838
+ return crypto.randomBytes(4).readInt32LE();
6839
+ } catch (err) {}
6840
+ }
6841
+ }
6842
+
6320
6843
  throw new Error('Native crypto module could not be used to get secure random number.');
6321
6844
  };
6322
6845
 
@@ -15152,6 +15675,7 @@ class AuthService extends BaseApi {
15152
15675
 
15153
15676
  const SPOT_ELEMENT_TAG = 'spot-element';
15154
15677
  const CAROUSEL_ELEMENT_TAG = 'spot-carousel-element';
15678
+ const SKELETON_ELEMENT_TAG = 'spot-skeleton-element';
15155
15679
  const GFONT_PRECONNECT = `
15156
15680
  <link rel="preconnect" href="https://fonts.googleapis.com">
15157
15681
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -15162,6 +15686,45 @@ const GFONT_SOURCE_SANS_3 = `
15162
15686
  const GFONT_CORMORANT = `
15163
15687
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,300..700;1,300..700&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap">
15164
15688
  `;
15689
+ const SPOT_DIMENSIONS = {
15690
+ [RMN_SPOT_TYPE.RB_HOMEPAGE_HERO]: { width: 1140, height: 640 },
15691
+ [RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE]: { width: 1140, height: 640 },
15692
+ [RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE]: { width: 1140, height: 640 },
15693
+ [RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE]: { width: 1140, height: 640 },
15694
+ [RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT]: { width: 468, height: 410 },
15695
+ [RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT]: { width: 224, height: 378 },
15696
+ [RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT]: { width: 224, height: 410 },
15697
+ [RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK]: { width: 887, height: 344 },
15698
+ [RMN_SPOT_TYPE.RB_PRODUCT_UPCS]: { width: 1, height: 1 },
15699
+ [RMN_SPOT_TYPE.RB_NAVIGATION_BANNER]: { width: 440, height: 220 },
15700
+ [RMN_SPOT_TYPE.SMALL_RECTANGLE]: { width: 180, height: 150 },
15701
+ [RMN_SPOT_TYPE.MEDIUM_RECTANGLE]: { width: 300, height: 250 },
15702
+ [RMN_SPOT_TYPE.LARGE_RECTANGLE]: { width: 336, height: 280 },
15703
+ [RMN_SPOT_TYPE.VERTICAL_RECTANGLE]: { width: 240, height: 400 },
15704
+ [RMN_SPOT_TYPE.BANNER]: { width: 468, height: 60 },
15705
+ [RMN_SPOT_TYPE.LEADERBOARD]: { width: 728, height: 90 },
15706
+ [RMN_SPOT_TYPE.LARGE_LEADERBOARD]: { width: 970, height: 90 },
15707
+ [RMN_SPOT_TYPE.BILLBOARD]: { width: 970, height: 250 },
15708
+ [RMN_SPOT_TYPE.SKYSCRAPER]: { width: 120, height: 600 },
15709
+ [RMN_SPOT_TYPE.WIDE_SKYSCRAPER]: { width: 160, height: 600 },
15710
+ [RMN_SPOT_TYPE.HALF_PAGE]: { width: 300, height: 600 },
15711
+ [RMN_SPOT_TYPE.SMALL_SQUARE]: { width: 200, height: 200 },
15712
+ [RMN_SPOT_TYPE.SQUARE]: { width: 250, height: 250 },
15713
+ [RMN_SPOT_TYPE.VERTICAL_BANNER]: { width: 120, height: 240 },
15714
+ [RMN_SPOT_TYPE.BUTTON_2]: { width: 120, height: 60 },
15715
+ [RMN_SPOT_TYPE.MICRO_BAR]: { width: 88, height: 31 },
15716
+ [RMN_SPOT_TYPE.POP_UP]: { width: 550, height: 480 },
15717
+ [RMN_SPOT_TYPE.PORTRAIT]: { width: 300, height: 1050 },
15718
+ [RMN_SPOT_TYPE.SMARTPHONE_BANNER_1]: { width: 300, height: 50 },
15719
+ [RMN_SPOT_TYPE.SMARTPHONE_BANNER_2]: { width: 320, height: 50 },
15720
+ [RMN_SPOT_TYPE.MOBILE_PHONE_INTERSTITIAL_1]: { width: 640, height: 1136 },
15721
+ [RMN_SPOT_TYPE.MOBILE_PHONE_INTERSTITIAL_2]: { width: 750, height: 1334 },
15722
+ [RMN_SPOT_TYPE.MOBILE_PHONE_INTERSTITIAL_3]: { width: 1080, height: 1920 },
15723
+ [RMN_SPOT_TYPE.FEATURE_PHONE_SMALL_BANNER]: { width: 120, height: 20 },
15724
+ [RMN_SPOT_TYPE.FEATURE_PHONE_MEDIUM_BANNER]: { width: 168, height: 28 },
15725
+ [RMN_SPOT_TYPE.FEATURE_PHONE_LARGE_BANNER]: { width: 216, height: 36 },
15726
+ [RMN_SPOT_TYPE.IN_TEXT]: { width: 1, height: 1 },
15727
+ };
15165
15728
 
15166
15729
  class IntersectionObserverService {
15167
15730
  constructor(defaultOptions = {}) {
@@ -15203,8 +15766,33 @@ class IntersectionObserverService {
15203
15766
  }
15204
15767
  }
15205
15768
 
15206
- class LocalStorage {
15769
+ var ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX;
15770
+ (function (ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX) {
15771
+ ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX["PLACEMENT_ID"] = 0] = "PLACEMENT_ID";
15772
+ ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX["SPOT_ID"] = 1] = "SPOT_ID";
15773
+ ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX["SPOT_TYPE"] = 2] = "SPOT_TYPE";
15774
+ ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX["EVENTS"] = 3] = "EVENTS";
15775
+ ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX["PRODUCT_IDS"] = 4] = "PRODUCT_IDS";
15776
+ ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX["CREATED_AT"] = 5] = "CREATED_AT";
15777
+ })(ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX || (ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX = {}));
15778
+ const LOCAL_STORAGE_SPOT_EVENTS_ARRAY_INDEX = {
15779
+ [RMN_SPOT_EVENT.IMPRESSION]: 0,
15780
+ [RMN_SPOT_EVENT.CLICK]: 1,
15781
+ [RMN_SPOT_EVENT.PURCHASE]: 2,
15782
+ [RMN_SPOT_EVENT.ADD_TO_CART]: 3,
15783
+ [RMN_SPOT_EVENT.REMOVE_FROM_CART]: 4,
15784
+ [RMN_SPOT_EVENT.ADD_TO_CART_FROM_DETAILS]: 5,
15785
+ [RMN_SPOT_EVENT.ADD_TO_WISHLIST]: 6,
15786
+ [RMN_SPOT_EVENT.EXPAND_PRODUCT]: 7,
15787
+ [RMN_SPOT_EVENT.BUY_NOW]: 8,
15788
+ };
15789
+ class LocalStorageService {
15207
15790
  constructor() {
15791
+ if (typeof window.localStorage === 'undefined') {
15792
+ console.warn('Local storage is not supported in this environment');
15793
+ return;
15794
+ }
15795
+ this.setUserId();
15208
15796
  this.spots = new Map();
15209
15797
  // Sync local storage with the current state
15210
15798
  this.syncLocalStorage();
@@ -15212,19 +15800,23 @@ class LocalStorage {
15212
15800
  this.removeExpiredSpots();
15213
15801
  }
15214
15802
  static getInstance() {
15215
- if (!LocalStorage.instance) {
15216
- LocalStorage.instance = new LocalStorage();
15803
+ if (!LocalStorageService.instance) {
15804
+ LocalStorageService.instance = new LocalStorageService();
15217
15805
  }
15218
- return LocalStorage.instance;
15806
+ return LocalStorageService.instance;
15219
15807
  }
15220
15808
  syncLocalStorage() {
15221
- const localStorageData = localStorage.getItem(LocalStorage.localStorageKey);
15222
- // TODO: Encrypt the data before storing it in the local storage
15809
+ const localStorageData = window.localStorage.getItem(LocalStorageService.localStorageKey);
15223
15810
  if (localStorageData) {
15224
15811
  try {
15225
- const parsedData = JSON.parse(localStorageData);
15812
+ const decryptedData = this.decryptData(localStorageData);
15813
+ const parsedData = JSON.parse(decryptedData);
15226
15814
  if (parsedData && typeof parsedData === 'object') {
15227
- this.spots = this.objToMap(parsedData);
15815
+ const data = {};
15816
+ for (const [key, value] of Object.entries(parsedData)) {
15817
+ data[key] = this.spotArrayToObject(value);
15818
+ }
15819
+ this.spots = this.objectToMap(data);
15228
15820
  }
15229
15821
  else {
15230
15822
  this.clearLocalStorage();
@@ -15237,43 +15829,237 @@ class LocalStorage {
15237
15829
  }
15238
15830
  }
15239
15831
  setSpot(spotId, data) {
15832
+ var _a;
15240
15833
  data.createdAt = Date.now();
15241
- this.spots.set(spotId, data);
15834
+ (_a = this.spots) === null || _a === void 0 ? void 0 : _a.set(spotId, data);
15242
15835
  this.updateLocalStorage();
15243
15836
  }
15244
- getSpot(spotId) {
15245
- return this.spots.get(spotId);
15246
- }
15247
15837
  removeSpot(spotId) {
15248
- this.spots.delete(spotId);
15838
+ var _a;
15839
+ (_a = this.spots) === null || _a === void 0 ? void 0 : _a.delete(spotId);
15249
15840
  this.updateLocalStorage();
15250
15841
  }
15842
+ getSpot(spotId) {
15843
+ var _a;
15844
+ return (_a = this.spots) === null || _a === void 0 ? void 0 : _a.get(spotId);
15845
+ }
15846
+ getSpots() {
15847
+ if (!this.spots)
15848
+ return undefined;
15849
+ return this.mapToObject(this.spots);
15850
+ }
15251
15851
  updateLocalStorage() {
15252
- const data = this.mapToObj(this.spots);
15253
- localStorage.setItem(LocalStorage.localStorageKey, JSON.stringify(data));
15852
+ if (!this.spots)
15853
+ return undefined;
15854
+ const data = this.mapToObject(this.spots);
15855
+ const dataArray = {};
15856
+ for (const [key, value] of Object.entries(data)) {
15857
+ dataArray[key] = this.spotObjectToArray(value);
15858
+ }
15859
+ try {
15860
+ const encryptedData = this.encryptData(JSON.stringify(dataArray));
15861
+ window.localStorage.setItem(LocalStorageService.localStorageKey, encryptedData);
15862
+ }
15863
+ catch (_a) {
15864
+ // If there is an error parsing the data, clear the local storage to prevent any issues
15865
+ this.clearLocalStorage();
15866
+ }
15254
15867
  }
15255
15868
  clearLocalStorage() {
15256
- localStorage.removeItem(LocalStorage.localStorageKey);
15869
+ window.localStorage.removeItem(LocalStorageService.localStorageKey);
15257
15870
  }
15258
15871
  removeExpiredSpots() {
15872
+ var _a;
15259
15873
  const currentTime = Date.now();
15260
- this.spots.forEach((spot, spotId) => {
15261
- var _a;
15262
- if (currentTime - ((_a = spot.createdAt) !== null && _a !== void 0 ? _a : 0) > LocalStorage.spotExpirationTime) {
15263
- this.spots.delete(spotId);
15874
+ (_a = this.spots) === null || _a === void 0 ? void 0 : _a.forEach((spot, spotId) => {
15875
+ var _a, _b;
15876
+ if (currentTime - ((_a = spot.createdAt) !== null && _a !== void 0 ? _a : 0) > LocalStorageService.spotExpirationTime) {
15877
+ (_b = this.spots) === null || _b === void 0 ? void 0 : _b.delete(spotId);
15264
15878
  }
15265
15879
  });
15266
15880
  this.updateLocalStorage();
15267
15881
  }
15268
- mapToObj(map) {
15882
+ // ======================== Utility functions ======================== //
15883
+ getUserId() {
15884
+ const key = LocalStorageService.localStorageKey;
15885
+ if (!key) {
15886
+ this.setUserId();
15887
+ }
15888
+ return key.replace(`${LocalStorageService.localStorageKeyPrefix}_`, '');
15889
+ }
15890
+ /**
15891
+ * Sets the user ID in the local storage.
15892
+ * If no existing key is found,
15893
+ * it generates a new unique ID and sets it as the local storage key.
15894
+ */
15895
+ setUserId() {
15896
+ const prefix = LocalStorageService.localStorageKeyPrefix;
15897
+ const existingKeys = Object.keys(window.localStorage).filter((key) => key.startsWith(prefix));
15898
+ const setNewKey = () => {
15899
+ const uniqueId = UniqueIdGenerator.generate();
15900
+ const newLocalStorageKey = `${prefix}_${uniqueId}`;
15901
+ window.localStorage.setItem(newLocalStorageKey, '');
15902
+ LocalStorageService.localStorageKey = newLocalStorageKey;
15903
+ };
15904
+ if (existingKeys.length === 0) {
15905
+ setNewKey();
15906
+ }
15907
+ else {
15908
+ const validKey = existingKeys.find((key) => UniqueIdGenerator.isValid(key.replace(`${prefix}_`, '')));
15909
+ // Delete all other keys except the valid one
15910
+ existingKeys.forEach((key) => {
15911
+ if (key !== validKey) {
15912
+ window.localStorage.removeItem(key);
15913
+ }
15914
+ });
15915
+ if (validKey) {
15916
+ // Found a valid key, assign it to localStorageKey
15917
+ LocalStorageService.localStorageKey = validKey;
15918
+ }
15919
+ else {
15920
+ // No valid key found, generate a new key
15921
+ setNewKey();
15922
+ }
15923
+ }
15924
+ }
15925
+ mapToObject(map) {
15269
15926
  return Object.fromEntries(map);
15270
15927
  }
15271
- objToMap(obj) {
15928
+ objectToMap(obj) {
15272
15929
  return new Map(Object.entries(obj));
15273
15930
  }
15931
+ spotEventObjectToArray(events) {
15932
+ return Object.keys(LOCAL_STORAGE_SPOT_EVENTS_ARRAY_INDEX).map((type) => {
15933
+ const result = events.find((item) => item.event === type);
15934
+ return result && result.event === type ? result.url : '';
15935
+ });
15936
+ }
15937
+ spotEventArrayToObject(arr) {
15938
+ return Object.keys(LOCAL_STORAGE_SPOT_EVENTS_ARRAY_INDEX).map((type) => ({
15939
+ event: type,
15940
+ url: arr[LOCAL_STORAGE_SPOT_EVENTS_ARRAY_INDEX[type]],
15941
+ }));
15942
+ }
15943
+ spotObjectToArray(obj) {
15944
+ return [
15945
+ obj.placementId,
15946
+ obj.spotId,
15947
+ obj.spotType,
15948
+ this.spotEventObjectToArray(obj.events),
15949
+ obj.productIds,
15950
+ obj.createdAt,
15951
+ ];
15952
+ }
15953
+ spotArrayToObject(arr) {
15954
+ return {
15955
+ placementId: arr[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX.PLACEMENT_ID],
15956
+ spotId: arr[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX.SPOT_ID],
15957
+ spotType: arr[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX.SPOT_TYPE],
15958
+ events: this.spotEventArrayToObject(arr[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX.EVENTS]),
15959
+ productIds: arr[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX.PRODUCT_IDS],
15960
+ createdAt: arr[ENUM_LOCAL_STORAGE_SPOT_ARRAY_INDEX.CREATED_AT],
15961
+ };
15962
+ }
15963
+ encryptData(data) {
15964
+ if (!LocalStorageService.encryptData)
15965
+ return data;
15966
+ // For now, we are using base64 encoding to encrypt the data
15967
+ // Later we will use Jose encryption
15968
+ return btoa(data);
15969
+ }
15970
+ decryptData(data) {
15971
+ if (!LocalStorageService.encryptData)
15972
+ return data;
15973
+ // For now, we are using base64 encoding to encrypt
15974
+ // Later we will use Jose encryption
15975
+ return atob(data);
15976
+ }
15977
+ }
15978
+ LocalStorageService.localStorageKeyPrefix = 'lc_rmn';
15979
+ LocalStorageService.localStorageKey = '';
15980
+ LocalStorageService.spotExpirationTime = 1000 * 60 * 60 * 24 * 7; // 7 days
15981
+ LocalStorageService.encryptData = true;
15982
+
15983
+ /**
15984
+ * PubsubService class
15985
+ * Manages event subscriptions and publications
15986
+ * @template IRmnEventMap A record type defining the structure of events and their data
15987
+ */
15988
+ class PubsubService {
15989
+ constructor() {
15990
+ /**
15991
+ * Object to store subscribers for each event type
15992
+ */
15993
+ this.subscribers = {};
15994
+ }
15995
+ static getInstance() {
15996
+ if (!PubsubService.instance) {
15997
+ PubsubService.instance = new PubsubService();
15998
+ }
15999
+ return PubsubService.instance;
16000
+ }
16001
+ /**
16002
+ * Subscribe to an event
16003
+ * @param eventType - The type of event to subscribe to
16004
+ * @param callback - The function to be called when the event is published
16005
+ * @returns A function to unsubscribe from the event
16006
+ *
16007
+ * @Example:
16008
+ * const unsubscribe = pubSub.subscribe('userLogin', (data) => {
16009
+ * console.log(`User ${data.username} logged in`);
16010
+ * });
16011
+ */
16012
+ subscribe(eventType, callback) {
16013
+ if (!this.subscribers[eventType]) {
16014
+ this.subscribers[eventType] = [];
16015
+ }
16016
+ this.subscribers[eventType].push(callback);
16017
+ // Return an unsubscribe function
16018
+ return () => {
16019
+ this.subscribers[eventType] = this.subscribers[eventType].filter((cb) => cb !== callback);
16020
+ };
16021
+ }
16022
+ /**
16023
+ * Publish an event
16024
+ * @param eventType - The type of event to publish
16025
+ * @param data - The data to be passed to the event subscribers
16026
+ *
16027
+ * @Example:
16028
+ * pubSub.publish('userLogin', { username: 'john_doe', timestamp: Date.now() });
16029
+ */
16030
+ publish(eventType, data) {
16031
+ if (!this.subscribers[eventType]) {
16032
+ return;
16033
+ }
16034
+ this.subscribers[eventType].forEach((callback) => callback(data));
16035
+ }
15274
16036
  }
15275
- LocalStorage.localStorageKey = 'lc_rmn';
15276
- LocalStorage.spotExpirationTime = 1000 * 60 * 60 * 24 * 7; // 7 days
16037
+ /**
16038
+ * Usage Example:
16039
+ *
16040
+ * interface IRmnEventMap {
16041
+ * userLogin: { username: string; timestamp: number };
16042
+ * pageView: { url: string; timestamp: number };
16043
+ * }
16044
+ *
16045
+ * const pubSub = new PubsubService<IRmnEventMap>();
16046
+ *
16047
+ * // Subscribe to events
16048
+ * const unsubscribeLogin = pubSub.subscribe('userLogin', (data) => {
16049
+ * console.log(`User ${data.username} logged in at ${new Date(data.timestamp)}`);
16050
+ * });
16051
+ *
16052
+ * pubSub.subscribe('pageView', (data) => {
16053
+ * console.log(`Page ${data.url} viewed at ${new Date(data.timestamp)}`);
16054
+ * });
16055
+ *
16056
+ * // Publish events
16057
+ * pubSub.publish('userLogin', { username: 'john_doe', timestamp: Date.now() });
16058
+ * pubSub.publish('pageView', { url: '/home', timestamp: Date.now() });
16059
+ *
16060
+ * // Unsubscribe from an event
16061
+ * unsubscribeLogin();
16062
+ */
15277
16063
 
15278
16064
  class ResizeObserverService {
15279
16065
  constructor({ element, maxSize, minScale }) {
@@ -15343,36 +16129,6 @@ class ResizeObserverService {
15343
16129
  }
15344
16130
  }
15345
16131
 
15346
- function calculateScaleFactor(elementScale) {
15347
- // Step 1: Apply square root for non-linear scaling
15348
- // This creates a more gradual scaling effect, especially for larger changes
15349
- // For example:
15350
- // - elementScale of 0.25 (1/4 size) becomes 0.5
15351
- // - elementScale of 1 (unchanged) remains 1
15352
- // - elementScale of 4 (4x size) becomes 2
15353
- const baseFactor = Math.sqrt(elementScale);
15354
- // Step 2: Apply additional dampening to further soften the scaling effect
15355
- // The dampening factor (0.5) can be adjusted:
15356
- // - Lower values (closer to 0) make scaling more subtle
15357
- // - Higher values (closer to 1) make scaling more pronounced
15358
- const dampening = 0.35;
15359
- // Calculate the scaleFactor:
15360
- // 1. (baseFactor - 1) represents the change from the original size
15361
- // 2. Multiply by dampening to reduce the effect
15362
- // 3. Add 1 to center the scaling around the original size
15363
- // For example, if baseFactor is 2:
15364
- // scaleFactor = 1 + (2 - 1) * 0.5 = 1.5
15365
- const scaleFactor = 1 + (baseFactor - 1) * dampening;
15366
- // Step 3: Define the allowed range for the scale factor
15367
- // This ensures that the font size never changes too drastically
15368
- const minScale = 0.35; // Font will never be smaller than 50% of original
15369
- const maxScale = 1.5; // Font will never be larger than 150% of original
15370
- // Step 4: Clamp the scale factor to the defined range
15371
- // Math.min ensures the value doesn't exceed maxScale
15372
- // Math.max ensures the value isn't less than minScale
15373
- return Math.max(minScale, Math.min(maxScale, scaleFactor));
15374
- }
15375
-
15376
16132
  const CAROUSEL_COMPONENT_STYLE = ({ width, height, fluid }) => `
15377
16133
  :host {
15378
16134
  position: relative;
@@ -15383,15 +16139,23 @@ const CAROUSEL_COMPONENT_STYLE = ({ width, height, fluid }) => `
15383
16139
  height: ${fluid ? '100%' : `${height}px`};
15384
16140
  }
15385
16141
 
16142
+ .container {
16143
+ position: relative;
16144
+ width: 100%;
16145
+ height: 100%;
16146
+ }
16147
+
15386
16148
  .slides {
15387
16149
  position: relative;
15388
16150
  height: 100%;
15389
16151
  width: 100%;
16152
+ display: flex;
16153
+ transition: transform 0.5s ease-in-out;
15390
16154
  }
15391
16155
 
15392
16156
  .slide {
15393
- display: none;
15394
-
16157
+ flex: 0 0 100%;
16158
+ display: flex;
15395
16159
  justify-content: center;
15396
16160
  align-items: center;
15397
16161
  height: 100%;
@@ -15576,57 +16340,74 @@ const CAROUSEL_COMPONENT_STYLE = ({ width, height, fluid }) => `
15576
16340
  let CarouselElement;
15577
16341
  if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined') {
15578
16342
  class CustomCarouselElement extends HTMLElement {
16343
+ /**
16344
+ * Initializes the web component and attaches shadow DOM
16345
+ * Binds all event handlers to maintain proper 'this' context
16346
+ */
15579
16347
  constructor() {
15580
16348
  super();
15581
- this.currentSlide = 0;
15582
- this.dotElements = [];
15583
- this.prevButton = null;
15584
- this.nextButton = null;
15585
- this.autoplayInterval = null;
15586
- this.useDots = false;
15587
- this.useButtons = false;
16349
+ this.state = {
16350
+ currentSlide: 0,
16351
+ autoplayInterval: null,
16352
+ isAutoplayPaused: false,
16353
+ useDots: false,
16354
+ useButtons: false,
16355
+ autoplay: true,
16356
+ interval: CustomCarouselElement.defaultConfigs.interval,
16357
+ dots: { ...CustomCarouselElement.defaultConfigs.dots },
16358
+ buttons: { ...CustomCarouselElement.defaultConfigs.buttons },
16359
+ isDragging: false,
16360
+ startX: 0,
16361
+ currentX: 0,
16362
+ dragStartTime: 0,
16363
+ dragDistance: 0,
16364
+ containerWidth: 0,
16365
+ isTransitioning: false,
16366
+ realIndex: 0, // Track the actual slide index
16367
+ virtualIndex: 1, // Track the virtual slide position (start at 1 to show clone)
16368
+ isVirtualizing: false, // Flag for handling virtual slide transitions
16369
+ };
16370
+ this.elements = {
16371
+ slidesContainer: null,
16372
+ dots: [],
16373
+ prevButton: null,
16374
+ nextButton: null,
16375
+ };
15588
16376
  this.originalFontSizes = new Map();
16377
+ this.cloneToOriginalMap = new WeakMap();
15589
16378
  this.attachShadow({ mode: 'open' });
16379
+ this.handleTransitionEnd = this.handleTransitionEnd.bind(this);
16380
+ this.handleTouchStart = this.handleTouchStart.bind(this);
16381
+ this.handleTouchMove = this.handleTouchMove.bind(this);
16382
+ this.handleTouchEnd = this.handleTouchEnd.bind(this);
16383
+ this.handleDragStart = this.handleDragStart.bind(this);
16384
+ this.handleDrag = this.handleDrag.bind(this);
16385
+ this.handleDragEnd = this.handleDragEnd.bind(this);
16386
+ this.updateContainerWidth = this.updateContainerWidth.bind(this);
15590
16387
  }
16388
+ /**
16389
+ * Web component lifecycle method called when element is added to DOM
16390
+ * Initializes state, renders UI, sets up events and starts autoplay
16391
+ */
15591
16392
  connectedCallback() {
15592
- this.initializeOptions();
15593
- this.setupResizeObserver();
16393
+ this.initializeState();
15594
16394
  this.render();
15595
16395
  this.setupCarousel();
16396
+ this.setupTouchEvents();
16397
+ this.startAutoplayIfEnabled();
16398
+ this.updateContainerWidth();
16399
+ window.addEventListener('resize', this.updateContainerWidth.bind(this));
16400
+ this.setupResizeObserver();
15596
16401
  }
16402
+ /**
16403
+ * Web component lifecycle method called when element is removed from DOM
16404
+ * Cleans up all event listeners and intervals
16405
+ */
15597
16406
  disconnectedCallback() {
15598
16407
  var _a;
15599
- this.stopAutoplay();
16408
+ this.cleanupEventListeners();
15600
16409
  (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
15601
16410
  }
15602
- initializeOptions() {
15603
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
15604
- this.useDots = ((_a = this.data) === null || _a === void 0 ? void 0 : _a.useDots) === true || typeof ((_b = this.data) === null || _b === void 0 ? void 0 : _b.useDots) === 'object';
15605
- this.useButtons = ((_c = this.data) === null || _c === void 0 ? void 0 : _c.useButtons) === true || typeof ((_d = this.data) === null || _d === void 0 ? void 0 : _d.useButtons) === 'object';
15606
- this.autoplay = (_f = (_e = this.data) === null || _e === void 0 ? void 0 : _e.autoplay) !== null && _f !== void 0 ? _f : true;
15607
- this.interval = (_h = (_g = this.data) === null || _g === void 0 ? void 0 : _g.interval) !== null && _h !== void 0 ? _h : CustomCarouselElement.defaultInterval;
15608
- this.dotsOptions = {
15609
- position: 'bottom-center',
15610
- color: '#d9d9d9',
15611
- activeColor: '#b5914a',
15612
- size: 'base',
15613
- opacity: 1,
15614
- ...(typeof ((_j = this.data) === null || _j === void 0 ? void 0 : _j.useDots) === 'object' ? this.data.useDots : {}),
15615
- };
15616
- this.buttonsOptions = {
15617
- together: false,
15618
- position: 'middle-sides',
15619
- textColor: '#000000',
15620
- backgroundColor: '#ffffff',
15621
- borderRadius: '50%',
15622
- prev: 'Prev',
15623
- next: 'Next',
15624
- size: 'base',
15625
- opacity: 1,
15626
- ...(typeof ((_k = this.data) === null || _k === void 0 ? void 0 : _k.useButtons) === 'object' ? this.data.useButtons : {}),
15627
- };
15628
- this.validateOptions();
15629
- }
15630
16411
  setupResizeObserver() {
15631
16412
  if (this.data && !this.data.fluid) {
15632
16413
  this.resizeObserver = new ResizeObserverService({
@@ -15641,18 +16422,20 @@ if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined
15641
16422
  }
15642
16423
  }
15643
16424
  handleCarouselSizeChanged(event) {
15644
- const isRBSpot = 'rbHeroadaksda'.startsWith('rb');
15645
- if (!isRBSpot) {
15646
- // Adjust text elements font size based on the scale factor
15647
- this.adjustFontSize(event.detail.scale);
15648
- }
16425
+ // Adjust text elements font size based on the scale factor
16426
+ this.adjustFontSize(event.detail.scale);
15649
16427
  }
15650
16428
  adjustFontSize(elementScale) {
15651
16429
  var _a;
15652
- const scaleFactor = calculateScaleFactor(elementScale);
15653
16430
  // Find all text elements within the shadow root
15654
- const elements = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('h1, h2, h3, h4, p, span');
15655
- elements === null || elements === void 0 ? void 0 : elements.forEach((element) => {
16431
+ const selectors = ['h1', 'h2', 'h3', 'h4', 'p', 'span']
16432
+ .map((tag) => `[data-spot="iab"] ${tag}`)
16433
+ .join(', ');
16434
+ const elements = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll(selectors);
16435
+ if (!elements)
16436
+ return;
16437
+ const scaleFactor = calculateScaleFactor(elementScale);
16438
+ elements.forEach((element) => {
15656
16439
  if (element instanceof HTMLElement) {
15657
16440
  if (!this.originalFontSizes.has(element)) {
15658
16441
  const originalSize = parseFloat(window.getComputedStyle(element).fontSize);
@@ -15664,180 +16447,610 @@ if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined
15664
16447
  }
15665
16448
  });
15666
16449
  }
16450
+ /** State and Configuration Management */
16451
+ /**
16452
+ * Initializes all state values from provided configuration
16453
+ * Sets up dots, buttons, autoplay and other feature flags
16454
+ */
16455
+ initializeState() {
16456
+ var _a, _b, _c, _d, _e, _f, _g, _h;
16457
+ this.state.useDots = Boolean((_a = this.data) === null || _a === void 0 ? void 0 : _a.useDots);
16458
+ this.state.useButtons = Boolean((_b = this.data) === null || _b === void 0 ? void 0 : _b.useButtons);
16459
+ this.state.autoplay = (_d = (_c = this.data) === null || _c === void 0 ? void 0 : _c.autoplay) !== null && _d !== void 0 ? _d : true;
16460
+ this.state.interval = (_f = (_e = this.data) === null || _e === void 0 ? void 0 : _e.interval) !== null && _f !== void 0 ? _f : CustomCarouselElement.defaultConfigs.interval;
16461
+ if (typeof ((_g = this.data) === null || _g === void 0 ? void 0 : _g.useDots) === 'object') {
16462
+ this.state.dots = { ...this.state.dots, ...this.data.useDots };
16463
+ }
16464
+ if (typeof ((_h = this.data) === null || _h === void 0 ? void 0 : _h.useButtons) === 'object') {
16465
+ this.state.buttons = { ...this.state.buttons, ...this.data.useButtons };
16466
+ }
16467
+ this.validateConfiguration();
16468
+ }
16469
+ /**
16470
+ * Validates all configuration options
16471
+ * Ensures positions and other settings are valid
16472
+ */
16473
+ validateConfiguration() {
16474
+ this.validatePosition(this.state.dots.position, 'dotsPosition', 'bottom-center');
16475
+ this.validateButtonsPosition();
16476
+ }
16477
+ /**
16478
+ * Validates navigation element positions
16479
+ * Ensures dots and buttons are placed in valid locations
16480
+ */
16481
+ validatePosition(position, optionName, defaultValue) {
16482
+ if (!CustomCarouselElement.validPositions.has(position)) {
16483
+ console.warn(`Invalid ${optionName}: ${position}. Defaulting to '${defaultValue}'.`);
16484
+ if (optionName === 'dotsPosition') {
16485
+ this.state.dots.position = defaultValue;
16486
+ }
16487
+ else if (optionName === 'buttonsPosition') {
16488
+ this.state.buttons.position = defaultValue;
16489
+ }
16490
+ }
16491
+ }
16492
+ /** Rendering and DOM Management */
16493
+ /**
16494
+ * Main rendering function that builds the carousel structure
16495
+ * Creates slides, dots, and navigation buttons
16496
+ */
15667
16497
  render() {
15668
- var _a;
15669
16498
  if (!this.shadowRoot)
15670
16499
  return;
15671
16500
  const style = document.createElement('style');
15672
16501
  style.textContent = CAROUSEL_COMPONENT_STYLE(this.data);
15673
16502
  this.shadowRoot.appendChild(style);
15674
- const slides = this.renderSlides();
15675
- this.shadowRoot.appendChild(slides);
15676
- this.slidesContainer = (_a = this.shadowRoot.querySelector('.slides')) !== null && _a !== void 0 ? _a : undefined;
15677
- if (this.useDots) {
15678
- const dots = this.renderDots();
15679
- if (dots)
15680
- this.shadowRoot.appendChild(dots);
15681
- }
15682
- if (this.useButtons) {
15683
- const buttons = this.renderButtons();
15684
- if (buttons)
15685
- this.shadowRoot.appendChild(buttons);
16503
+ const carouselContainer = document.createElement('div');
16504
+ carouselContainer.className = 'container';
16505
+ const slides = this.createSlides();
16506
+ carouselContainer.appendChild(slides);
16507
+ if (this.state.useDots) {
16508
+ const dots = this.createDots();
16509
+ carouselContainer.appendChild(dots);
15686
16510
  }
15687
- }
15688
- setupCarousel() {
15689
- this.setupDots();
15690
- this.setupButtons();
15691
- if (this.autoplay) {
15692
- this.startAutoplay();
16511
+ if (this.state.useButtons) {
16512
+ const buttons = this.createButtons();
16513
+ carouselContainer.appendChild(buttons);
15693
16514
  }
15694
- this.updateCarousel();
16515
+ this.shadowRoot.appendChild(carouselContainer);
16516
+ this.cacheElements();
15695
16517
  }
15696
- renderSlides() {
16518
+ /**
16519
+ * Creates the slides container with infinite scroll support
16520
+ * Adds cloned slides at start and end for seamless scrolling
16521
+ */
16522
+ createSlides() {
15697
16523
  const slidesContainer = document.createElement('div');
15698
16524
  slidesContainer.className = 'slides';
15699
- this.slides.forEach((slide, index) => {
15700
- const slideElement = document.createElement('div');
15701
- slideElement.className = `slide ${index === this.currentSlide ? 'active' : ''}`;
15702
- if (slide instanceof HTMLElement) {
15703
- slideElement.appendChild(slide);
15704
- }
15705
- slidesContainer.appendChild(slideElement);
16525
+ // Add the last slide at the beginning for seamless backward transition
16526
+ this.appendSlide(slidesContainer, this.slides.length - 1);
16527
+ // Add all slides
16528
+ this.slides.forEach((_, index) => {
16529
+ this.appendSlide(slidesContainer, index);
15706
16530
  });
16531
+ // Add the first slide at the end for seamless forward transition
16532
+ this.appendSlide(slidesContainer, 0);
16533
+ // Set initial position to show first real slide
16534
+ slidesContainer.style.transform = 'translateX(-100%)';
15707
16535
  return slidesContainer;
15708
16536
  }
15709
- renderDots() {
16537
+ /**
16538
+ * Appends a single slide to the container
16539
+ * Handles cloning of slide elements
16540
+ */
16541
+ appendSlide(container, index) {
16542
+ const slideElement = document.createElement('div');
16543
+ slideElement.className = 'slide';
16544
+ if (this.slides[index] instanceof HTMLElement) {
16545
+ // Clone the slide
16546
+ const clonedSlide = this.slides[index].cloneNode(true);
16547
+ // Map clone to original for event handling
16548
+ this.cloneToOriginalMap.set(clonedSlide, this.slides[index]);
16549
+ // Add event delegation to the slide container
16550
+ slideElement.addEventListener('click', (e) => {
16551
+ const original = this.slides[index];
16552
+ // Dispatch a new event on the original element
16553
+ const cloneEvent = new Event(e.type, { bubbles: true, cancelable: true });
16554
+ original.dispatchEvent(cloneEvent);
16555
+ });
16556
+ slideElement.appendChild(clonedSlide);
16557
+ }
16558
+ container.appendChild(slideElement);
16559
+ }
16560
+ /**
16561
+ * Creates navigation dots based on number of slides
16562
+ * Sets up initial active state and styling
16563
+ */
16564
+ createDots() {
16565
+ const { position, size, opacity, color } = this.state.dots;
15710
16566
  const dotsContainer = document.createElement('div');
15711
- dotsContainer.className = `dots ${this.dotsOptions.position} ${this.dotsOptions.size}`;
15712
- dotsContainer.style.cssText = `--opacity: ${this.dotsOptions.opacity}`;
16567
+ dotsContainer.className = `dots ${position} ${size}`;
16568
+ dotsContainer.style.setProperty('--opacity', opacity.toString());
15713
16569
  this.slides.forEach((_, index) => {
15714
16570
  const dot = document.createElement('span');
15715
- dot.className = `dot ${index === this.currentSlide ? 'active' : ''}`;
15716
- dot.style.backgroundColor = this.dotsOptions.color;
16571
+ dot.className = `dot ${index === this.state.currentSlide ? 'active' : ''}`;
16572
+ dot.style.backgroundColor = color;
15717
16573
  dotsContainer.appendChild(dot);
15718
16574
  });
15719
16575
  return dotsContainer;
15720
16576
  }
15721
- renderButtons() {
16577
+ /**
16578
+ * Creates navigation buttons (prev/next)
16579
+ * Applies styling and position configuration
16580
+ */
16581
+ createButtons() {
16582
+ // eslint-disable-next-line @typescript-eslint/naming-convention
16583
+ const { together, position, size, opacity } = this.state.buttons;
15722
16584
  const buttonsContainer = document.createElement('div');
15723
- const buttonsClass = this.buttonsOptions.together
15724
- ? `buttons-together ${this.buttonsOptions.position}`
15725
- : 'buttons-separate';
15726
- buttonsContainer.className = `buttons ${buttonsClass} ${this.buttonsOptions.size}`;
15727
- buttonsContainer.style.cssText = `--opacity: ${this.buttonsOptions.opacity}`;
15728
- this.prevButton = this.createButton('prev-button', this.buttonsOptions.prev);
15729
- this.nextButton = this.createButton('next-button', this.buttonsOptions.next);
15730
- buttonsContainer.appendChild(this.prevButton);
15731
- buttonsContainer.appendChild(this.nextButton);
16585
+ const buttonsClass = together ? `buttons-together ${position}` : 'buttons-separate';
16586
+ buttonsContainer.className = `buttons ${buttonsClass} ${size}`;
16587
+ buttonsContainer.style.setProperty('--opacity', opacity.toString());
16588
+ this.elements.prevButton = this.createButton('prev-button', this.state.buttons.prev);
16589
+ this.elements.nextButton = this.createButton('next-button', this.state.buttons.next);
16590
+ buttonsContainer.appendChild(this.elements.prevButton);
16591
+ buttonsContainer.appendChild(this.elements.nextButton);
15732
16592
  return buttonsContainer;
15733
16593
  }
16594
+ /**
16595
+ * Creates a single button element with specified properties
16596
+ * Used for both prev and next buttons
16597
+ */
15734
16598
  createButton(className, text) {
16599
+ const { textColor, backgroundColor, borderRadius } = this.state.buttons;
15735
16600
  const button = document.createElement('button');
15736
16601
  button.className = className;
15737
16602
  button.textContent = text;
15738
- button.style.color = this.buttonsOptions.textColor;
15739
- button.style.backgroundColor = this.buttonsOptions.backgroundColor;
15740
- button.style.borderRadius = this.buttonsOptions.borderRadius;
16603
+ button.style.color = textColor;
16604
+ button.style.backgroundColor = backgroundColor;
16605
+ button.style.borderRadius = borderRadius;
15741
16606
  return button;
15742
16607
  }
15743
- setupDots() {
15744
- if (!this.shadowRoot || !this.useDots)
16608
+ /**
16609
+ * Caches references to important DOM elements
16610
+ * Stores references to slides container, dots, and buttons
16611
+ */
16612
+ cacheElements() {
16613
+ if (!this.shadowRoot)
15745
16614
  return;
15746
- this.dotElements = Array.from(this.shadowRoot.querySelectorAll('.dot'));
15747
- this.dotElements.forEach((dot, index) => {
15748
- dot.addEventListener('click', () => {
15749
- this.goToSlide(index);
15750
- this.resetAutoplay();
15751
- });
15752
- });
16615
+ this.elements.slidesContainer = this.shadowRoot.querySelector('.slides');
16616
+ this.elements.dots = Array.from(this.shadowRoot.querySelectorAll('.dot'));
16617
+ this.elements.prevButton = this.shadowRoot.querySelector('.prev-button');
16618
+ this.elements.nextButton = this.shadowRoot.querySelector('.next-button');
15753
16619
  }
15754
- setupButtons() {
15755
- var _a, _b;
15756
- if (!this.useButtons)
16620
+ /** Event Management */
16621
+ /**
16622
+ * Sets up all carousel event listeners
16623
+ * Includes dots, buttons, and transition events
16624
+ */
16625
+ setupCarousel() {
16626
+ this.setupEventListeners();
16627
+ // Add transition event listener with bound method
16628
+ if (this.elements.slidesContainer) {
16629
+ this.elements.slidesContainer.addEventListener('transitionend', this.handleTransitionEnd);
16630
+ }
16631
+ this.updateCarousel(false);
16632
+ }
16633
+ /**
16634
+ * Sets up touch and drag event listeners
16635
+ * Handles both mobile touch and desktop mouse events
16636
+ */
16637
+ setupTouchEvents() {
16638
+ if (!this.elements.slidesContainer)
15757
16639
  return;
15758
- (_a = this.prevButton) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => {
15759
- this.prevSlide();
15760
- this.resetAutoplay();
16640
+ // Touch events
16641
+ this.elements.slidesContainer.addEventListener('touchstart', this.handleTouchStart, {
16642
+ passive: true,
15761
16643
  });
15762
- (_b = this.nextButton) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => {
15763
- this.nextSlide();
15764
- this.resetAutoplay();
16644
+ this.elements.slidesContainer.addEventListener('touchmove', this.handleTouchMove, {
16645
+ passive: false,
15765
16646
  });
16647
+ this.elements.slidesContainer.addEventListener('touchend', this.handleTouchEnd);
16648
+ // Mouse drag events
16649
+ this.elements.slidesContainer.addEventListener('mousedown', this.handleDragStart);
16650
+ document.addEventListener('mousemove', this.handleDrag);
16651
+ document.addEventListener('mouseup', this.handleDragEnd);
15766
16652
  }
15767
- nextSlide() {
15768
- this.goToSlide((this.currentSlide + 1) % this.slides.length);
15769
- }
15770
- prevSlide() {
15771
- this.goToSlide((this.currentSlide - 1 + this.slides.length) % this.slides.length);
16653
+ /**
16654
+ * Removes all event listeners
16655
+ * Called during cleanup and before re-adding listeners
16656
+ */
16657
+ removeEventListeners() {
16658
+ this.elements.dots.forEach((dot) => {
16659
+ dot.replaceWith(dot.cloneNode(true));
16660
+ });
16661
+ if (this.elements.prevButton) {
16662
+ this.elements.prevButton.replaceWith(this.elements.prevButton.cloneNode(true));
16663
+ }
16664
+ if (this.elements.nextButton) {
16665
+ this.elements.nextButton.replaceWith(this.elements.nextButton.cloneNode(true));
16666
+ }
15772
16667
  }
15773
- goToSlide(index) {
15774
- this.currentSlide = index;
15775
- this.updateCarousel();
16668
+ /**
16669
+ * Removes touch and drag event listeners
16670
+ * Separate from other event listeners for better management
16671
+ */
16672
+ removeTouchEvents() {
16673
+ if (!this.elements.slidesContainer)
16674
+ return;
16675
+ this.elements.slidesContainer.removeEventListener('touchstart', this.handleTouchStart);
16676
+ this.elements.slidesContainer.removeEventListener('touchmove', this.handleTouchMove);
16677
+ this.elements.slidesContainer.removeEventListener('touchend', this.handleTouchEnd);
16678
+ this.elements.slidesContainer.removeEventListener('mousedown', this.handleDragStart);
16679
+ document.removeEventListener('mousemove', this.handleDrag);
16680
+ document.removeEventListener('mouseup', this.handleDragEnd);
16681
+ }
16682
+ /** Touch and Drag Event Handlers */
16683
+ /**
16684
+ * Handles start of touch interaction
16685
+ * Initializes drag tracking and pauses autoplay
16686
+ */
16687
+ handleTouchStart(e) {
16688
+ if (this.state.isTransitioning)
16689
+ return;
16690
+ this.pauseAutoplay();
16691
+ this.state.startX = e.touches[0].clientX;
16692
+ this.state.dragStartTime = Date.now();
16693
+ this.state.isDragging = true;
16694
+ this.state.dragDistance = 0;
15776
16695
  }
15777
- updateCarousel() {
15778
- if (!this.slidesContainer)
16696
+ /**
16697
+ * Handles touch movement
16698
+ * Calculates drag distance and updates carousel position
16699
+ */
16700
+ handleTouchMove(e) {
16701
+ if (!this.state.isDragging || this.state.isTransitioning)
15779
16702
  return;
15780
- const slides = Array.from(this.slidesContainer.children);
15781
- slides.forEach((slide, index) => {
15782
- slide.classList.toggle('active', index === this.currentSlide);
15783
- });
15784
- this.updateDots();
16703
+ e.preventDefault();
16704
+ const currentX = e.touches[0].clientX;
16705
+ this.state.dragDistance = this.state.startX - currentX;
16706
+ this.updateDragPosition();
15785
16707
  }
15786
- updateDots() {
15787
- if (!this.useDots)
16708
+ /**
16709
+ * Handles end of touch interaction
16710
+ * Determines if slide should change based on drag distance
16711
+ */
16712
+ handleTouchEnd() {
16713
+ if (!this.state.isDragging)
15788
16714
  return;
15789
- this.dotElements.forEach((dot, index) => {
15790
- const isActive = index === this.currentSlide;
15791
- dot.classList.toggle('active', isActive);
15792
- dot.style.backgroundColor = isActive
15793
- ? this.dotsOptions.activeColor
15794
- : this.dotsOptions.color;
15795
- });
16715
+ const dragDuration = Date.now() - this.state.dragStartTime;
16716
+ const velocity = Math.abs(this.state.dragDistance) / dragDuration;
16717
+ const isQuickSwipe = velocity > 0.5;
16718
+ this.handleDragComplete(isQuickSwipe);
16719
+ setTimeout(() => this.resumeAutoplay(), 100);
15796
16720
  }
15797
- startAutoplay() {
15798
- this.autoplayInterval = window.setInterval(() => this.nextSlide(), this.interval);
16721
+ /**
16722
+ * Handles start of mouse drag
16723
+ * Similar to touch start but for desktop interaction
16724
+ */
16725
+ handleDragStart(e) {
16726
+ if (this.state.isTransitioning)
16727
+ return;
16728
+ this.pauseAutoplay();
16729
+ this.state.startX = e.clientX;
16730
+ this.state.dragStartTime = Date.now();
16731
+ this.state.isDragging = true;
16732
+ this.state.dragDistance = 0;
16733
+ e.preventDefault();
15799
16734
  }
15800
- stopAutoplay() {
15801
- if (this.autoplayInterval !== null) {
15802
- window.clearInterval(this.autoplayInterval);
15803
- this.autoplayInterval = null;
15804
- }
16735
+ /**
16736
+ * Handles mouse movement during drag
16737
+ * Updates carousel position during drag
16738
+ */
16739
+ handleDrag(e) {
16740
+ if (!this.state.isDragging || this.state.isTransitioning)
16741
+ return;
16742
+ const currentX = e.clientX;
16743
+ this.state.dragDistance = this.state.startX - currentX;
16744
+ this.updateDragPosition();
15805
16745
  }
16746
+ /**
16747
+ * Handles end of mouse drag
16748
+ * Determines final slide position based on drag
16749
+ */
16750
+ handleDragEnd() {
16751
+ if (!this.state.isDragging)
16752
+ return;
16753
+ const dragDuration = Date.now() - this.state.dragStartTime;
16754
+ const velocity = Math.abs(this.state.dragDistance) / dragDuration;
16755
+ const isQuickSwipe = velocity > 0.5;
16756
+ this.handleDragComplete(isQuickSwipe);
16757
+ setTimeout(() => this.resumeAutoplay(), 100);
16758
+ }
16759
+ /** Navigation and Position Management */
16760
+ /**
16761
+ * Handles navigation button clicks and programmatic navigation
16762
+ * Updates both real and virtual indices
16763
+ */
16764
+ handleNavigation(direction) {
16765
+ if (this.state.isTransitioning)
16766
+ return;
16767
+ const totalSlides = this.slides.length;
16768
+ if (direction === 'next') {
16769
+ this.state.realIndex = (this.state.realIndex + 1) % totalSlides;
16770
+ this.state.virtualIndex++;
16771
+ }
16772
+ else {
16773
+ this.state.realIndex = (this.state.realIndex - 1 + totalSlides) % totalSlides;
16774
+ this.state.virtualIndex--;
16775
+ }
16776
+ this.updateCarousel(true);
16777
+ }
16778
+ /**
16779
+ * Handles dot navigation clicks
16780
+ * Calculates shortest path to target slide
16781
+ */
16782
+ handleDotClick(index) {
16783
+ const currentRealIndex = this.state.realIndex;
16784
+ const totalSlides = this.slides.length;
16785
+ // Calculate the shortest path to the target slide
16786
+ let diff = index - currentRealIndex;
16787
+ // Adjust for shortest path when crossing the boundary
16788
+ if (Math.abs(diff) > totalSlides / 2) {
16789
+ diff = diff > 0 ? diff - totalSlides : diff + totalSlides;
16790
+ }
16791
+ // Update both real and virtual indices
16792
+ this.state.realIndex = index;
16793
+ this.state.virtualIndex += diff;
16794
+ this.updateCarousel(true);
16795
+ this.resetAutoplay();
16796
+ }
16797
+ /**
16798
+ * Completes a drag interaction
16799
+ * Determines whether to change slides based on drag distance
16800
+ */
16801
+ handleDragComplete(isQuickSwipe) {
16802
+ this.state.isDragging = false;
16803
+ if (!this.elements.slidesContainer)
16804
+ return;
16805
+ const dragPercentage = this.state.dragDistance / this.state.containerWidth;
16806
+ const threshold = isQuickSwipe ? 0.1 : CustomCarouselElement.defaultConfigs.dragThreshold;
16807
+ if (Math.abs(dragPercentage) > threshold) {
16808
+ if (this.state.dragDistance > 0) {
16809
+ this.handleNavigation('next');
16810
+ }
16811
+ else {
16812
+ this.handleNavigation('prev');
16813
+ }
16814
+ }
16815
+ else {
16816
+ this.updateCarousel(true);
16817
+ }
16818
+ this.state.dragDistance = 0;
16819
+ }
16820
+ /**
16821
+ * Updates carousel position during drag
16822
+ * Applies transform without transition
16823
+ */
16824
+ updateDragPosition() {
16825
+ if (!this.elements.slidesContainer || this.state.isTransitioning)
16826
+ return;
16827
+ const translateX = -this.state.virtualIndex * 100 -
16828
+ (this.state.dragDistance / this.state.containerWidth) * 100;
16829
+ this.elements.slidesContainer.style.transform = `translateX(${translateX}%)`;
16830
+ this.elements.slidesContainer.style.transition = 'none';
16831
+ }
16832
+ /**
16833
+ * Navigates to a specific slide
16834
+ * Used primarily by dot navigation
16835
+ */
16836
+ goToSlide(index) {
16837
+ if (this.state.isTransitioning)
16838
+ return;
16839
+ this.state.currentSlide = index;
16840
+ this.updateCarousel(true);
16841
+ }
16842
+ /** Transition and Animation Management */
16843
+ /**
16844
+ * Updates carousel position and state
16845
+ * Handles both animated and immediate updates
16846
+ */
16847
+ updateCarousel(animated = true) {
16848
+ if (!this.elements.slidesContainer)
16849
+ return;
16850
+ const totalSlides = this.slides.length;
16851
+ // Calculate the translation for the current virtual position
16852
+ const translateX = -this.state.virtualIndex * 100;
16853
+ if (animated) {
16854
+ this.state.isTransitioning = true;
16855
+ this.elements.slidesContainer.style.transition = 'transform 0.3s ease-out';
16856
+ }
16857
+ else {
16858
+ this.elements.slidesContainer.style.transition = 'none';
16859
+ }
16860
+ this.elements.slidesContainer.style.transform = `translateX(${translateX}%)`;
16861
+ this.updateDots();
16862
+ // Check if we need to reset virtual position
16863
+ if (this.state.virtualIndex <= 0 || this.state.virtualIndex >= totalSlides + 1) {
16864
+ this.state.isVirtualizing = true;
16865
+ }
16866
+ }
16867
+ /**
16868
+ * Handles the end of slide transitions
16869
+ * Manages infinite scroll position reset
16870
+ */
16871
+ handleTransitionEnd(event) {
16872
+ if (event.target !== this.elements.slidesContainer || event.propertyName !== 'transform')
16873
+ return;
16874
+ this.state.isTransitioning = false;
16875
+ if (this.state.isVirtualizing) {
16876
+ this.resetVirtualPosition();
16877
+ }
16878
+ if (this.elements.slidesContainer) {
16879
+ this.elements.slidesContainer.style.transition = 'none';
16880
+ }
16881
+ }
16882
+ /**
16883
+ * Resets virtual position for infinite scroll
16884
+ * Called after hitting first or last clone
16885
+ */
16886
+ resetVirtualPosition() {
16887
+ var _a;
16888
+ const totalSlides = this.slides.length;
16889
+ if (this.state.virtualIndex <= 0) {
16890
+ // If we've moved before the first clone, jump to the last real slide
16891
+ this.state.virtualIndex = totalSlides;
16892
+ }
16893
+ else if (this.state.virtualIndex >= totalSlides + 1) {
16894
+ // If we've moved after the last clone, jump to the first real slide
16895
+ this.state.virtualIndex = 1;
16896
+ }
16897
+ if (this.elements.slidesContainer) {
16898
+ this.elements.slidesContainer.style.transition = 'none';
16899
+ this.elements.slidesContainer.style.transform = `translateX(${-this.state.virtualIndex * 100}%)`;
16900
+ }
16901
+ this.state.isVirtualizing = false;
16902
+ // Force reflow to ensure the style change takes effect immediately
16903
+ void ((_a = this.elements.slidesContainer) === null || _a === void 0 ? void 0 : _a.offsetHeight);
16904
+ }
16905
+ /**
16906
+ * Updates dot active states
16907
+ * Reflects current slide position in navigation
16908
+ */
16909
+ updateDots() {
16910
+ if (!this.state.useDots)
16911
+ return;
16912
+ this.elements.dots.forEach((dot, index) => {
16913
+ const isActive = index === this.state.realIndex;
16914
+ dot.classList.toggle('active', isActive);
16915
+ dot.style.backgroundColor = isActive ? this.state.dots.activeColor : this.state.dots.color;
16916
+ });
16917
+ }
16918
+ /** Autoplay Management */
16919
+ /**
16920
+ * Starts autoplay if enabled
16921
+ * Called during initialization
16922
+ */
16923
+ startAutoplayIfEnabled() {
16924
+ if (this.state.autoplay && !this.state.isAutoplayPaused) {
16925
+ this.startAutoplay();
16926
+ }
16927
+ }
16928
+ /**
16929
+ * Starts the autoplay interval
16930
+ * Ensures only one interval is running
16931
+ */
16932
+ startAutoplay() {
16933
+ this.stopAutoplay(); // Ensure any existing interval is cleared first
16934
+ if (!this.state.isAutoplayPaused && this.state.autoplay) {
16935
+ this.state.autoplayInterval = window.setInterval(() => this.handleNavigation('next'), this.state.interval);
16936
+ }
16937
+ }
16938
+ /**
16939
+ * Pauses autoplay
16940
+ * Used during user interaction
16941
+ */
16942
+ pauseAutoplay() {
16943
+ this.state.isAutoplayPaused = true;
16944
+ this.stopAutoplay();
16945
+ }
16946
+ /**
16947
+ * Resumes autoplay after pause
16948
+ * Used after user interaction ends
16949
+ */
16950
+ resumeAutoplay() {
16951
+ this.state.isAutoplayPaused = false;
16952
+ if (this.state.autoplay) {
16953
+ this.startAutoplay();
16954
+ }
16955
+ }
16956
+ /**
16957
+ * Stops autoplay completely
16958
+ * Cleans up interval
16959
+ */
16960
+ stopAutoplay() {
16961
+ if (this.state.autoplayInterval !== null) {
16962
+ window.clearInterval(this.state.autoplayInterval);
16963
+ this.state.autoplayInterval = null;
16964
+ }
16965
+ }
16966
+ /**
16967
+ * Resets autoplay interval
16968
+ * Used after user interactions
16969
+ */
15806
16970
  resetAutoplay() {
15807
- if (this.autoplay) {
16971
+ if (!this.state.isDragging && this.state.autoplay) {
15808
16972
  this.stopAutoplay();
15809
16973
  this.startAutoplay();
15810
16974
  }
15811
16975
  }
15812
- validateOptions() {
15813
- this.validatePosition(this.dotsOptions.position, 'dotsPosition', 'bottom-center');
15814
- this.validateButtonsPosition();
16976
+ /** Utility Methods */
16977
+ /**
16978
+ * Updates container width on resize
16979
+ * Used for drag distance calculations
16980
+ */
16981
+ updateContainerWidth() {
16982
+ if (this.elements.slidesContainer) {
16983
+ this.state.containerWidth = this.elements.slidesContainer.offsetWidth;
16984
+ }
15815
16985
  }
15816
- validatePosition(position, optionName, defaultValue) {
15817
- if (!CustomCarouselElement.validPositions.includes(position)) {
15818
- console.warn(`Invalid ${optionName}: ${position}. Defaulting to '${defaultValue}'.`);
15819
- if (optionName === 'dotsPosition') {
15820
- this.dotsOptions.position = defaultValue;
15821
- }
15822
- else if (optionName === 'buttonsPosition') {
15823
- this.buttonsOptions.position = defaultValue;
15824
- }
16986
+ /**
16987
+ * Cleans up all event listeners
16988
+ * Called during component cleanup
16989
+ */
16990
+ cleanupEventListeners() {
16991
+ this.stopAutoplay();
16992
+ this.removeEventListeners();
16993
+ this.removeTouchEvents();
16994
+ // Remove transition listener
16995
+ if (this.elements.slidesContainer) {
16996
+ this.elements.slidesContainer.removeEventListener('transitionend', this.handleTransitionEnd);
16997
+ }
16998
+ window.removeEventListener('resize', this.updateContainerWidth);
16999
+ }
17000
+ /**
17001
+ * Sets up all event listeners
17002
+ * Includes dots, buttons, and transition events
17003
+ */
17004
+ setupEventListeners() {
17005
+ var _a, _b;
17006
+ if (this.state.useDots) {
17007
+ this.elements.dots.forEach((dot, index) => {
17008
+ dot.addEventListener('click', () => this.handleDotClick(index));
17009
+ });
17010
+ }
17011
+ if (this.state.useButtons) {
17012
+ (_a = this.elements.prevButton) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => this.handleNavigation('prev'));
17013
+ (_b = this.elements.nextButton) === null || _b === void 0 ? void 0 : _b.addEventListener('click', () => this.handleNavigation('next'));
15825
17014
  }
15826
17015
  }
17016
+ /**
17017
+ * Validates button position configuration
17018
+ * Ensures only valid positions are used
17019
+ */
15827
17020
  validateButtonsPosition() {
15828
- if (this.useButtons) {
15829
- if (this.buttonsOptions.together) {
15830
- this.validatePosition(this.buttonsOptions.position, 'buttonsPosition', 'bottom-center');
15831
- }
15832
- else if (this.buttonsOptions.position !== 'middle-sides') {
15833
- console.warn(`Invalid buttonsPosition: ${this.buttonsOptions.position}. When buttons are not together, only 'middle-sides' is allowed. Defaulting to 'middle-sides'.`);
15834
- this.buttonsOptions.position = 'middle-sides';
15835
- }
17021
+ if (this.state.useButtons &&
17022
+ !this.state.buttons.together &&
17023
+ this.state.buttons.position !== 'middle-sides') {
17024
+ console.warn('When buttons are not together, only "middle-sides" is allowed. Defaulting to "middle-sides".');
17025
+ this.state.buttons.position = 'middle-sides';
15836
17026
  }
15837
17027
  }
15838
17028
  }
15839
- CustomCarouselElement.defaultInterval = 5000;
15840
- CustomCarouselElement.validPositions = [
17029
+ /** Core initialization and lifecycle methods */
17030
+ CustomCarouselElement.defaultConfigs = {
17031
+ interval: 5000,
17032
+ touchThreshold: 50, // minimum swipe distance in pixels
17033
+ dragThreshold: 0.2, // minimum drag percentage of carousel width
17034
+ dots: {
17035
+ position: 'bottom-center',
17036
+ color: '#d9d9d9',
17037
+ activeColor: '#b5914a',
17038
+ size: 'base',
17039
+ opacity: 1,
17040
+ },
17041
+ buttons: {
17042
+ together: false,
17043
+ position: 'middle-sides',
17044
+ textColor: '#000000',
17045
+ backgroundColor: '#ffffff',
17046
+ borderRadius: '50%',
17047
+ prev: 'Prev',
17048
+ next: 'Next',
17049
+ size: 'base',
17050
+ opacity: 1,
17051
+ },
17052
+ };
17053
+ CustomCarouselElement.validPositions = new Set([
15841
17054
  'top-left',
15842
17055
  'top-center',
15843
17056
  'top-right',
@@ -15847,10 +17060,122 @@ if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined
15847
17060
  'middle-left',
15848
17061
  'middle-right',
15849
17062
  'middle-sides',
15850
- ];
17063
+ ]);
15851
17064
  CarouselElement = CustomCarouselElement;
15852
17065
  }
15853
17066
 
17067
+ function SkeletonTemplate({ fluid, width, height }) {
17068
+ return `
17069
+ <style>
17070
+ :host {
17071
+ display: block;
17072
+ position: relative;
17073
+ box-sizing: border-box;
17074
+ overflow: hidden;
17075
+ width: ${fluid ? '100%' : `${width}px`};
17076
+ height: ${fluid ? '100%' : `${height}px`};
17077
+ background: #ffffff;
17078
+ padding: 20px;
17079
+ border-radius: 5px;
17080
+ }
17081
+
17082
+ .content {
17083
+ height: 100%;
17084
+ display: flex;
17085
+ flex-direction: column;
17086
+ gap: 20px;
17087
+ }
17088
+
17089
+ .image-placeholder {
17090
+ width: 100%;
17091
+ height: 100%;
17092
+ background: #f0f0f0;
17093
+ border-radius: 4px;
17094
+ position: relative;
17095
+ overflow: hidden;
17096
+ }
17097
+
17098
+ .lines-container {
17099
+ display: flex;
17100
+ flex-direction: column;
17101
+ justify-content: flex-end;
17102
+ }
17103
+
17104
+ .line {
17105
+ height: 20px;
17106
+ background: #f0f0f0;
17107
+ border-radius: 4px;
17108
+ margin-bottom: 15px;
17109
+ position: relative;
17110
+ overflow: hidden;
17111
+ }
17112
+
17113
+ .image-placeholder::after,
17114
+ .line::after {
17115
+ content: "";
17116
+ position: absolute;
17117
+ top: 0;
17118
+ left: 0;
17119
+ width: 100%;
17120
+ height: 100%;
17121
+ background: linear-gradient(
17122
+ 90deg,
17123
+ rgba(255, 255, 255, 0) 0%,
17124
+ rgba(255, 255, 255, 0.5) 50%,
17125
+ rgba(255, 255, 255, 0) 100%
17126
+ );
17127
+ animation: shimmer 1.5s infinite;
17128
+ }
17129
+
17130
+ .line.header {
17131
+ width: 25%;
17132
+ }
17133
+
17134
+ .line.description {
17135
+ width: 65%;
17136
+ }
17137
+
17138
+ .line.button {
17139
+ width: 40%;
17140
+ }
17141
+
17142
+ @keyframes shimmer {
17143
+ 0% {
17144
+ transform: translateX(-100%);
17145
+ }
17146
+ 100% {
17147
+ transform: translateX(100%);
17148
+ }
17149
+ }
17150
+ </style>
17151
+
17152
+ <div class="content">
17153
+ <div class="image-placeholder"></div>
17154
+ <div class="lines-container">
17155
+ <div class="line header"></div>
17156
+ <div class="line description"></div>
17157
+ <div class="line button"></div>
17158
+ </div>
17159
+ </div>
17160
+ `;
17161
+ }
17162
+
17163
+ let SkeletonElement;
17164
+ if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined') {
17165
+ class CustomSkeletonElement extends HTMLElement {
17166
+ constructor() {
17167
+ super();
17168
+ this.attachShadow({ mode: 'open' });
17169
+ }
17170
+ connectedCallback() {
17171
+ if (!this.shadowRoot || !this.data)
17172
+ return;
17173
+ this.shadowRoot.innerHTML = SkeletonTemplate(this.data);
17174
+ }
17175
+ }
17176
+ SkeletonElement = CustomSkeletonElement;
17177
+ }
17178
+
15854
17179
  let SpotElement;
15855
17180
  if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined') {
15856
17181
  class CustomSpotElement extends HTMLElement {
@@ -15890,19 +17215,20 @@ if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined
15890
17215
  * #########################################################
15891
17216
  */
15892
17217
  handleSpotSizeChanged(event) {
15893
- var _a, _b, _c;
15894
- const isRBSpot = (_c = (_b = (_a = this.data) === null || _a === void 0 ? void 0 : _a.spot) === null || _b === void 0 ? void 0 : _b.startsWith('rb')) !== null && _c !== void 0 ? _c : false;
15895
- if (!isRBSpot) {
15896
- // Adjust text elements font size based on the scale factor
15897
- this.adjustFontSize(event.detail.scale);
15898
- }
17218
+ // Adjust text elements font size based on the scale factor
17219
+ this.adjustFontSize(event.detail.scale);
15899
17220
  }
15900
17221
  adjustFontSize(elementScale) {
15901
17222
  var _a;
15902
- const scaleFactor = calculateScaleFactor(elementScale);
15903
17223
  // Find all text elements within the shadow root
15904
- const elements = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('h1, h2, h3, h4, p, span');
15905
- elements === null || elements === void 0 ? void 0 : elements.forEach((element) => {
17224
+ const selectors = ['h1', 'h2', 'h3', 'h4', 'p', 'span']
17225
+ .map((tag) => `[data-spot="iab"] ${tag}`)
17226
+ .join(', ');
17227
+ const elements = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll(selectors);
17228
+ if (!elements)
17229
+ return;
17230
+ const scaleFactor = calculateScaleFactor(elementScale);
17231
+ elements.forEach((element) => {
15906
17232
  if (element instanceof HTMLElement) {
15907
17233
  if (!this.originalFontSizes.has(element)) {
15908
17234
  const originalSize = parseFloat(window.getComputedStyle(element).fontSize);
@@ -15958,15 +17284,14 @@ class ElementService {
15958
17284
  * @return {HTMLElement | null} - The html element or null if the browser environment is not available.
15959
17285
  */
15960
17286
  createSpotElement({ content, config }) {
15961
- var _a, _b;
17287
+ var _a;
15962
17288
  if (!this.ensureBrowserEnvironmentAndDefineElement()) {
15963
17289
  return null;
15964
17290
  }
15965
17291
  const spot = document.createElement(SPOT_ELEMENT_TAG);
15966
- spot.setAttribute('type', (_a = config === null || config === void 0 ? void 0 : config.spot) !== null && _a !== void 0 ? _a : '');
15967
17292
  spot.data = {
15968
17293
  spot: config === null || config === void 0 ? void 0 : config.spot,
15969
- fluid: (_b = config === null || config === void 0 ? void 0 : config.fluid) !== null && _b !== void 0 ? _b : false,
17294
+ fluid: (_a = config === null || config === void 0 ? void 0 : config.fluid) !== null && _a !== void 0 ? _a : false,
15970
17295
  ...config,
15971
17296
  };
15972
17297
  spot.content = content;
@@ -15994,6 +17319,27 @@ class ElementService {
15994
17319
  carousel.slides = slides;
15995
17320
  return carousel;
15996
17321
  }
17322
+ /**
17323
+ * Creates the skeleton html element based on the provided data using shadow dom.
17324
+ *
17325
+ * This method is only available in browser environments.
17326
+ *
17327
+ * @param {ICreateSkeletonElementParams} params - The parameters to create the final element.
17328
+ *
17329
+ * @return {HTMLElement | null} - The html element or null if the browser environment is not available.
17330
+ */
17331
+ createSkeletonElement(params) {
17332
+ if (!this.ensureBrowserEnvironmentAndDefineElement()) {
17333
+ return null;
17334
+ }
17335
+ const skeleton = document.createElement(SKELETON_ELEMENT_TAG);
17336
+ const dimensions = SPOT_DIMENSIONS[params.spotType];
17337
+ skeleton.data = {
17338
+ fluid: params.fluid,
17339
+ ...dimensions,
17340
+ };
17341
+ return skeleton;
17342
+ }
15997
17343
  /**
15998
17344
  * Overrides the spot colors with the provided colors.
15999
17345
  *
@@ -16028,158 +17374,26 @@ class ElementService {
16028
17374
  if (!window.customElements.get(CAROUSEL_ELEMENT_TAG)) {
16029
17375
  window.customElements.define(CAROUSEL_ELEMENT_TAG, CarouselElement);
16030
17376
  }
17377
+ if (!window.customElements.get(SKELETON_ELEMENT_TAG)) {
17378
+ window.customElements.define(SKELETON_ELEMENT_TAG, SkeletonElement);
17379
+ }
16031
17380
  return true;
16032
17381
  }
16033
17382
  }
16034
17383
 
16035
- class UniqueIdGenerator {
16036
- /**
16037
- * Initialize the generator with a node ID
16038
- * @param nodeId Number between 0-1023 to identify this instance
16039
- */
16040
- static initialize(nodeId = Math.floor(Math.random() * 1024)) {
16041
- if (nodeId < 0 || nodeId >= 1 << this.nodeBits) {
16042
- throw new Error(`Node ID must be between 0 and ${(1 << this.nodeBits) - 1}`);
16043
- }
16044
- this.nodeId = nodeId;
16045
- }
16046
- /**
16047
- * Convert a number to base32 string with specified length
16048
- */
16049
- static toBase32(num, length) {
16050
- let result = '';
16051
- while (num > 0) {
16052
- result = this.base32Chars[Number(num % BigInt(32))] + result;
16053
- // @ts-expect-error - TS doesn't support bigint division
16054
- num = num / 32n;
16055
- }
16056
- return result.padStart(length, '0');
16057
- }
16058
- /**
16059
- * Generate a cryptographically secure random number
16060
- */
16061
- static getSecureRandom() {
16062
- if (typeof crypto !== 'undefined') {
16063
- const buffer = new Uint32Array(1);
16064
- crypto.getRandomValues(buffer);
16065
- return buffer[0];
16066
- }
16067
- return Math.floor(Math.random() * 0xffffffff);
16068
- }
16069
- /**
16070
- * Wait until next millisecond
16071
- */
16072
- static waitNextMillis(lastTimestamp) {
16073
- let timestamp = Date.now();
16074
- while (timestamp <= lastTimestamp) {
16075
- timestamp = Date.now();
16076
- }
16077
- return timestamp;
16078
- }
16079
- /**
16080
- * Generates a highly unique ID with the following format:
16081
- * TTTTTTTTTTCCCCNNNNNRRRR
16082
- * T: Timestamp (10 chars)
16083
- * C: Counter (4 chars)
16084
- * N: Node ID (5 chars)
16085
- * R: Random (4 chars)
16086
- *
16087
- * Total length: 23 characters, always uppercase alphanumeric
16088
- */
16089
- static generate() {
16090
- if (this.nodeId === undefined) {
16091
- this.initialize();
16092
- }
16093
- let timestamp = Date.now() - this.epoch;
16094
- // Handle clock moving backwards or same millisecond
16095
- if (timestamp < this.lastTimestamp) {
16096
- throw new Error('Clock moved backwards. Refusing to generate ID.');
16097
- }
16098
- if (timestamp === this.lastTimestamp) {
16099
- this.sequence = (this.sequence + 1) & ((1 << this.sequenceBits) - 1);
16100
- if (this.sequence === 0) {
16101
- timestamp = this.waitNextMillis(this.lastTimestamp);
16102
- }
16103
- }
16104
- else {
16105
- this.sequence = 0;
16106
- }
16107
- this.lastTimestamp = timestamp;
16108
- // Generate random component
16109
- const random = this.getSecureRandom() & 0xffff; // 16 bits of randomness
16110
- // Combine all components into a BigInt
16111
- // const id =
16112
- // (BigInt(timestamp) << BigInt(this.nodeBits + this.sequenceBits + 16)) |
16113
- // (BigInt(this.nodeId) << BigInt(this.sequenceBits + 16)) |
16114
- // (BigInt(this.sequence) << BigInt(16)) |
16115
- // BigInt(random);
16116
- // Convert to base32 representation
16117
- const timeComponent = this.toBase32(BigInt(timestamp), 10);
16118
- const counterComponent = this.toBase32(BigInt(this.sequence), 4);
16119
- const nodeComponent = this.toBase32(BigInt(this.nodeId), 5);
16120
- const randomComponent = this.toBase32(BigInt(random), 4);
16121
- return `${timeComponent}${counterComponent}${nodeComponent}${randomComponent}`;
16122
- }
16123
- /**
16124
- * Validates if a string matches the expected ID format
16125
- */
16126
- static isValid(id) {
16127
- if (!/^[0-9A-HJ-NP-Z]{23}$/.test(id))
16128
- return false;
16129
- try {
16130
- const timeComponent = id.slice(0, 10);
16131
- const timestamp = this.decodeBase32(timeComponent);
16132
- const now = Date.now() - this.epoch;
16133
- return timestamp >= 0 && timestamp <= now;
16134
- }
16135
- catch (_a) {
16136
- return false;
16137
- }
16138
- }
16139
- /**
16140
- * Decode base32 string to number
16141
- */
16142
- static decodeBase32(str) {
16143
- let result = 0;
16144
- for (const char of str) {
16145
- result = result * 32 + this.base32Chars.indexOf(char);
16146
- }
16147
- return result;
16148
- }
16149
- /**
16150
- * Extract timestamp from ID
16151
- */
16152
- static getTimestamp(id) {
16153
- if (!this.isValid(id))
16154
- throw new Error('Invalid ID format');
16155
- const timeComponent = id.slice(0, 10);
16156
- const timestamp = this.decodeBase32(timeComponent);
16157
- return new Date(timestamp + this.epoch);
16158
- }
16159
- }
16160
- // Constants for bit manipulation
16161
- UniqueIdGenerator.epoch = 1577836800000; // 2020-01-01 as epoch
16162
- UniqueIdGenerator.nodeBits = 10;
16163
- UniqueIdGenerator.sequenceBits = 12;
16164
- // Instance variables
16165
- UniqueIdGenerator.lastTimestamp = -1;
16166
- UniqueIdGenerator.sequence = 0;
16167
- // Character set for base32 encoding (excluding similar looking characters)
16168
- UniqueIdGenerator.base32Chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
16169
-
16170
- function convertHexToRgba(hex, opacity = 1) {
16171
- // Remove # if present
16172
- const cleanHex = hex.replace('#', '');
16173
- // Convert hex to RGB
16174
- const r = parseInt(cleanHex.substring(0, 2), 16);
16175
- const g = parseInt(cleanHex.substring(2, 4), 16);
16176
- const b = parseInt(cleanHex.substring(4, 6), 16);
16177
- // Return rgba string
16178
- return `rgba(${r}, ${g}, ${b}, ${opacity})`;
16179
- }
16180
- function generateGradientColor(overlay, fallback = '') {
16181
- if (!overlay) {
16182
- return fallback;
17384
+ function convertHexToRgba(hex, opacity = 1) {
17385
+ // Remove # if present
17386
+ const cleanHex = hex.replace('#', '');
17387
+ // Convert hex to RGB
17388
+ const r = parseInt(cleanHex.substring(0, 2), 16);
17389
+ const g = parseInt(cleanHex.substring(2, 4), 16);
17390
+ const b = parseInt(cleanHex.substring(4, 6), 16);
17391
+ // Return rgba string
17392
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
17393
+ }
17394
+ function generateGradientColor(overlay, fallback = '') {
17395
+ if (!overlay) {
17396
+ return fallback;
16183
17397
  }
16184
17398
  const OVERLAY_SIZE = {
16185
17399
  small: 10,
@@ -16199,17 +17413,19 @@ function generateGradientColor(overlay, fallback = '') {
16199
17413
  const gradientColor = convertHexToRgba(color, overlayOpacity);
16200
17414
  return `${fullColor} 0%, ${gradientColor} ${goTo}%, ${transparentColor} 100%`;
16201
17415
  }
16202
- function spotHtmlStringToElement(htmlString) {
17416
+ function spotHtmlStringToElement(htmlString, spotType) {
16203
17417
  const spot = document.createElement('div');
17418
+ spot.setAttribute('data-spot', spotType.startsWith('rb') ? 'rb' : 'iab');
16204
17419
  spot.className = 'spot';
16205
17420
  spot.innerHTML = htmlString;
16206
17421
  Object.assign(spot.style, {
16207
- position: 'relative',
16208
17422
  display: 'block',
16209
17423
  width: '100%',
16210
17424
  height: '100%',
16211
17425
  margin: '0',
16212
17426
  padding: '0',
17427
+ containerType: 'inline-size',
17428
+ position: 'relative',
16213
17429
  });
16214
17430
  return spot;
16215
17431
  }
@@ -17079,7 +18295,6 @@ const STYLES$6 = ({ textColor = '#ffffff', ctaTextColor = textColor, ctaBorderCo
17079
18295
  box-sizing: border-box;
17080
18296
  color: ${textColor};
17081
18297
  cursor: pointer;
17082
- container-type: inline-size;
17083
18298
  }
17084
18299
 
17085
18300
  .${prefix}__text {
@@ -17095,7 +18310,7 @@ const STYLES$6 = ({ textColor = '#ffffff', ctaTextColor = textColor, ctaBorderCo
17095
18310
  .${prefix}__header {
17096
18311
  font-size: 24px;
17097
18312
  margin: 0;
17098
- font-family: "Cormorant";
18313
+ font-family: "Cormorant", system-ui;
17099
18314
  font-style: normal;
17100
18315
  font-weight: 300;
17101
18316
  line-height: normal;
@@ -17132,6 +18347,10 @@ const STYLES$6 = ({ textColor = '#ffffff', ctaTextColor = textColor, ctaBorderCo
17132
18347
  .${prefix} {
17133
18348
  background-image: linear-gradient(to top, ${linearGradient}), url("${primaryImage}");
17134
18349
  }
18350
+
18351
+ .${prefix}__text {
18352
+ width: 70%;
18353
+ }
17135
18354
  }
17136
18355
 
17137
18356
  @container (min-width: 768px) {
@@ -17214,7 +18433,6 @@ const STYLES$5 = ({ textColor = '#212121', backgroundColor = '#e8e6de', ctaTextC
17214
18433
  height: 100%;
17215
18434
  display: block;
17216
18435
  position: relative;
17217
- container-type: inline-size;
17218
18436
  }
17219
18437
 
17220
18438
  .${prefix}__content {
@@ -17405,15 +18623,20 @@ function rbHomepageHeroThreeTileTemplate(spot, config) {
17405
18623
  const STYLES$4 = ({ textColor = '#212121', backgroundColor = '#e8e6de', ctaTextColor = textColor, primaryImage, mobilePrimaryImage = primaryImage, }, { prefix }) => `
17406
18624
  <style>
17407
18625
  .${prefix} {
18626
+ width: 100%;
18627
+ height: 100%;
18628
+ background-color: transparent;
18629
+ cursor: pointer;
18630
+ position: relative;
18631
+ }
18632
+
18633
+ .${prefix}__content {
17408
18634
  width: 100%;
17409
18635
  height: 100%;
17410
18636
  display: flex;
17411
18637
  flex-direction: column-reverse;
17412
18638
  background-color: transparent;
17413
18639
  gap: 6px;
17414
- cursor: pointer;
17415
- container-type: inline-size;
17416
- position: relative;
17417
18640
  }
17418
18641
 
17419
18642
  .${prefix}__image {
@@ -17444,7 +18667,7 @@ const STYLES$4 = ({ textColor = '#212121', backgroundColor = '#e8e6de', ctaTextC
17444
18667
  font-size: 18px;
17445
18668
  margin: 0;
17446
18669
  color: inherit;
17447
- font-family: "Cormorant";
18670
+ font-family: "Cormorant", system-ui;
17448
18671
  font-style: normal;
17449
18672
  font-weight: 700;
17450
18673
  line-height: normal;
@@ -17486,7 +18709,7 @@ const STYLES$4 = ({ textColor = '#212121', backgroundColor = '#e8e6de', ctaTextC
17486
18709
  }
17487
18710
 
17488
18711
  @container (min-width: 768px) {
17489
- .${prefix} {
18712
+ .${prefix}__content {
17490
18713
  flex-direction: row;
17491
18714
  }
17492
18715
  .${prefix}__image {
@@ -17541,12 +18764,14 @@ function rbHomepageHeroTwoTileTemplate(spot, config) {
17541
18764
  ${GFONT_CORMORANT}
17542
18765
  ${STYLES$4(spot, config)}
17543
18766
  <div class="${prefix}">
17544
- <div class="${prefix}__text">
17545
- ${spot.header ? `<h2 class="${prefix}__header">${spot.header}</h2>` : ''}
17546
- ${spot.description ? `<p class="${prefix}__description">${spot.description}</p>` : ''}
17547
- ${spot.ctaText ? `<span class="${prefix}__cta-button">${spot.ctaText}</span>` : ''}
17548
- </div>
17549
- <div class="${prefix}__image"></div>
18767
+ <div class="${prefix}__content">
18768
+ <div class="${prefix}__text">
18769
+ ${spot.header ? `<h2 class="${prefix}__header">${spot.header}</h2>` : ''}
18770
+ ${spot.description ? `<p class="${prefix}__description">${spot.description}</p>` : ''}
18771
+ ${spot.ctaText ? `<span class="${prefix}__cta-button">${spot.ctaText}</span>` : ''}
18772
+ </div>
18773
+ <div class="${prefix}__image"></div>
18774
+ </div>
17550
18775
  </div>
17551
18776
  `;
17552
18777
  }
@@ -17569,12 +18794,10 @@ const STYLES$3 = ({ textColor = '#ffffff', ctaTextColor = textColor, ctaBorderCo
17569
18794
  overflow: hidden;
17570
18795
  cursor: pointer;
17571
18796
  color: ${textColor};
17572
- container-type: inline-size;
17573
18797
  }
17574
18798
 
17575
18799
  .${prefix}__text {
17576
18800
  padding: 20px;
17577
- width: 70%;
17578
18801
  display: flex;
17579
18802
  flex-direction: column;
17580
18803
  justify-content: center;
@@ -17586,7 +18809,7 @@ const STYLES$3 = ({ textColor = '#ffffff', ctaTextColor = textColor, ctaBorderCo
17586
18809
  color: inherit;
17587
18810
  margin: 0;
17588
18811
  font-size: 20px;
17589
- font-family: "Cormorant";
18812
+ font-family: "Cormorant", system-ui;
17590
18813
  font-style: normal;
17591
18814
  font-weight: 300;
17592
18815
  line-height: normal;
@@ -17705,7 +18928,6 @@ const STYLES$2 = ({ textColor = '#ffffff', primaryImage, mobilePrimaryImage = pr
17705
18928
  background-size: cover;
17706
18929
  background-position: center;
17707
18930
  background-repeat: no-repeat;
17708
- container-type: inline-size;
17709
18931
  position: relative;
17710
18932
  }
17711
18933
 
@@ -17775,12 +18997,7 @@ const STYLES$1 = ({ textColor = '#ffffff', primaryImage, mobilePrimaryImage = pr
17775
18997
  border-radius: 5px;
17776
18998
  overflow: hidden;
17777
18999
  cursor: pointer;
17778
- container-type: inline-size;
17779
- }
17780
-
17781
- .${prefix}__text {
17782
- padding: 10px;
17783
- width: 70%;
19000
+ position: relative;
17784
19001
  }
17785
19002
 
17786
19003
  .${prefix}__header {
@@ -17791,6 +19008,7 @@ const STYLES$1 = ({ textColor = '#ffffff', primaryImage, mobilePrimaryImage = pr
17791
19008
  font-style: normal;
17792
19009
  font-weight: 400;
17793
19010
  margin: 0;
19011
+ padding: 10px;
17794
19012
  }
17795
19013
 
17796
19014
  @container (min-width: 640px) {
@@ -17808,9 +19026,7 @@ function rbSmallCategoryImageToutTemplate(spot, config) {
17808
19026
  ${GFONT_CORMORANT}
17809
19027
  ${STYLES$1(spot, config)}
17810
19028
  <div class="${prefix}">
17811
- <div class="${prefix}__text">
17812
- ${spot.header ? `<h2 class="${prefix}__header">${spot.header}</h2>` : ''}
17813
- </div>
19029
+ ${spot.header ? `<h2 class="${prefix}__header">${spot.header}</h2>` : ''}
17814
19030
  </div>
17815
19031
  `;
17816
19032
  }
@@ -17825,7 +19041,6 @@ const STYLES = ({ textColor = '#000000', backgroundColor = 'transparent', primar
17825
19041
  display: flex;
17826
19042
  flex-direction: column;
17827
19043
  border-radius: 5px;
17828
- container-type: inline-size;
17829
19044
  }
17830
19045
 
17831
19046
  .${prefix}__image {
@@ -17958,97 +19173,186 @@ const SPOT_TEMPLATE_HTML_ELEMENT = (spot, config) => {
17958
19173
  // Generate a highly unique prefix to avoid conflicts with other elements.
17959
19174
  const prefix = 's' + UniqueIdGenerator.generate().toLowerCase();
17960
19175
  const spotHtmlString = variantTemplate(spot, { ...config, prefix });
17961
- return spotHtmlStringToElement(spotHtmlString);
19176
+ return spotHtmlStringToElement(spotHtmlString, spot.spot);
17962
19177
  };
17963
19178
 
17964
- /**
17965
- * PubSub class
17966
- * Manages event subscriptions and publications
17967
- * @template IEventMap A record type defining the structure of events and their data
17968
- */
17969
- class PubSub {
19179
+ // For the moment, we will only focus on sites that use Google Analytics,
19180
+ // but we will add support for other analytics tools in the future.
19181
+ var AnalyticsTool;
19182
+ (function (AnalyticsTool) {
19183
+ AnalyticsTool["GoogleAnalytics"] = "google-analytics";
19184
+ AnalyticsTool["Other"] = "Other";
19185
+ })(AnalyticsTool || (AnalyticsTool = {}));
19186
+
19187
+ class DataLayerMonitor {
17970
19188
  constructor() {
17971
- /**
17972
- * Object to store subscribers for each event type
17973
- */
17974
- this.subscribers = {};
19189
+ if (!window.dataLayer) {
19190
+ return;
19191
+ }
19192
+ this.originalPush = window.dataLayer.push;
17975
19193
  }
17976
19194
  static getInstance() {
17977
- if (!PubSub.instance) {
17978
- PubSub.instance = new PubSub();
17979
- }
17980
- return PubSub.instance;
19195
+ if (!DataLayerMonitor.instance) {
19196
+ DataLayerMonitor.instance = new DataLayerMonitor();
19197
+ }
19198
+ return DataLayerMonitor.instance;
19199
+ }
19200
+ setListener(listener) {
19201
+ this.listener = listener;
19202
+ }
19203
+ start() {
19204
+ window.dataLayer.push = (...args) => {
19205
+ const result = this.originalPush.apply(window.dataLayer, args);
19206
+ const pushedEvent = args[0];
19207
+ if (this.listener) {
19208
+ const normalizedData = this.cleanEventData(pushedEvent);
19209
+ if (normalizedData) {
19210
+ this.listener(normalizedData);
19211
+ }
19212
+ }
19213
+ return result;
19214
+ };
17981
19215
  }
17982
- /**
17983
- * Subscribe to an event
17984
- * @param eventType - The type of event to subscribe to
17985
- * @param callback - The function to be called when the event is published
17986
- * @returns A function to unsubscribe from the event
17987
- *
17988
- * @Example:
17989
- * const unsubscribe = pubSub.subscribe('userLogin', (data) => {
17990
- * console.log(`User ${data.username} logged in`);
17991
- * });
17992
- */
17993
- subscribe(eventType, callback) {
17994
- if (!this.subscribers[eventType]) {
17995
- this.subscribers[eventType] = [];
19216
+ cleanEventData(data) {
19217
+ const eventName = getEventTypeFromRawEvent(data.event);
19218
+ if (!eventName) {
19219
+ return null;
17996
19220
  }
17997
- this.subscribers[eventType].push(callback);
17998
- // Return an unsubscribe function
17999
- return () => {
18000
- this.subscribers[eventType] = this.subscribers[eventType].filter((cb) => cb !== callback);
19221
+ const productIds = extractDeepValues(data, 'ids', {
19222
+ onlyFirst: false,
19223
+ shouldIncludeZero: true,
19224
+ });
19225
+ if (Array.isArray(productIds) && productIds.length === 0) {
19226
+ return null;
19227
+ }
19228
+ const normalizedData = {
19229
+ event: eventName,
19230
+ productIds,
18001
19231
  };
19232
+ if (eventName === RMN_SPOT_EVENT.PURCHASE) {
19233
+ const productPrice = extractDeepValues(data, 'price', {
19234
+ onlyFirst: true,
19235
+ shouldIncludeZero: true,
19236
+ });
19237
+ if (productPrice) {
19238
+ normalizedData.productPrice = productPrice;
19239
+ }
19240
+ }
19241
+ return normalizedData;
18002
19242
  }
18003
- /**
18004
- * Publish an event
18005
- * @param eventType - The type of event to publish
18006
- * @param data - The data to be passed to the event subscribers
18007
- *
18008
- * @Example:
18009
- * pubSub.publish('userLogin', { username: 'john_doe', timestamp: Date.now() });
18010
- */
18011
- publish(eventType, data) {
18012
- if (!this.subscribers[eventType]) {
19243
+ stop() {
19244
+ if (this.originalPush) {
19245
+ window.dataLayer.push = this.originalPush;
19246
+ }
19247
+ this.listener = undefined;
19248
+ }
19249
+ }
19250
+
19251
+ // @TODO: Add support for user to push events to our own data layer, if they don't use any analytics tool.
19252
+ // window.rmnDataLayer = window.rmnDataLayer || [];
19253
+ class MonitorService {
19254
+ constructor() {
19255
+ const analyticsTool = this.detectAnalyticsTool();
19256
+ switch (analyticsTool) {
19257
+ case AnalyticsTool.GoogleAnalytics:
19258
+ this.implementedMonitor = DataLayerMonitor.getInstance();
19259
+ break;
19260
+ case AnalyticsTool.Other:
19261
+ default:
19262
+ console.warn('This site uses an unsupported analytics tool.');
19263
+ break;
19264
+ }
19265
+ if (analyticsTool === AnalyticsTool.Other) {
18013
19266
  return;
18014
19267
  }
18015
- this.subscribers[eventType].forEach((callback) => callback(data));
19268
+ this.pubSubService = PubsubService.getInstance();
19269
+ this.localStorageService = LocalStorageService.getInstance();
19270
+ }
19271
+ static getInstance() {
19272
+ if (!MonitorService.instance) {
19273
+ MonitorService.instance = new MonitorService();
19274
+ }
19275
+ return MonitorService.instance;
19276
+ }
19277
+ start() {
19278
+ if (!this.implementedMonitor)
19279
+ return;
19280
+ this.implementedMonitor.setListener(async (eventData) => {
19281
+ var _a;
19282
+ await this.matchAndFireEvent(eventData, (_a = this.localStorageService) === null || _a === void 0 ? void 0 : _a.getSpots());
19283
+ });
19284
+ this.implementedMonitor.start();
19285
+ }
19286
+ async matchAndFireEvent(eventData, spots) {
19287
+ var _a, _b;
19288
+ if (!spots)
19289
+ return;
19290
+ const eventProductIds = new Set(cleanProductIds(eventData.productIds));
19291
+ for (const spot of Object.values(spots)) {
19292
+ if (!spot.productIds.length)
19293
+ continue;
19294
+ const hasCommonProductIds = cleanProductIds(spot.productIds).find((productId) => eventProductIds.has(productId));
19295
+ if (!hasCommonProductIds || !Object.values(RMN_SPOT_EVENT).includes(eventData.event)) {
19296
+ continue;
19297
+ }
19298
+ const eventUrl = (_b = (_a = spot.events.find((event) => event.event === eventData.event)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : '';
19299
+ const additionalQueryParams = objectToQueryParams({
19300
+ override: eventData.productPrice,
19301
+ });
19302
+ await this.fireAndPublishSpotEvent({
19303
+ spotEvent: eventData.event,
19304
+ eventUrl: `${eventUrl}${additionalQueryParams ? `&${additionalQueryParams}` : ''}`,
19305
+ placementId: spot.placementId,
19306
+ spotId: spot.spotId,
19307
+ });
19308
+ }
19309
+ }
19310
+ async fireAndPublishSpotEvent({ spotEvent, eventUrl, placementId, spotId, }) {
19311
+ await fireEvent({
19312
+ event: spotEvent,
19313
+ eventUrl,
19314
+ });
19315
+ if (!this.pubSubService)
19316
+ return;
19317
+ this.pubSubService.publish(RMN_EVENT.SPOT_EVENT, {
19318
+ eventType: spotEvent,
19319
+ placementId,
19320
+ spotId,
19321
+ });
19322
+ }
19323
+ detectAnalyticsTool() {
19324
+ let analyticsTool = AnalyticsTool.Other;
19325
+ // Check for Google Analytics
19326
+ if (typeof window.ga !== 'undefined') {
19327
+ analyticsTool = AnalyticsTool.GoogleAnalytics;
19328
+ }
19329
+ // Check for Google Analytics 4
19330
+ if (typeof window.gtag !== 'undefined') {
19331
+ analyticsTool = AnalyticsTool.GoogleAnalytics;
19332
+ }
19333
+ // Check for Google Tag Manager
19334
+ if (typeof window.google_tag_manager !== 'undefined') {
19335
+ analyticsTool = AnalyticsTool.GoogleAnalytics;
19336
+ }
19337
+ // @TODO: Add support for other analytics tools
19338
+ // Check for Heap Analytics
19339
+ // Check for Mixpanel
19340
+ // Check for Woopra
19341
+ // Check for Segment
19342
+ // Check for Amplitude
19343
+ return analyticsTool;
18016
19344
  }
18017
19345
  }
18018
- /**
18019
- * Usage Example:
18020
- *
18021
- * interface IEventMap {
18022
- * userLogin: { username: string; timestamp: number };
18023
- * pageView: { url: string; timestamp: number };
18024
- * }
18025
- *
18026
- * const pubSub = new PubSub<IEventMap>();
18027
- *
18028
- * // Subscribe to events
18029
- * const unsubscribeLogin = pubSub.subscribe('userLogin', (data) => {
18030
- * console.log(`User ${data.username} logged in at ${new Date(data.timestamp)}`);
18031
- * });
18032
- *
18033
- * pubSub.subscribe('pageView', (data) => {
18034
- * console.log(`Page ${data.url} viewed at ${new Date(data.timestamp)}`);
18035
- * });
18036
- *
18037
- * // Publish events
18038
- * pubSub.publish('userLogin', { username: 'john_doe', timestamp: Date.now() });
18039
- * pubSub.publish('pageView', { url: '/home', timestamp: Date.now() });
18040
- *
18041
- * // Unsubscribe from an event
18042
- * unsubscribeLogin();
18043
- */
18044
19346
 
18045
19347
  class EventService {
18046
19348
  constructor() {
18047
- this.pubSub = PubSub.getInstance();
18048
- this.localStorage = LocalStorage.getInstance();
19349
+ this.pubSubService = PubsubService.getInstance();
19350
+ this.localStorageService = LocalStorageService.getInstance();
18049
19351
  this.activeSpots = new Map();
18050
19352
  this.spotStates = new Map();
18051
19353
  this.intersectionObserver = new IntersectionObserverService();
19354
+ // Start the user monitor, which will track and check user interactions
19355
+ MonitorService.getInstance().start();
18052
19356
  }
18053
19357
  static getInstance() {
18054
19358
  if (!EventService.instance) {
@@ -18057,25 +19361,28 @@ class EventService {
18057
19361
  return EventService.instance;
18058
19362
  }
18059
19363
  subscribe(eventType, callback) {
18060
- return this.pubSub.subscribe(eventType, callback);
19364
+ return this.pubSubService.subscribe(eventType, callback);
18061
19365
  }
18062
19366
  publish(eventType, data) {
18063
- this.pubSub.publish(eventType, data);
19367
+ this.pubSubService.publish(eventType, data);
18064
19368
  }
18065
19369
  registerSpot(params) {
18066
19370
  const { placementId, spot, spotElement } = params;
18067
- this.activeSpots.set(placementId, { spotElement });
18068
- // Fire impression event
18069
- this.fireImpressionEvent(placementId, spot, spotElement);
18070
- // Handle intersection observer
18071
- this.handleIntersectionObserver(placementId, spot, spotElement);
18072
- // Attach click event listener
18073
- spotElement.addEventListener('click', async () => await this.handleClick(params));
19371
+ this.activeSpots.set(spot.id, { placementId, spotElement });
19372
+ // Handle Impression Event
19373
+ this.handleImpressionEvent(placementId, spot);
19374
+ // Handle Click Event
19375
+ spotElement.addEventListener('click', async () => {
19376
+ await this.handleClickEvent(params);
19377
+ });
19378
+ // Handle Intersection Observer
19379
+ this.handleIntersectionObserver(placementId, spotElement);
18074
19380
  }
18075
19381
  unregisterSpot(placementId) {
18076
19382
  const placementIdClean = placementId.replace('#', '');
18077
- const spotData = this.activeSpots.get(placementIdClean);
18078
- if (!spotData) {
19383
+ const spotId = this.getSpotIdByPlacementId(this.activeSpots, placementIdClean);
19384
+ const data = this.activeSpots.get(placementIdClean !== null && placementIdClean !== void 0 ? placementIdClean : '');
19385
+ if (!spotId || !data) {
18079
19386
  this.handleSpotState(placementIdClean, {
18080
19387
  state: {
18081
19388
  error: `Active spot with placementId ${placementIdClean} not found.`,
@@ -18083,7 +19390,7 @@ class EventService {
18083
19390
  });
18084
19391
  return;
18085
19392
  }
18086
- this.intersectionObserver.unobserve(spotData.spotElement);
19393
+ this.intersectionObserver.unobserve(data.spotElement);
18087
19394
  this.handleSpotState(placementIdClean, {
18088
19395
  dom: {
18089
19396
  spotElement: undefined,
@@ -18092,9 +19399,10 @@ class EventService {
18092
19399
  state: {
18093
19400
  unmounted: true,
18094
19401
  mounted: false,
19402
+ loading: false,
18095
19403
  },
18096
19404
  });
18097
- this.activeSpots.delete(placementIdClean);
19405
+ this.activeSpots.delete(spotId);
18098
19406
  const placementElement = document.getElementById(placementIdClean);
18099
19407
  if (!placementElement) {
18100
19408
  this.handleSpotState(placementIdClean, {
@@ -18141,128 +19449,77 @@ class EventService {
18141
19449
  },
18142
19450
  };
18143
19451
  }
18144
- this.spotStates.set(placementId, { ...currentState, ...updates });
19452
+ const merged = this.deepMerge(currentState, updates);
19453
+ this.spotStates.set(placementId, merged);
18145
19454
  if (publish) {
18146
- this.pubSub.publish(RMN_SPOT_EVENT.LIFECYCLE_STATE, this.spotStates.get(placementId));
19455
+ this.pubSubService.publish(RMN_EVENT.LIFECYCLE_STATE, this.spotStates.get(placementId));
18147
19456
  }
18148
19457
  }
18149
- async handleClick({ placementId, spot, spotElement, }) {
19458
+ async handleClickEvent({ placementId, spot }) {
18150
19459
  var _a, _b, _c;
18151
- // Publish click event
18152
- this.pubSub.publish(RMN_SPOT_EVENT.CLICK, {
19460
+ this.pubSubService.publish(RMN_EVENT.SPOT_EVENT, {
19461
+ eventType: RMN_SPOT_EVENT.CLICK,
18153
19462
  placementId,
18154
19463
  spotId: spot.id,
18155
- spotElement,
18156
19464
  });
18157
- // Fire click event
18158
- await this.fireEvent({
19465
+ await fireEvent({
18159
19466
  event: RMN_SPOT_EVENT.CLICK,
18160
19467
  eventUrl: (_b = (_a = spot.events.find((event) => event.event === RMN_SPOT_EVENT.CLICK)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : '',
18161
19468
  });
18162
19469
  // Save spot to local storage for event tracking
18163
- this.localStorage.setSpot(spot.id, {
19470
+ this.localStorageService.setSpot(spot.id, {
19471
+ placementId,
18164
19472
  spotId: spot.id,
18165
19473
  spotType: spot.spot,
18166
19474
  events: spot.events,
18167
- productIds: (_c = spot.productIds) !== null && _c !== void 0 ? _c : [1, 'GROUPING-12345', 'DAN-12345', 131398103],
19475
+ productIds: (_c = spot.productIds) !== null && _c !== void 0 ? _c : [],
18168
19476
  });
18169
19477
  }
18170
- handleIntersectionObserver(placementId, _spot, spotElement) {
18171
- const spotIsVisibleCallback = async () => {
18172
- this.intersectionObserver.unobserve(spotElement);
18173
- this.handleSpotState(placementId, {
18174
- dom: {
18175
- spotElement,
18176
- visibleOnViewport: true,
18177
- },
18178
- });
18179
- };
18180
- this.intersectionObserver.observe(spotElement, spotIsVisibleCallback);
18181
- }
18182
- fireImpressionEvent(placementId, spot, spotElement) {
18183
- this.pubSub.publish(RMN_SPOT_EVENT.IMPRESSION, {
19478
+ handleImpressionEvent(placementId, spot) {
19479
+ this.pubSubService.publish(RMN_EVENT.SPOT_EVENT, {
19480
+ eventType: RMN_SPOT_EVENT.IMPRESSION,
18184
19481
  placementId,
18185
19482
  spotId: spot.id,
18186
- spotElement,
18187
19483
  });
18188
19484
  (async () => {
18189
19485
  var _a, _b;
18190
- await this.fireEvent({
19486
+ await fireEvent({
18191
19487
  event: RMN_SPOT_EVENT.IMPRESSION,
18192
19488
  eventUrl: (_b = (_a = spot.events.find((event) => event.event === RMN_SPOT_EVENT.IMPRESSION)) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : '',
18193
19489
  });
18194
19490
  })();
18195
19491
  }
18196
- /**
18197
- * Fires an event using the navigator.sendBeacon method or a fallback method if sendBeacon is not available.
18198
- * If the event is a click event and a redirect URL is found, it redirects the user to that URL.
18199
- *
18200
- * @param {IFireEventParams} params - The parameters for firing the event.
18201
- * @param {RMN_SPOT_EVENT} params.event - The event type.
18202
- * @param {string} params.eventUrl - The URL to which the event is sent.
18203
- * @returns {Promise<void>} - A promise that resolves when the event is fired.
18204
- */
18205
- async fireEvent({ event, eventUrl }) {
18206
- var _a;
18207
- try {
18208
- const didFireEvent = ((_a = navigator === null || navigator === void 0 ? void 0 : navigator.sendBeacon) === null || _a === void 0 ? void 0 : _a.call(navigator, eventUrl)) || (await this.fallbackEventFire(eventUrl));
18209
- if (!didFireEvent) {
18210
- return;
18211
- }
18212
- if (event === RMN_SPOT_EVENT.CLICK) {
18213
- const redirectUrl = this.getRedirectUrlFromPayload(eventUrl);
18214
- if (redirectUrl) {
18215
- window.location.href = redirectUrl;
18216
- }
19492
+ getSpotIdByPlacementId(activeSpots, searchPlacementId) {
19493
+ for (const [spotId, spotData] of activeSpots) {
19494
+ if (spotData.placementId === searchPlacementId) {
19495
+ return spotId;
18217
19496
  }
18218
19497
  }
18219
- catch (_b) {
18220
- // Handle errors silently
18221
- }
19498
+ return null;
18222
19499
  }
18223
- // Fallback method using fetch if sendBeacon isn't available
18224
- async fallbackEventFire(url) {
18225
- try {
18226
- const racePromise = Promise.race([
18227
- // Promise #1: The fetch request
18228
- fetch(url, {
18229
- method: 'POST',
18230
- keepalive: true,
18231
- }),
18232
- // Promise #2: The timeout
18233
- new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), 2000)),
18234
- ]);
18235
- /**
18236
- * Prevent requests from hanging indefinitely
18237
- * Improve user experience by failing fast
18238
- * Handle slow network conditions gracefully
18239
- * Ensure resources are freed up in a timely manner
18240
- */
18241
- const response = await racePromise;
18242
- return response.ok;
18243
- }
18244
- catch (_a) {
18245
- return false;
18246
- }
19500
+ deepMerge(current, updates) {
19501
+ return {
19502
+ identifier: updates.identifier
19503
+ ? { ...current.identifier, ...updates.identifier }
19504
+ : current.identifier,
19505
+ dom: updates.dom ? { ...current.dom, ...updates.dom } : current.dom,
19506
+ state: updates.state ? { ...current.state, ...updates.state } : current.state,
19507
+ displayConfig: updates.displayConfig
19508
+ ? { ...current.displayConfig, ...updates.displayConfig }
19509
+ : current.displayConfig,
19510
+ };
18247
19511
  }
18248
- /**
18249
- * Extracts and decodes a URL from a base64-encoded query parameter.
18250
- *
18251
- * @param {string} url - The URL containing the base64-encoded query parameter.
18252
- * @returns {string | null} - The decoded URL or null if not found or invalid.
18253
- */
18254
- getRedirectUrlFromPayload(url) {
18255
- try {
18256
- const base64String = new URL(url).searchParams.get('e');
18257
- if (!base64String) {
18258
- return null;
18259
- }
18260
- const data = JSON.parse(atob(base64String));
18261
- return data.ur || null;
18262
- }
18263
- catch (_a) {
18264
- return null;
18265
- }
19512
+ handleIntersectionObserver(placementId, spotElement) {
19513
+ const spotIsVisibleCallback = async () => {
19514
+ this.intersectionObserver.unobserve(spotElement);
19515
+ this.handleSpotState(placementId, {
19516
+ dom: {
19517
+ spotElement,
19518
+ visibleOnViewport: true,
19519
+ },
19520
+ });
19521
+ };
19522
+ this.intersectionObserver.observe(spotElement, spotIsVisibleCallback);
18266
19523
  }
18267
19524
  }
18268
19525
 
@@ -18283,6 +19540,9 @@ class SelectionService extends BaseApi {
18283
19540
  * @return {Promise<ISpots | { error: string }>} - The spots response object.
18284
19541
  */
18285
19542
  async spotSelection(data) {
19543
+ if (data.userId === undefined) {
19544
+ data.userId = this.getUserId();
19545
+ }
18286
19546
  const { isOk, val, isErr } = await this.post(SELECTION_API_PATH, data, {});
18287
19547
  if (isErr) {
18288
19548
  return { error: `There was an error during spot selection: (${isErr === null || isErr === void 0 ? void 0 : isErr.errorMessage})` };
@@ -18294,6 +19554,14 @@ class SelectionService extends BaseApi {
18294
19554
  }
18295
19555
  return { error: 'Spot selection response was not successful' };
18296
19556
  }
19557
+ getUserId() {
19558
+ const isWeb = typeof window !== 'undefined';
19559
+ if (isWeb) {
19560
+ const localStorageService = LocalStorageService.getInstance();
19561
+ return localStorageService.getUserId();
19562
+ }
19563
+ return undefined;
19564
+ }
18297
19565
  }
18298
19566
 
18299
19567
  const SPOT_EVENTS_EXAMPLE = [
@@ -18322,80 +19590,278 @@ const SPOT_EVENTS_EXAMPLE = [
18322
19590
  url: 'https://dev.rmn.liquidcommerce.cloud/api/spots/event?e=eyJ2IjoiMS4xMiIsImF2IjozMDY1NzgzLCJhdCI6MTYzLCJidCI6MCwiY20iOjQ0MDE5MjQxOCwiY2giOjYzMTg0LCJjayI6e30sImNyIjo0ODE4NTUzNzUsImRpIjoiOWMxNGFhMGI3NWY4NDMxNTllMTAwYWQzNzA1NzQyYzMiLCJkaiI6MCwiaWkiOiIxZjU0MGM5NmQ1N2M0YmZjODFlZjRkNjhkMzFjNDVkOSIsImRtIjozLCJmYyI6NjU2NjgyNTQ5LCJmbCI6NjQzOTMxODIxLCJpcCI6IjM1LjIyMy4xOTguOTUiLCJudyI6MTE1MDAsInBjIjo1MDAwLCJvcCI6NTAwMCwibXAiOjUwMDAsImVjIjowLCJnbSI6MCwiZXAiOm51bGwsInByIjoyNDkzMTYsInJ0IjoxLCJycyI6NTAwLCJzYSI6IjU1Iiwic2IiOiJpLTA0MDI0ODg4ZDlkMWRjZWQ3Iiwic3AiOjI3MjI3Miwic3QiOjEyODcyOTYsInRyIjp0cnVlLCJ1ayI6IjNhZWRhMWMxLTZhYjItNDUwNy04Mzg5LTEwZTJmNDMxYjM5MSIsInRzIjoxNzI5MzU5MDU0OTI3LCJiZiI6dHJ1ZSwicG4iOiJyYlByb2R1Y3RVcGNzIiwiZ2MiOnRydWUsImdDIjp0cnVlLCJncyI6Im5vbmUiLCJkYyI6MSwidHoiOiJBbWVyaWNhL05ld19Zb3JrIiwiZXQiOjY5fQ&s=l6MOscQC-q-FkC2Ksd7w6jjySCQ',
18323
19591
  },
18324
19592
  ];
18325
- const RB_SPOTS_SELECTION_EXAMPLE = {
19593
+ ({
19594
+ rbHomepageHero: [
19595
+ {
19596
+ id: 'abc123',
19597
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
19598
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
19599
+ width: 1140,
19600
+ height: 640,
19601
+ header: 'Premium Wine Collection',
19602
+ description: 'Discover our exclusive selection of vintage wines',
19603
+ ctaText: 'Shop Wines',
19604
+ textColor: '#ffffff',
19605
+ ctaTextColor: '#ffffff',
19606
+ primaryImage: 'https://placehold.co/1140x640/png?text=Wine+Collection',
19607
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Wine',
19608
+ events: SPOT_EVENTS_EXAMPLE,
19609
+ productIds: [1, 2, 3],
19610
+ },
19611
+ {
19612
+ id: 'jkl012',
19613
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19614
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19615
+ width: 1140,
19616
+ height: 640,
19617
+ header: 'Whiskey Collection',
19618
+ description: 'Premium whiskeys from around the world',
19619
+ ctaText: 'Shop Whiskeys',
19620
+ textColor: '#ffffff',
19621
+ backgroundColor: '#2c1810',
19622
+ ctaTextColor: '#2c1810',
19623
+ primaryImage: 'https://placehold.co/1140x640/png?text=Whiskey',
19624
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Whiskey',
19625
+ events: SPOT_EVENTS_EXAMPLE,
19626
+ productIds: [10, 11],
19627
+ },
19628
+ {
19629
+ id: 'stu901',
19630
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19631
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19632
+ width: 1140,
19633
+ height: 640,
19634
+ header: 'Summer Cocktails',
19635
+ description: 'Essential spirits for summer mixing',
19636
+ ctaText: 'Mix It Up',
19637
+ textColor: '#ffffff',
19638
+ backgroundColor: '#4d3a0a',
19639
+ ctaTextColor: '#4d3a0a',
19640
+ primaryImage: 'https://placehold.co/1140x640/png?text=Cocktails',
19641
+ secondaryImage: 'https://placehold.co/1140x640/png?text=Mixers',
19642
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Cocktails',
19643
+ mobileSecondaryImage: 'https://placehold.co/640x320/png?text=Mobile+Mixers',
19644
+ events: SPOT_EVENTS_EXAMPLE,
19645
+ productIds: [16, 17, 18],
19646
+ },
19647
+ {
19648
+ id: 'def456',
19649
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
19650
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
19651
+ width: 1140,
19652
+ height: 640,
19653
+ header: 'Craft Beer Festival',
19654
+ description: 'Local breweries and exclusive releases',
19655
+ ctaText: 'Explore Beers',
19656
+ textColor: '#ffffff',
19657
+ ctaTextColor: '#ffffff',
19658
+ primaryImage: 'https://placehold.co/1140x640/png?text=Beer+Festival',
19659
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Beer',
19660
+ events: SPOT_EVENTS_EXAMPLE,
19661
+ productIds: [4, 5, 6],
19662
+ },
19663
+ {
19664
+ id: 'mno345',
19665
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19666
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19667
+ width: 1140,
19668
+ height: 640,
19669
+ header: 'Champagne Selection',
19670
+ description: 'Finest champagnes for every occasion',
19671
+ ctaText: 'View Champagnes',
19672
+ textColor: '#ffffff',
19673
+ backgroundColor: '#1a1a1a',
19674
+ ctaTextColor: '#1a1a1a',
19675
+ primaryImage: 'https://placehold.co/1140x640/png?text=Champagne',
19676
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Champagne',
19677
+ events: SPOT_EVENTS_EXAMPLE,
19678
+ productIds: [12, 13],
19679
+ },
19680
+ {
19681
+ id: 'vwx234',
19682
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19683
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19684
+ width: 1140,
19685
+ height: 640,
19686
+ header: 'Wine Regions',
19687
+ description: 'Explore wines from top regions',
19688
+ ctaText: 'Tour Regions',
19689
+ textColor: '#ffffff',
19690
+ backgroundColor: '#4d0a0a',
19691
+ ctaTextColor: '#4d0a0a',
19692
+ primaryImage: 'https://placehold.co/1140x640/png?text=Wine+Regions',
19693
+ secondaryImage: 'https://placehold.co/1140x640/png?text=Vineyards',
19694
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Regions',
19695
+ mobileSecondaryImage: 'https://placehold.co/640x320/png?text=Mobile+Vineyards',
19696
+ events: SPOT_EVENTS_EXAMPLE,
19697
+ productIds: [19, 20, 21],
19698
+ },
19699
+ {
19700
+ id: 'ghi789',
19701
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
19702
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
19703
+ width: 1140,
19704
+ height: 640,
19705
+ header: 'Rare Spirits',
19706
+ description: 'Limited edition spirits collection',
19707
+ ctaText: 'View Collection',
19708
+ textColor: '#ffffff',
19709
+ ctaTextColor: '#ffffff',
19710
+ primaryImage: 'https://placehold.co/1140x640/png?text=Rare+Spirits',
19711
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Spirits',
19712
+ events: SPOT_EVENTS_EXAMPLE,
19713
+ productIds: [7, 8, 9],
19714
+ },
19715
+ {
19716
+ id: 'pqr678',
19717
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19718
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19719
+ width: 1140,
19720
+ height: 640,
19721
+ header: 'Gin Collection',
19722
+ description: 'Artisanal gins and botanicals',
19723
+ ctaText: 'Explore Gins',
19724
+ textColor: '#ffffff',
19725
+ backgroundColor: '#0a4d4d',
19726
+ ctaTextColor: '#0a4d4d',
19727
+ primaryImage: 'https://placehold.co/1140x640/png?text=Gin',
19728
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Gin',
19729
+ events: SPOT_EVENTS_EXAMPLE,
19730
+ productIds: [14, 15],
19731
+ },
19732
+ {
19733
+ id: 'yz5678',
19734
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19735
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19736
+ width: 1140,
19737
+ height: 640,
19738
+ header: 'Tequila Collection',
19739
+ description: 'Premium tequilas and mezcals',
19740
+ ctaText: 'Shop Tequila',
19741
+ textColor: '#ffffff',
19742
+ backgroundColor: '#0a4d2b',
19743
+ ctaTextColor: '#0a4d2b',
19744
+ primaryImage: 'https://placehold.co/1140x640/png?text=Tequila',
19745
+ secondaryImage: 'https://placehold.co/1140x640/png?text=Mezcal',
19746
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Tequila',
19747
+ mobileSecondaryImage: 'https://placehold.co/640x320/png?text=Mobile+Mezcal',
19748
+ events: SPOT_EVENTS_EXAMPLE,
19749
+ productIds: [22, 23, 24],
19750
+ },
19751
+ ],
18326
19752
  rbHomepageHeroFullImage: [
18327
19753
  {
18328
- id: '111111_111111',
19754
+ id: 'hjk567',
18329
19755
  spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
18330
19756
  variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
18331
19757
  width: 1140,
18332
19758
  height: 640,
18333
- header: 'Artisanal Craft Beer Collection',
18334
- description: 'Discover our curated selection of small-batch, flavor-packed craft beers.',
18335
- ctaText: 'Explore the Collection',
19759
+ header: 'Holiday Gift Guide',
19760
+ description: 'Perfect spirits for every occasion',
19761
+ ctaText: 'Shop Gifts',
18336
19762
  textColor: '#ffffff',
18337
19763
  ctaTextColor: '#ffffff',
18338
- primaryImage: 'https://placehold.co/1140x640/png?text=Craft+Beer+Collection',
18339
- mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Craft+Beer',
19764
+ primaryImage: 'https://placehold.co/1140x640/png?text=Gift+Guide',
19765
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Gifts',
18340
19766
  events: SPOT_EVENTS_EXAMPLE,
19767
+ productIds: [25, 26],
18341
19768
  },
18342
19769
  {
18343
- id: '222222_222222',
19770
+ id: 'qwe890',
18344
19771
  spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
18345
19772
  variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_FULL_IMAGE,
18346
19773
  width: 1140,
18347
19774
  height: 640,
18348
- header: 'Summer Wine Spectacular',
18349
- description: 'Refresh your palate with our handpicked selection of crisp, summer wines.',
18350
- ctaText: 'Shop Summer Wines',
18351
- textColor: '#000000',
19775
+ header: 'Summer Wine Festival',
19776
+ description: 'Refreshing wines for summer',
19777
+ ctaText: 'Shop Festival',
19778
+ textColor: '#ffffff',
18352
19779
  ctaTextColor: '#ffffff',
18353
- primaryImage: 'https://placehold.co/1140x640/png?text=Summer+Wines',
18354
- mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Summer+Wines',
19780
+ primaryImage: 'https://placehold.co/1140x640/png?text=Wine+Festival',
19781
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Festival',
18355
19782
  events: SPOT_EVENTS_EXAMPLE,
19783
+ productIds: [27, 28],
18356
19784
  },
18357
19785
  ],
18358
19786
  rbHomepageHeroTwoTile: [
18359
19787
  {
18360
- id: '333333_333333',
19788
+ id: 'iop987',
18361
19789
  spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
18362
19790
  variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
18363
19791
  width: 1140,
18364
19792
  height: 640,
18365
- header: 'Whiskey Wonderland',
18366
- description: 'Embark on a journey through our premium whiskey selection.',
18367
- ctaText: 'Discover Whiskeys',
19793
+ header: 'Bourbon Selection',
19794
+ description: "Kentucky's finest bourbons",
19795
+ ctaText: 'Shop Bourbon',
18368
19796
  textColor: '#ffffff',
18369
- backgroundColor: '#2c1a05',
18370
- ctaTextColor: '#2c1a05',
18371
- primaryImage: 'https://placehold.co/1140x640/png?text=Whiskey+Collection',
18372
- mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Whiskey',
19797
+ backgroundColor: '#2c1810',
19798
+ ctaTextColor: '#2c1810',
19799
+ primaryImage: 'https://placehold.co/1140x640/png?text=Bourbon',
19800
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Bourbon',
19801
+ events: SPOT_EVENTS_EXAMPLE,
19802
+ productIds: [29, 30],
19803
+ },
19804
+ {
19805
+ id: 'lkj012',
19806
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19807
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_TWO_TILE,
19808
+ width: 1140,
19809
+ height: 640,
19810
+ header: 'Vodka Collection',
19811
+ description: 'Premium vodkas from around the world',
19812
+ ctaText: 'Shop Vodka',
19813
+ textColor: '#ffffff',
19814
+ backgroundColor: '#1a1a1a',
19815
+ ctaTextColor: '#1a1a1a',
19816
+ primaryImage: 'https://placehold.co/1140x640/png?text=Vodka',
19817
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Vodka',
18373
19818
  events: SPOT_EVENTS_EXAMPLE,
19819
+ productIds: [31, 32],
18374
19820
  },
18375
19821
  ],
18376
19822
  rbHomepageHeroThreeTile: [
18377
19823
  {
18378
- id: '444444_444444',
19824
+ id: 'bnm345',
19825
+ spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19826
+ variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
19827
+ width: 1140,
19828
+ height: 640,
19829
+ header: 'Rum Collection',
19830
+ description: 'Caribbean and craft rums',
19831
+ ctaText: 'Shop Rum',
19832
+ textColor: '#ffffff',
19833
+ backgroundColor: '#4d3a0a',
19834
+ ctaTextColor: '#4d3a0a',
19835
+ primaryImage: 'https://placehold.co/1140x640/png?text=Rum',
19836
+ secondaryImage: 'https://placehold.co/1140x640/png?text=Craft+Rum',
19837
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Rum',
19838
+ mobileSecondaryImage: 'https://placehold.co/640x320/png?text=Mobile+Craft',
19839
+ events: SPOT_EVENTS_EXAMPLE,
19840
+ productIds: [33, 34],
19841
+ },
19842
+ {
19843
+ id: 'fgh678',
18379
19844
  spot: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
18380
19845
  variant: RMN_SPOT_TYPE.RB_HOMEPAGE_HERO_THREE_TILE,
18381
19846
  width: 1140,
18382
19847
  height: 640,
18383
- header: 'Cocktail Essentials',
18384
- description: 'Stock your bar with premium spirits and mixers for the perfect cocktail.',
18385
- ctaText: 'Build Your Bar',
19848
+ header: 'Scotch Selection',
19849
+ description: 'Single malts and blends',
19850
+ ctaText: 'Shop Scotch',
18386
19851
  textColor: '#ffffff',
18387
- backgroundColor: '#1a3c4d',
18388
- ctaTextColor: '#1a3c4d',
18389
- primaryImage: 'https://placehold.co/1140x640/png?text=Cocktail+Spirits',
18390
- secondaryImage: 'https://placehold.co/1140x640/png?text=Cocktail+Mixers',
18391
- mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Cocktail+Kit',
18392
- mobileSecondaryImage: 'https://placehold.co/640x320/png?text=Mobile+Cocktail+Mixers',
19852
+ backgroundColor: '#4d0a0a',
19853
+ ctaTextColor: '#4d0a0a',
19854
+ primaryImage: 'https://placehold.co/1140x640/png?text=Scotch',
19855
+ secondaryImage: 'https://placehold.co/1140x640/png?text=Single+Malts',
19856
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Scotch',
19857
+ mobileSecondaryImage: 'https://placehold.co/640x320/png?text=Mobile+Malts',
18393
19858
  events: SPOT_EVENTS_EXAMPLE,
19859
+ productIds: [35, 36],
18394
19860
  },
18395
19861
  ],
18396
19862
  rbLargeCategoryImageTout: [
18397
19863
  {
18398
- id: '555555_555555',
19864
+ id: 'cde567',
18399
19865
  spot: RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT,
18400
19866
  variant: RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT,
18401
19867
  width: 468,
@@ -18408,11 +19874,44 @@ const RB_SPOTS_SELECTION_EXAMPLE = {
18408
19874
  mobilePrimaryImage: 'https://placehold.co/468x410/png?text=Mobile+Rare+Spirits',
18409
19875
  ctaText: 'Shop Rare Spirits',
18410
19876
  events: SPOT_EVENTS_EXAMPLE,
19877
+ productIds: [37, 38, 39, 40, 41],
19878
+ },
19879
+ {
19880
+ id: 'efg789',
19881
+ spot: RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT,
19882
+ variant: RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT,
19883
+ width: 468,
19884
+ height: 410,
19885
+ header: 'Vintage Champagnes',
19886
+ description: 'Explore our prestigious collection of aged champagnes.',
19887
+ textColor: '#ffffff',
19888
+ ctaTextColor: '#ffffff',
19889
+ primaryImage: 'https://placehold.co/468x410/png?text=Vintage+Champagne',
19890
+ mobilePrimaryImage: 'https://placehold.co/468x410/png?text=Mobile+Champagne',
19891
+ ctaText: 'View Collection',
19892
+ events: SPOT_EVENTS_EXAMPLE,
19893
+ productIds: [42, 43, 44, 45, 46],
19894
+ },
19895
+ {
19896
+ id: 'ghi012',
19897
+ spot: RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT,
19898
+ variant: RMN_SPOT_TYPE.RB_LARGE_CATEGORY_IMAGE_TOUT,
19899
+ width: 468,
19900
+ height: 410,
19901
+ header: 'Small Batch Bourbon',
19902
+ description: 'Hand-selected premium bourbon from craft distilleries.',
19903
+ textColor: '#ffffff',
19904
+ ctaTextColor: '#ffffff',
19905
+ primaryImage: 'https://placehold.co/468x410/png?text=Craft+Bourbon',
19906
+ mobilePrimaryImage: 'https://placehold.co/468x410/png?text=Mobile+Bourbon',
19907
+ ctaText: 'Explore Bourbon',
19908
+ events: SPOT_EVENTS_EXAMPLE,
19909
+ productIds: [47, 48, 49, 50, 51],
18411
19910
  },
18412
19911
  ],
18413
19912
  rbSmallDiscoverTout: [
18414
19913
  {
18415
- id: '666666_666666',
19914
+ id: 'jkl345',
18416
19915
  spot: RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT,
18417
19916
  variant: RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT,
18418
19917
  width: 224,
@@ -18422,11 +19921,38 @@ const RB_SPOTS_SELECTION_EXAMPLE = {
18422
19921
  primaryImage: 'https://placehold.co/224x378/png?text=Château+Margaux',
18423
19922
  mobilePrimaryImage: 'https://placehold.co/224x378/png?text=Mobile+Château+Margaux',
18424
19923
  events: SPOT_EVENTS_EXAMPLE,
19924
+ productIds: [52, 53, 54, 55, 56],
19925
+ },
19926
+ {
19927
+ id: 'lmn678',
19928
+ spot: RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT,
19929
+ variant: RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT,
19930
+ width: 224,
19931
+ height: 378,
19932
+ header: 'Macallan 25 Year',
19933
+ textColor: '#ffffff',
19934
+ primaryImage: 'https://placehold.co/224x378/png?text=Macallan+25',
19935
+ mobilePrimaryImage: 'https://placehold.co/224x378/png?text=Mobile+Macallan',
19936
+ events: SPOT_EVENTS_EXAMPLE,
19937
+ productIds: [57, 58, 59, 60, 61],
19938
+ },
19939
+ {
19940
+ id: 'nop901',
19941
+ spot: RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT,
19942
+ variant: RMN_SPOT_TYPE.RB_SMALL_DISCOVER_TOUT,
19943
+ width: 224,
19944
+ height: 378,
19945
+ header: 'Louis XIII Cognac',
19946
+ textColor: '#ffffff',
19947
+ primaryImage: 'https://placehold.co/224x378/png?text=Louis+XIII',
19948
+ mobilePrimaryImage: 'https://placehold.co/224x378/png?text=Mobile+Louis+XIII',
19949
+ events: SPOT_EVENTS_EXAMPLE,
19950
+ productIds: [62, 63, 64, 65, 66],
18425
19951
  },
18426
19952
  ],
18427
19953
  rbSmallCategoryImageTout: [
18428
19954
  {
18429
- id: '777777_777777',
19955
+ id: 'qrs234',
18430
19956
  spot: RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT,
18431
19957
  variant: RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT,
18432
19958
  width: 224,
@@ -18436,11 +19962,38 @@ const RB_SPOTS_SELECTION_EXAMPLE = {
18436
19962
  primaryImage: 'https://placehold.co/224x410/png?text=Japanese+Sake',
18437
19963
  mobilePrimaryImage: 'https://placehold.co/224x410/png?text=Mobile+Japanese+Sake',
18438
19964
  events: SPOT_EVENTS_EXAMPLE,
19965
+ productIds: [67, 68, 69, 70, 71],
19966
+ },
19967
+ {
19968
+ id: 'stu567',
19969
+ spot: RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT,
19970
+ variant: RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT,
19971
+ width: 224,
19972
+ height: 410,
19973
+ header: 'Craft Vermouth',
19974
+ textColor: '#ffffff',
19975
+ primaryImage: 'https://placehold.co/224x410/png?text=Craft+Vermouth',
19976
+ mobilePrimaryImage: 'https://placehold.co/224x410/png?text=Mobile+Vermouth',
19977
+ events: SPOT_EVENTS_EXAMPLE,
19978
+ productIds: [72, 73, 74, 75, 76],
19979
+ },
19980
+ {
19981
+ id: 'vwx890',
19982
+ spot: RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT,
19983
+ variant: RMN_SPOT_TYPE.RB_SMALL_CATEGORY_IMAGE_TOUT,
19984
+ width: 224,
19985
+ height: 410,
19986
+ header: 'Premium Mezcal',
19987
+ textColor: '#ffffff',
19988
+ primaryImage: 'https://placehold.co/224x410/png?text=Premium+Mezcal',
19989
+ mobilePrimaryImage: 'https://placehold.co/224x410/png?text=Mobile+Mezcal',
19990
+ events: SPOT_EVENTS_EXAMPLE,
19991
+ productIds: [77, 78, 79, 80, 81],
18439
19992
  },
18440
19993
  ],
18441
19994
  rbCollectionBannerWithoutTextBlock: [
18442
19995
  {
18443
- id: '888888_888888',
19996
+ id: 'yz1234',
18444
19997
  spot: RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK,
18445
19998
  variant: RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK,
18446
19999
  width: 887,
@@ -18448,11 +20001,34 @@ const RB_SPOTS_SELECTION_EXAMPLE = {
18448
20001
  primaryImage: 'https://placehold.co/887x344/png?text=Summer+Cocktails',
18449
20002
  mobilePrimaryImage: 'https://placehold.co/887x344/png?text=Mobile+Summer+Cocktails',
18450
20003
  events: SPOT_EVENTS_EXAMPLE,
20004
+ productIds: [82, 83, 84, 85, 86],
20005
+ },
20006
+ {
20007
+ id: 'abc567',
20008
+ spot: RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK,
20009
+ variant: RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK,
20010
+ width: 887,
20011
+ height: 344,
20012
+ primaryImage: 'https://placehold.co/887x344/png?text=Holiday+Spirits',
20013
+ mobilePrimaryImage: 'https://placehold.co/887x344/png?text=Mobile+Holiday+Spirits',
20014
+ events: SPOT_EVENTS_EXAMPLE,
20015
+ productIds: [87, 88, 89, 90, 91],
20016
+ },
20017
+ {
20018
+ id: 'def901',
20019
+ spot: RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK,
20020
+ variant: RMN_SPOT_TYPE.RB_COLLECTION_BANNER_WITHOUT_TEXT_BLOCK,
20021
+ width: 887,
20022
+ height: 344,
20023
+ primaryImage: 'https://placehold.co/887x344/png?text=Wine+Essentials',
20024
+ mobilePrimaryImage: 'https://placehold.co/887x344/png?text=Mobile+Wine+Essentials',
20025
+ events: SPOT_EVENTS_EXAMPLE,
20026
+ productIds: [92, 93, 94, 95, 96],
18451
20027
  },
18452
20028
  ],
18453
20029
  rbNavigationBanner: [
18454
20030
  {
18455
- id: '999999_999999',
20031
+ id: 'ghi234',
18456
20032
  spot: RMN_SPOT_TYPE.RB_NAVIGATION_BANNER,
18457
20033
  variant: RMN_SPOT_TYPE.RB_NAVIGATION_BANNER,
18458
20034
  width: 440,
@@ -18462,15 +20038,227 @@ const RB_SPOTS_SELECTION_EXAMPLE = {
18462
20038
  primaryImage: 'https://placehold.co/440x220/png?text=Tequila+Collection',
18463
20039
  mobilePrimaryImage: 'https://placehold.co/440x220/png?text=Mobile+Tequila+Collection',
18464
20040
  events: SPOT_EVENTS_EXAMPLE,
20041
+ productIds: [97, 98, 99, 100, 101],
20042
+ },
20043
+ {
20044
+ id: 'jkl678',
20045
+ spot: RMN_SPOT_TYPE.RB_NAVIGATION_BANNER,
20046
+ variant: RMN_SPOT_TYPE.RB_NAVIGATION_BANNER,
20047
+ width: 440,
20048
+ height: 220,
20049
+ header: 'Craft Gin Selection',
20050
+ textColor: '#ffffff',
20051
+ primaryImage: 'https://placehold.co/440x220/png?text=Gin+Selection',
20052
+ mobilePrimaryImage: 'https://placehold.co/440x220/png?text=Mobile+Gin+Selection',
20053
+ events: SPOT_EVENTS_EXAMPLE,
20054
+ productIds: [102, 103, 104, 105, 106],
20055
+ },
20056
+ {
20057
+ id: 'mno012',
20058
+ spot: RMN_SPOT_TYPE.RB_NAVIGATION_BANNER,
20059
+ variant: RMN_SPOT_TYPE.RB_NAVIGATION_BANNER,
20060
+ width: 440,
20061
+ height: 220,
20062
+ header: 'Premium Vodka',
20063
+ textColor: '#ffffff',
20064
+ primaryImage: 'https://placehold.co/440x220/png?text=Vodka+Premium',
20065
+ mobilePrimaryImage: 'https://placehold.co/440x220/png?text=Mobile+Vodka+Premium',
20066
+ events: SPOT_EVENTS_EXAMPLE,
20067
+ productIds: [107, 108, 109, 110, 111],
18465
20068
  },
18466
20069
  ],
18467
- };
20070
+ });
20071
+ ({
20072
+ banner: [],
20073
+ billboard: [
20074
+ {
20075
+ id: 'kol567',
20076
+ spot: RMN_SPOT_TYPE.BILLBOARD,
20077
+ variant: `${RMN_SPOT_TYPE.BILLBOARD}V2`,
20078
+ width: 1140,
20079
+ height: 640,
20080
+ header: 'Holiday Gift Guide',
20081
+ description: 'Perfect spirits for every occasion',
20082
+ ctaText: 'Shop Gifts',
20083
+ textColor: '#ffffff',
20084
+ ctaTextColor: '#ffffff',
20085
+ primaryImage: 'https://placehold.co/1140x640/png?text=Gift+Guide',
20086
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Gifts',
20087
+ events: SPOT_EVENTS_EXAMPLE,
20088
+ productIds: [25, 26],
20089
+ },
20090
+ {
20091
+ id: 'hpm390',
20092
+ spot: RMN_SPOT_TYPE.BILLBOARD,
20093
+ variant: `${RMN_SPOT_TYPE.BILLBOARD}V2`,
20094
+ width: 1140,
20095
+ height: 640,
20096
+ header: 'Summer Wine Festival',
20097
+ description: 'Refreshing wines for summer',
20098
+ ctaText: 'Shop Festival',
20099
+ textColor: '#ffffff',
20100
+ ctaTextColor: '#ffffff',
20101
+ primaryImage: 'https://placehold.co/1140x640/png?text=Wine+Festival',
20102
+ mobilePrimaryImage: 'https://placehold.co/640x640/png?text=Mobile+Festival',
20103
+ events: SPOT_EVENTS_EXAMPLE,
20104
+ productIds: [27, 28],
20105
+ },
20106
+ ],
20107
+ button2: [],
20108
+ featurePhoneLargeBanner: [],
20109
+ featurePhoneMediumBanner: [],
20110
+ featurePhoneSmallBanner: [],
20111
+ halfPage: [],
20112
+ inText: [],
20113
+ largeLeaderboard: [],
20114
+ largeRectangle: [],
20115
+ leaderboard: [],
20116
+ mediumRectangle: [],
20117
+ microBar: [],
20118
+ mobilePhoneInterstitial1: [],
20119
+ mobilePhoneInterstitial2: [],
20120
+ mobilePhoneInterstitial3: [],
20121
+ popUp: [],
20122
+ portrait: [],
20123
+ rbProductUpcs: [],
20124
+ skyscraper: [],
20125
+ smallRectangle: [],
20126
+ smallSquare: [],
20127
+ smartphoneBanner1: [],
20128
+ smartphoneBanner2: [],
20129
+ square: [],
20130
+ verticalBanner: [],
20131
+ verticalRectangle: [],
20132
+ wideSkyscraper: [],
20133
+ });
18468
20134
 
20135
+ /**
20136
+ * Checks if the current environment is a browser environment.
20137
+ *
20138
+ * @return {boolean} - Whether the current environment is a browser environment.
20139
+ */
20140
+ function isBrowserEnvironment() {
20141
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
20142
+ console.warn('LiquidCommerce Rmn Sdk: Methods which create elements are only available in browser environments.');
20143
+ return false;
20144
+ }
20145
+ return true;
20146
+ }
20147
+ /**
20148
+ * Validates the inject data by preventing duplicate placement ids and non-existent spot types.
20149
+ *
20150
+ * @param {IInjectSpotElement[]} inject - The inject data.
20151
+ * @return {IInjectSpotElement[]} - The validated inject data.
20152
+ */
20153
+ function validateInjectData(inject) {
20154
+ const eventService = EventService.getInstance();
20155
+ const placementIds = new Set();
20156
+ const validSpotTypes = new Set(Object.values(RMN_SPOT_TYPE));
20157
+ const validatedInject = [];
20158
+ for (const item of inject) {
20159
+ const placementId = item.placementId.replace('#', '');
20160
+ // Check for duplicate placement ids
20161
+ if (placementIds.has(placementId)) {
20162
+ eventService.handleSpotState(placementId, {
20163
+ state: {
20164
+ error: `Duplicate placement id (${placementId}) found. Please provide a unique placement id for each spot element.`,
20165
+ },
20166
+ });
20167
+ continue;
20168
+ }
20169
+ // Check for non-existent spot types
20170
+ if (!validSpotTypes.has(item.spotType)) {
20171
+ eventService.handleSpotState(placementId, {
20172
+ state: {
20173
+ error: `Invalid spot type (${item.spotType}) found. Please provide a valid spot type for each spot element.`,
20174
+ },
20175
+ });
20176
+ continue;
20177
+ }
20178
+ placementIds.add(placementId);
20179
+ validatedInject.push(item);
20180
+ }
20181
+ return validatedInject;
20182
+ }
20183
+ /**
20184
+ * Clears the placement element by removing all its children.
20185
+ *
20186
+ * @param {string} placementId - The placement id.
20187
+ *
20188
+ * @return {void}
20189
+ */
20190
+ function clearPlacement(placementId) {
20191
+ var _a;
20192
+ (_a = document.getElementById(placementId)) === null || _a === void 0 ? void 0 : _a.replaceChildren();
20193
+ }
20194
+ /**
20195
+ * Prepares the spot placement for rendering by taking care of its styling.
20196
+ *
20197
+ * @param {HTMLElement} placement - The placement element.
20198
+ *
20199
+ * @return {void}
20200
+ */
20201
+ function prepareSpotPlacement(placement) {
20202
+ placement.removeAttribute('style');
20203
+ placement.removeAttribute('class');
20204
+ placement.setAttribute('data-spot-parent', 'true');
20205
+ const styles = {
20206
+ width: '100%',
20207
+ height: '100%',
20208
+ display: 'flex',
20209
+ justifyContent: 'center',
20210
+ };
20211
+ Object.assign(placement.style, styles);
20212
+ }
20213
+ // Sets the id for the user who is browsing the website
20214
+ // This id is used to identify the user and provide personalized content
20215
+ function setUserId() {
20216
+ if (isBrowserEnvironment()) {
20217
+ const localStorageService = LocalStorageService.getInstance();
20218
+ localStorageService.setUserId();
20219
+ }
20220
+ }
20221
+ function initDOMObserver(elementIds, callback) {
20222
+ const checkElement = (element) => {
20223
+ if (element.id &&
20224
+ elementIds.includes(element.id) &&
20225
+ !element.getAttribute('data-spot-parent')) {
20226
+ callback(element);
20227
+ }
20228
+ };
20229
+ const observer = new MutationObserver((mutations) => {
20230
+ mutations.forEach((mutation) => {
20231
+ if (mutation.type === 'childList') {
20232
+ mutation.addedNodes.forEach((node) => {
20233
+ if (node.nodeType === Node.ELEMENT_NODE) {
20234
+ checkElement(node);
20235
+ node
20236
+ .querySelectorAll('*')
20237
+ .forEach((el) => checkElement(el));
20238
+ }
20239
+ });
20240
+ }
20241
+ });
20242
+ });
20243
+ observer.observe(document.body, {
20244
+ childList: true,
20245
+ subtree: true,
20246
+ });
20247
+ document.querySelectorAll('*').forEach((el) => checkElement(el));
20248
+ return observer;
20249
+ }
20250
+
20251
+ /**
20252
+ * LiquidCommerce Rmn Client
20253
+ *
20254
+ * @class
20255
+ */
18469
20256
  class LiquidCommerceRmnClient {
18470
20257
  constructor(auth) {
18471
20258
  this.selectionService = SelectionService.getInstance(auth);
18472
20259
  this.elementService = ElementService.getInstance();
18473
20260
  this.eventService = EventService.getInstance();
20261
+ this.intersectionObserver = new IntersectionObserverService();
18474
20262
  }
18475
20263
  /**
18476
20264
  * Makes a selection request on our server based on the provided data.
@@ -18479,7 +20267,7 @@ class LiquidCommerceRmnClient {
18479
20267
  *
18480
20268
  * @param {ISpotSelectionParams} params - Spots selection parameters.
18481
20269
  *
18482
- * @return {Promise<ISpots | {error : string}>} - The spots response object.
20270
+ * @return {Promise<ISpots | { error : string }>} - The spots response object.
18483
20271
  */
18484
20272
  async spotSelection(params) {
18485
20273
  return this.selectionService.spotSelection(params);
@@ -18492,105 +20280,188 @@ class LiquidCommerceRmnClient {
18492
20280
  * @return {Promise<void>} - A promise that resolves when the spot elements are injected.
18493
20281
  */
18494
20282
  async injectSpotElement(params) {
18495
- var _a;
18496
20283
  const config = params.config;
18497
- let inject = params.inject;
18498
- if (!inject.length) {
20284
+ // Handle no spots error state
20285
+ if (!params.inject.length) {
18499
20286
  this.eventService.handleSpotState('all', {
18500
20287
  state: {
18501
20288
  error: 'No spot elements provided for injection.',
18502
- loading: false,
18503
- },
18504
- });
18505
- return;
18506
- }
18507
- // Update the state of the spots to loading
18508
- this.updateSpotsState(inject);
18509
- // Prevent duplicate placement ids
18510
- const hasDuplicatePlacementIds = this.preventDuplicateSpotPlacementIds(inject);
18511
- if (!hasDuplicatePlacementIds) {
18512
- return;
18513
- }
18514
- // Prevent non-existent spot types
18515
- inject = this.preventNonExistentSpotTypes(inject);
18516
- // Make the spot selection request
18517
- const response = await this.spotSelectionRequest({ ...params, inject });
18518
- // const response = await this.useSpotSelectionExample(inject);
18519
- // Handle the response
18520
- if (typeof response === 'object' && 'error' in response) {
18521
- this.eventService.handleSpotState('all', {
18522
- state: {
18523
- error: response.error,
18524
20289
  },
18525
20290
  });
18526
20291
  return;
18527
20292
  }
18528
- for (const item of inject) {
18529
- const itemConfig = (_a = item.config) !== null && _a !== void 0 ? _a : config;
18530
- const spots = response[item.placementId];
18531
- if (!(spots === null || spots === void 0 ? void 0 : spots.length)) {
18532
- this.eventService.handleSpotState(item.placementId, {
20293
+ // Validate inject data
20294
+ const inject = validateInjectData(params.inject);
20295
+ const placementElementShowedInDOM = (placement) => {
20296
+ var _a;
20297
+ const data = inject.find((item) => item.placementId === placement.id);
20298
+ // Handle placement not found error state
20299
+ if (!data) {
20300
+ this.eventService.handleSpotState(placement.id, {
18533
20301
  state: {
18534
- error: `No spots found for type "${item.spotType}".`,
20302
+ error: `Placement not found for id "${placement.id}".`,
20303
+ mounted: false,
18535
20304
  loading: false,
18536
20305
  },
18537
20306
  });
18538
- continue;
20307
+ return;
18539
20308
  }
18540
- const placementId = item.placementId.replace('#', '');
18541
- const placement = document.getElementById(placementId);
18542
- if (!placement) {
18543
- this.eventService.handleSpotState(item.placementId, {
20309
+ // Identify the spot element
20310
+ this.eventService.handleSpotState(data.placementId, {
20311
+ identifier: {
20312
+ placementId: data.placementId,
20313
+ spotType: data.spotType,
20314
+ },
20315
+ }, false);
20316
+ prepareSpotPlacement(placement);
20317
+ const skeletonElement = this.elementService.createSkeletonElement({
20318
+ fluid: (_a = config === null || config === void 0 ? void 0 : config.fluid) !== null && _a !== void 0 ? _a : false,
20319
+ spotType: data.spotType,
20320
+ });
20321
+ // Handle skeleton loader error state
20322
+ if (!skeletonElement) {
20323
+ this.eventService.handleSpotState(data.placementId, {
18544
20324
  state: {
18545
- error: `Placement not found for id "${placementId}".`,
18546
- loading: false,
20325
+ error: `Failed to create skeleton loader element.`,
20326
+ mounted: false,
20327
+ loading: true,
18547
20328
  },
18548
20329
  });
18549
- continue;
20330
+ return;
18550
20331
  }
18551
- // Take over placement styles
18552
- placement.removeAttribute('style');
18553
- placement.removeAttribute('class');
18554
- Object.assign(placement.style, {
18555
- width: '100%',
18556
- height: 'auto',
18557
- display: 'flex',
18558
- justifyContent: 'center',
18559
- });
18560
- if (spots.length === 1) {
18561
- const isInjected = this.injectOneSpotElement(item, placement, spots[0], itemConfig);
18562
- if (!isInjected) {
18563
- continue;
20332
+ placement.replaceChildren(skeletonElement);
20333
+ const spotPlacementIsNearCallback = async () => {
20334
+ var _a;
20335
+ // Stop observing the placement, as we only need to do this once
20336
+ this.intersectionObserver.unobserve(placement);
20337
+ // Set the spot element to loading state
20338
+ this.eventService.handleSpotState(data.placementId, { state: { loading: true } });
20339
+ // Make the spot selection request
20340
+ const response = await this.injectSpotSelectionRequest({ ...params, inject: [data] });
20341
+ // const response = await useSpotSelectionExample(inject);
20342
+ // Handle request error state
20343
+ if (typeof response === 'object' && 'error' in response) {
20344
+ this.eventService.handleSpotState(data.placementId, {
20345
+ state: {
20346
+ error: response.error,
20347
+ mounted: false,
20348
+ loading: false,
20349
+ },
20350
+ });
20351
+ clearPlacement(data.placementId);
20352
+ return;
18564
20353
  }
18565
- }
18566
- if (spots.length > 1) {
18567
- const isInjected = this.injectCarouselSpotElement(placement, spots, itemConfig);
18568
- if (!isInjected) {
18569
- continue;
20354
+ const itemConfig = (_a = data.config) !== null && _a !== void 0 ? _a : config;
20355
+ const spots = response[data.placementId];
20356
+ // Handle no spots found error state
20357
+ if (!(spots === null || spots === void 0 ? void 0 : spots.length)) {
20358
+ this.eventService.handleSpotState(data.placementId, {
20359
+ state: {
20360
+ error: `No spots found for type "${data.spotType}".`,
20361
+ mounted: false,
20362
+ loading: false,
20363
+ },
20364
+ });
20365
+ clearPlacement(data.placementId);
20366
+ return;
18570
20367
  }
18571
- }
18572
- }
20368
+ // Handle single spot
20369
+ if (spots.length === 1) {
20370
+ this.injectOneSpotElement(placement, spots[0], itemConfig);
20371
+ }
20372
+ // Handle multiple spots (carousel)
20373
+ if (spots.length > 1) {
20374
+ this.injectCarouselSpotElement(placement, spots, itemConfig);
20375
+ }
20376
+ };
20377
+ /**
20378
+ * Observe the placement element to check if it is near the viewport.
20379
+ * If it is near, make the spot selection request.
20380
+ */
20381
+ this.intersectionObserver.observe(placement, spotPlacementIsNearCallback, {
20382
+ rootMargin: '1000px',
20383
+ threshold: 0,
20384
+ });
20385
+ };
20386
+ initDOMObserver(inject.map((item) => item.placementId), placementElementShowedInDOM);
18573
20387
  }
18574
20388
  /**
18575
- * Makes a selection request on our server based on the provided data.
20389
+ * Injects a single spot element into the provided placement.
18576
20390
  *
18577
- * @param {IInjectSpotElementParams} params - Parameters for injecting spot elements.
20391
+ * @param {HTMLElement} placement - The placement element.
20392
+ * @param {ISpot} spot - The spot data.
20393
+ * @param {IInjectSpotElementConfig} config - The configuration object.
18578
20394
  *
18579
- * @return {Promise<ISpots | {error: string}>} - The spots response object.
20395
+ * @return {void}
18580
20396
  */
18581
- async spotSelectionRequest(params) {
18582
- const { inject, filter, config } = params;
18583
- const request = {
18584
- url: config === null || config === void 0 ? void 0 : config.url,
18585
- filter,
18586
- spots: inject.map((item) => ({
18587
- placementId: item.placementId,
18588
- spot: item.spotType,
18589
- count: item === null || item === void 0 ? void 0 : item.count,
18590
- ...item === null || item === void 0 ? void 0 : item.filter,
18591
- })),
18592
- };
18593
- return this.spotSelection(request);
20397
+ injectOneSpotElement(placement, spot, config) {
20398
+ var _a;
20399
+ const placementId = placement.id;
20400
+ const spotType = spot.spot;
20401
+ this.eventService.handleSpotState(placementId, {
20402
+ identifier: {
20403
+ placementId,
20404
+ spotType,
20405
+ spotId: spot.id,
20406
+ },
20407
+ displayConfig: {
20408
+ isSingleItem: true,
20409
+ isCarousel: false,
20410
+ isCarouselItem: false,
20411
+ },
20412
+ });
20413
+ const spotData = this.elementService.overrideSpotColors(spot, config === null || config === void 0 ? void 0 : config.colors);
20414
+ const content = SPOT_TEMPLATE_HTML_ELEMENT(spotData, { overlay: config === null || config === void 0 ? void 0 : config.overlay });
20415
+ if (!content) {
20416
+ this.eventService.handleSpotState(placementId, {
20417
+ state: {
20418
+ error: `Failed to inject spot element. Could not create element for type "${spotType}".`,
20419
+ mounted: false,
20420
+ loading: false,
20421
+ },
20422
+ });
20423
+ clearPlacement(placementId);
20424
+ return;
20425
+ }
20426
+ // Create the spot element
20427
+ const spotElement = this.elementService.createSpotElement({
20428
+ content,
20429
+ config: {
20430
+ fluid: config === null || config === void 0 ? void 0 : config.fluid,
20431
+ spot: spot.spot,
20432
+ width: spot.width,
20433
+ height: spot.height,
20434
+ minScale: (_a = config === null || config === void 0 ? void 0 : config.minScale) !== null && _a !== void 0 ? _a : 0.25, // Scale down to 25% of the original size
20435
+ },
20436
+ });
20437
+ if (!spotElement) {
20438
+ this.eventService.handleSpotState(placementId, {
20439
+ state: {
20440
+ error: `Failed to inject spot element. Could not create element for type "${spotType}".`,
20441
+ mounted: false,
20442
+ loading: false,
20443
+ },
20444
+ });
20445
+ clearPlacement(placementId);
20446
+ return;
20447
+ }
20448
+ this.eventService.registerSpot({
20449
+ spot: spotData,
20450
+ placementId,
20451
+ spotElement,
20452
+ });
20453
+ placement.replaceChildren(spotElement);
20454
+ this.eventService.handleSpotState(placementId, {
20455
+ dom: {
20456
+ spotElement,
20457
+ visibleOnViewport: false,
20458
+ },
20459
+ state: {
20460
+ mounted: true,
20461
+ loading: false,
20462
+ error: undefined,
20463
+ },
20464
+ });
18594
20465
  }
18595
20466
  /**
18596
20467
  * Injects a carousel element with the provided spots into the placement.
@@ -18603,21 +20474,28 @@ class LiquidCommerceRmnClient {
18603
20474
  */
18604
20475
  injectCarouselSpotElement(placement, spots, config) {
18605
20476
  var _a;
20477
+ const placementId = placement.id;
18606
20478
  const carouselSlides = [];
18607
20479
  for (const spotItem of spots) {
18608
- this.eventService.handleSpotState(placement.id, {
20480
+ this.eventService.handleSpotState(placementId, {
20481
+ identifier: {
20482
+ placementId,
20483
+ spotType: spotItem.spot,
20484
+ spotId: spotItem.id,
20485
+ },
18609
20486
  displayConfig: {
20487
+ isSingleItem: false,
18610
20488
  isCarousel: true,
18611
20489
  isCarouselItem: true,
18612
- isSingleItem: false,
18613
20490
  },
18614
- }, false);
20491
+ });
18615
20492
  const spot = this.elementService.overrideSpotColors(spotItem, config === null || config === void 0 ? void 0 : config.colors);
18616
20493
  const content = SPOT_TEMPLATE_HTML_ELEMENT(spot, { overlay: config === null || config === void 0 ? void 0 : config.overlay });
18617
20494
  if (!content) {
18618
- this.eventService.handleSpotState(placement.id, {
20495
+ this.eventService.handleSpotState(placementId, {
18619
20496
  state: {
18620
20497
  error: `Failed to inject carousel spot item element. Could not create element for type "${spot.spot}".`,
20498
+ mounted: false,
18621
20499
  loading: false,
18622
20500
  },
18623
20501
  });
@@ -18625,7 +20503,7 @@ class LiquidCommerceRmnClient {
18625
20503
  }
18626
20504
  this.eventService.registerSpot({
18627
20505
  spot,
18628
- placementId: placement.id,
20506
+ placementId,
18629
20507
  spotElement: content,
18630
20508
  });
18631
20509
  carouselSlides.push(content);
@@ -18647,154 +20525,50 @@ class LiquidCommerceRmnClient {
18647
20525
  },
18648
20526
  });
18649
20527
  if (!carouselElement) {
18650
- this.eventService.handleSpotState(placement.id, {
20528
+ this.eventService.handleSpotState(placementId, {
18651
20529
  state: {
18652
20530
  error: `Failed to inject spot carousel element. Could not create spot carousel element.`,
20531
+ mounted: false,
18653
20532
  loading: false,
18654
20533
  },
18655
20534
  });
18656
- return false;
20535
+ clearPlacement(placementId);
20536
+ return;
18657
20537
  }
18658
20538
  placement.replaceChildren(carouselElement);
18659
- this.eventService.handleSpotState(placement.id, {
20539
+ this.eventService.handleSpotState(placementId, {
18660
20540
  dom: {
18661
20541
  spotElement: carouselElement,
20542
+ visibleOnViewport: false,
18662
20543
  },
18663
20544
  state: {
18664
20545
  mounted: true,
18665
20546
  loading: false,
20547
+ error: undefined,
18666
20548
  },
18667
20549
  });
18668
- return true;
18669
- }
18670
- /**
18671
- * Injects a single spot element into the provided placement.
18672
- *
18673
- * @param {IInjectSpotElement} injectItem - The inject item data.
18674
- * @param {HTMLElement} placement - The placement element.
18675
- * @param {ISpot} spot - The spot data.
18676
- * @param {IInjectSpotElementConfig} config - The configuration object.
18677
- *
18678
- * @return {void}
18679
- */
18680
- injectOneSpotElement(injectItem, placement, spot, config) {
18681
- var _a;
18682
- const spotData = this.elementService.overrideSpotColors(spot, config === null || config === void 0 ? void 0 : config.colors);
18683
- this.eventService.handleSpotState(injectItem.placementId, {
18684
- displayConfig: {
18685
- isSingleItem: true,
18686
- },
18687
- }, false);
18688
- // Create the spot template element
18689
- const content = SPOT_TEMPLATE_HTML_ELEMENT(spotData, { overlay: config === null || config === void 0 ? void 0 : config.overlay });
18690
- if (!content) {
18691
- this.eventService.handleSpotState(injectItem.placementId, {
18692
- state: {
18693
- error: `Failed to inject spot element. Could not create element for type "${injectItem.spotType}".`,
18694
- loading: false,
18695
- },
18696
- });
18697
- return false;
18698
- }
18699
- // Create the spot element
18700
- const spotElement = this.elementService.createSpotElement({
18701
- content,
18702
- config: {
18703
- fluid: config === null || config === void 0 ? void 0 : config.fluid,
18704
- spot: spot.spot,
18705
- width: spot.width,
18706
- height: spot.height,
18707
- minScale: (_a = config === null || config === void 0 ? void 0 : config.minScale) !== null && _a !== void 0 ? _a : 0.25, // Scale down to 25% of the original size
18708
- },
18709
- });
18710
- if (!spotElement) {
18711
- this.eventService.handleSpotState(injectItem.placementId, {
18712
- state: {
18713
- error: `Failed to inject spot element. Could not create element for type "${injectItem.spotType}".`,
18714
- loading: false,
18715
- },
18716
- });
18717
- return false;
18718
- }
18719
- this.eventService.registerSpot({
18720
- spot: spotData,
18721
- placementId: injectItem.placementId,
18722
- spotElement,
18723
- });
18724
- placement.replaceChildren(spotElement);
18725
- this.eventService.handleSpotState(injectItem.placementId, {
18726
- dom: {
18727
- spotElement,
18728
- },
18729
- state: {
18730
- mounted: true,
18731
- loading: false,
18732
- },
18733
- });
18734
- return true;
18735
20550
  }
18736
20551
  /**
18737
- * Prevents duplicate placement ids in the inject data.
18738
- *
18739
- * @param {IInjectSpotElement[]} inject - The inject data.
20552
+ * Makes a selection request on our server based on the provided data.
18740
20553
  *
18741
- * @throws {Error} - If a duplicate placement id is found.
20554
+ * @param {IInjectSpotElementParams} params - Parameters for injecting spot elements.
18742
20555
  *
18743
- * @return {void}
20556
+ * @return {Promise<ISpots | {error: string}>} - The spots response object.
18744
20557
  */
18745
- preventDuplicateSpotPlacementIds(inject) {
18746
- const placementIds = new Set();
18747
- for (const item of inject) {
18748
- if (placementIds.has(item.placementId)) {
18749
- this.eventService.handleSpotState(item.placementId, {
18750
- state: {
18751
- error: `Duplicate placement id (${item.placementId}) found. Please provide a unique placement id for each spot element.`,
18752
- },
18753
- });
18754
- return false;
18755
- }
18756
- placementIds.add(item.placementId);
18757
- }
18758
- return true;
18759
- }
18760
- preventNonExistentSpotTypes(inject) {
18761
- const newInject = [];
18762
- for (const item of inject) {
18763
- if (!Object.values(RMN_SPOT_TYPE).includes(item.spotType)) {
18764
- this.eventService.handleSpotState(item.placementId, {
18765
- state: {
18766
- error: `Invalid spot type (${item.spotType}) found. Please provide a valid spot type for each spot element.`,
18767
- },
18768
- });
18769
- continue;
18770
- }
18771
- newInject.push(item);
18772
- }
18773
- return newInject;
18774
- }
18775
- updateSpotsState(inject) {
18776
- for (const item of inject) {
18777
- this.eventService.handleSpotState(item.placementId, {
18778
- identifier: {
18779
- placementId: item.placementId,
18780
- spotType: item.spotType,
18781
- },
18782
- state: {
18783
- loading: true,
18784
- },
18785
- });
18786
- }
18787
- }
18788
- useSpotSelectionExample(inject) {
18789
- const examples = RB_SPOTS_SELECTION_EXAMPLE;
18790
- const data = {};
18791
- inject.map((item) => {
18792
- var _a, _b, _c;
18793
- data[item.placementId] = (_c = (_a = examples[item.spotType]) === null || _a === void 0 ? void 0 : _a.slice(0, (_b = item.count) !== null && _b !== void 0 ? _b : 1)) !== null && _c !== void 0 ? _c : [];
18794
- });
18795
- return new Promise((resolve) => {
18796
- resolve(data);
18797
- });
20558
+ async injectSpotSelectionRequest(params) {
20559
+ const { inject, filter, config } = params;
20560
+ const spots = inject.map((item) => ({
20561
+ placementId: item.placementId,
20562
+ spot: item.spotType,
20563
+ count: item === null || item === void 0 ? void 0 : item.count,
20564
+ ...item === null || item === void 0 ? void 0 : item.filter,
20565
+ }));
20566
+ const request = {
20567
+ url: config === null || config === void 0 ? void 0 : config.url,
20568
+ filter,
20569
+ spots,
20570
+ };
20571
+ return this.spotSelection(request);
18798
20572
  }
18799
20573
  }
18800
20574
  /**
@@ -18808,6 +20582,7 @@ class LiquidCommerceRmnClient {
18808
20582
  async function RmnClient(apiKey, config) {
18809
20583
  const authService = AuthService.getInstance(apiKey, config.env);
18810
20584
  const credentials = await authService.initialize();
20585
+ setUserId();
18811
20586
  return new LiquidCommerceRmnClient(credentials);
18812
20587
  }
18813
20588
  /**
@@ -18872,4 +20647,4 @@ function RmnCreateSpotElement(spot, config) {
18872
20647
  });
18873
20648
  }
18874
20649
 
18875
- export { LiquidCommerceRmnClient, RMN_ENV, RMN_FILTER_PROPERTIES, RMN_SPOT_EVENT, RMN_SPOT_TYPE, RmnClient, RmnCreateSpotElement, RmnEventManager };
20650
+ export { LiquidCommerceRmnClient, RMN_ENV, RMN_EVENT, RMN_FILTER_PROPERTIES, RMN_SPOT_EVENT, RMN_SPOT_TYPE, RmnClient, RmnCreateSpotElement, RmnEventManager };