@magic-spells/gift-with-purchase 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +332 -0
- package/dist/gift-with-purchase.cjs.css +43 -0
- package/dist/gift-with-purchase.cjs.css.map +1 -0
- package/dist/gift-with-purchase.cjs.js +339 -0
- package/dist/gift-with-purchase.cjs.js.map +1 -0
- package/dist/gift-with-purchase.esm.css +43 -0
- package/dist/gift-with-purchase.esm.css.map +1 -0
- package/dist/gift-with-purchase.esm.js +337 -0
- package/dist/gift-with-purchase.esm.js.map +1 -0
- package/dist/gift-with-purchase.min.css +2 -0
- package/dist/gift-with-purchase.min.css.map +1 -0
- package/dist/gift-with-purchase.min.js +2 -0
- package/dist/gift-with-purchase.min.js.map +1 -0
- package/dist/gift-with-purchase.scss +70 -0
- package/package.json +75 -0
- package/src/gift-with-purchase.js +338 -0
- package/src/gift-with-purchase.scss +70 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gift With Purchase Component - automatically adds/removes gift when cart threshold is met
|
|
3
|
+
* Emits gwp:added/gwp:removed/gwp:error events and broadcasts cart updates
|
|
4
|
+
*/
|
|
5
|
+
class GiftWithPurchase extends HTMLElement {
|
|
6
|
+
// private fields
|
|
7
|
+
#threshold = 0;
|
|
8
|
+
#currentAmount = 0;
|
|
9
|
+
#variantId = null;
|
|
10
|
+
#isActive = false;
|
|
11
|
+
#isAdded = false;
|
|
12
|
+
#promoEnded = false;
|
|
13
|
+
#cartDialog = null;
|
|
14
|
+
#boundHandleCartDataChange = null; // pre-bound listener ref for clean-up
|
|
15
|
+
#debounceTimer = null; // debouncing cart updates
|
|
16
|
+
#messageAbove = null; // message when threshold is met
|
|
17
|
+
#messageBelow = null; // message when below threshold
|
|
18
|
+
|
|
19
|
+
static get observedAttributes() {
|
|
20
|
+
return ['threshold', 'current', 'variant-id', 'promo-ended', 'message-above', 'message-below'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
// read initial attributes once
|
|
26
|
+
this.#threshold = parseFloat(this.getAttribute('threshold')) || 0;
|
|
27
|
+
this.#currentAmount = parseFloat(this.getAttribute('current')) || 0;
|
|
28
|
+
this.#variantId = this.getAttribute('variant-id');
|
|
29
|
+
this.#promoEnded = this.hasAttribute('promo-ended');
|
|
30
|
+
this.#messageAbove = this.getAttribute('message-above');
|
|
31
|
+
this.#messageBelow = this.getAttribute('message-below');
|
|
32
|
+
this.#boundHandleCartDataChange = this.#handleCartDataChange.bind(this);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
connectedCallback() {
|
|
36
|
+
this.#render();
|
|
37
|
+
this.#attachListeners();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
disconnectedCallback() {
|
|
41
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
42
|
+
if (this.#cartDialog)
|
|
43
|
+
this.#cartDialog.removeEventListener(
|
|
44
|
+
'cart-dialog:data-changed',
|
|
45
|
+
this.#boundHandleCartDataChange
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#render() {
|
|
50
|
+
this.classList.add('gift-with-purchase');
|
|
51
|
+
this.#renderMessages();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#renderMessages() {
|
|
55
|
+
// Look for existing message element with data-content-gwp-message
|
|
56
|
+
this.#updateMessages();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#updateMessages() {
|
|
60
|
+
const messageEl = this.querySelector('[data-content-gwp-message]');
|
|
61
|
+
if (!messageEl) return;
|
|
62
|
+
|
|
63
|
+
let message = '';
|
|
64
|
+
|
|
65
|
+
// console.log('updateMessages - this.#isActive', this.#isActive);
|
|
66
|
+
|
|
67
|
+
if (this.#isActive && this.#messageAbove) {
|
|
68
|
+
// set message to above threshold message
|
|
69
|
+
message = this.#messageAbove;
|
|
70
|
+
} else if (!this.#isActive && this.#messageBelow) {
|
|
71
|
+
// set message to below threshold message
|
|
72
|
+
const remaining = this.#threshold - this.#currentAmount;
|
|
73
|
+
const formattedAmount = remaining.toFixed(2).replace(/\.00$/, '');
|
|
74
|
+
message = this.#messageBelow
|
|
75
|
+
.replace(/\{\s*amount\s*\}/g, formattedAmount)
|
|
76
|
+
.replace(/\{amount\}/g, formattedAmount);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
messageEl.textContent = message;
|
|
80
|
+
messageEl.style.display = message ? 'block' : 'none';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#attachListeners() {
|
|
84
|
+
// Look for cart-dialog element when attaching listeners (more reliable timing)
|
|
85
|
+
this.#cartDialog = this.closest('cart-dialog');
|
|
86
|
+
|
|
87
|
+
if (this.#cartDialog) {
|
|
88
|
+
// console.log('cartDialog exists and is attaching events');
|
|
89
|
+
this.#cartDialog.addEventListener(
|
|
90
|
+
'cart-dialog:data-changed',
|
|
91
|
+
this.#boundHandleCartDataChange
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
// Try again after a short delay in case the DOM isn't fully ready
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
// console.log('cartDialog DIDNT exist and we waited to attach events');
|
|
97
|
+
this.#cartDialog = this.closest('cart-dialog');
|
|
98
|
+
if (this.#cartDialog) {
|
|
99
|
+
this.#cartDialog.addEventListener(
|
|
100
|
+
'cart-dialog:data-changed',
|
|
101
|
+
this.#boundHandleCartDataChange
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
console.error('GWP - cart-dialog still not found after delay');
|
|
105
|
+
}
|
|
106
|
+
}, 100);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#handleCartDataChange(event) {
|
|
111
|
+
const cart = event.detail;
|
|
112
|
+
// console.log('GWP - handleCartDataChange cart: ', cart.calculated_subtotal, cart);
|
|
113
|
+
|
|
114
|
+
if (!cart || typeof cart.calculated_subtotal === 'undefined') return;
|
|
115
|
+
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
116
|
+
|
|
117
|
+
this.#debounceTimer = setTimeout(() => {
|
|
118
|
+
this.#debounceTimer = null;
|
|
119
|
+
this.#currentAmount = parseFloat(cart.calculated_subtotal / 100) || 0;
|
|
120
|
+
this.#checkGiftInCart(cart);
|
|
121
|
+
this.#updateState(cart);
|
|
122
|
+
}, 300);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// checks to see if the gift is already in the cart
|
|
126
|
+
#checkGiftInCart(cart) {
|
|
127
|
+
if (!cart.items || !this.#variantId) {
|
|
128
|
+
this.#isAdded = false;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const giftLines = cart.items.filter(
|
|
132
|
+
(lineItem) =>
|
|
133
|
+
lineItem.variant_id.toString() === this.#variantId.toString() &&
|
|
134
|
+
lineItem.properties?._gwp_item === 'true'
|
|
135
|
+
);
|
|
136
|
+
this.#isAdded = giftLines.length > 0;
|
|
137
|
+
if (this.#promoEnded && giftLines.length) this.#removeAllGiftItems(giftLines);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#updateState(cart) {
|
|
141
|
+
// console.log('********** ---------- Updating state....');
|
|
142
|
+
const wasActive = this.#isActive;
|
|
143
|
+
this.#isActive = this.#currentAmount >= this.#threshold && !this.#promoEnded;
|
|
144
|
+
|
|
145
|
+
// console.log('********** ---------- this.#isActive', this.#isActive);
|
|
146
|
+
|
|
147
|
+
if (this.#promoEnded) {
|
|
148
|
+
// remove GWP from cart
|
|
149
|
+
this.#removeGiftFromCart(cart);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.#isActive && !wasActive && !this.#isAdded && this.#variantId) {
|
|
153
|
+
this.#addGiftToCart();
|
|
154
|
+
} else if (!this.#isActive && wasActive && this.#isAdded && this.#variantId) {
|
|
155
|
+
this.#removeGiftFromCart(cart);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.#updateVisualState();
|
|
159
|
+
this.#updateMessages();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#updateVisualState() {
|
|
163
|
+
if (this.#promoEnded) {
|
|
164
|
+
this.setAttribute('state', 'ended');
|
|
165
|
+
this.style.display = 'none';
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.style.display = '';
|
|
170
|
+
if (this.#isAdded) {
|
|
171
|
+
this.setAttribute('state', 'added');
|
|
172
|
+
} else if (this.#isActive) {
|
|
173
|
+
this.setAttribute('state', 'active');
|
|
174
|
+
}
|
|
175
|
+
// Note: no 'inactive' state since component wouldn't be loaded if inactive
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async #addGiftToCart() {
|
|
179
|
+
try {
|
|
180
|
+
const res = await fetch('/cart/add.js', {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
credentials: 'same-origin',
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
items: [
|
|
189
|
+
{
|
|
190
|
+
id: this.#variantId,
|
|
191
|
+
quantity: 1,
|
|
192
|
+
properties: {
|
|
193
|
+
_gwp_item: 'true',
|
|
194
|
+
_hide_in_cart: 'true',
|
|
195
|
+
_ignore_price_in_subtotal: 'true',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) throw new Error(`http ${res.status}`);
|
|
202
|
+
await res.json();
|
|
203
|
+
this.#isAdded = true;
|
|
204
|
+
this.dispatchEvent(
|
|
205
|
+
new CustomEvent('gwp:added', {
|
|
206
|
+
detail: { variantId: this.#variantId },
|
|
207
|
+
bubbles: true,
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
console.error('giftwithpurchase: add error', err);
|
|
212
|
+
this.dispatchEvent(
|
|
213
|
+
new CustomEvent('gwp:error', {
|
|
214
|
+
detail: { action: 'add', error: err.message },
|
|
215
|
+
bubbles: true,
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async #removeGiftFromCart(cart) {
|
|
222
|
+
try {
|
|
223
|
+
// get all GWP items in the cart
|
|
224
|
+
const giftLines = cart.items.filter(
|
|
225
|
+
(lineItem) =>
|
|
226
|
+
lineItem.variant_id.toString() === this.#variantId.toString() &&
|
|
227
|
+
lineItem.properties?._gwp_item === 'true'
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// exit if no items in the cart
|
|
231
|
+
if (!giftLines.length) {
|
|
232
|
+
this.#isAdded = false;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// remove all GWP items from the cart
|
|
237
|
+
await this.#removeAllGiftItems(giftLines);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error('giftwithpurchase: remove error', err);
|
|
240
|
+
this.dispatchEvent(
|
|
241
|
+
new CustomEvent('gwp:error', {
|
|
242
|
+
detail: { action: 'remove', error: err.message },
|
|
243
|
+
bubbles: true,
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async #removeAllGiftItems(giftLines) {
|
|
250
|
+
try {
|
|
251
|
+
await Promise.all(
|
|
252
|
+
giftLines.map((giftItem) =>
|
|
253
|
+
fetch('/cart/change.js', {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
credentials: 'same-origin',
|
|
256
|
+
headers: {
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({ id: giftItem.key, quantity: 0 }),
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
this.#isAdded = false;
|
|
265
|
+
// note: you can broadcast cart again here if you want real-time re-render after remove
|
|
266
|
+
this.dispatchEvent(
|
|
267
|
+
new CustomEvent('gwp:removed', {
|
|
268
|
+
detail: { variantId: this.#variantId },
|
|
269
|
+
bubbles: true,
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error('giftwithpurchase: bulk remove error', err);
|
|
274
|
+
this.dispatchEvent(
|
|
275
|
+
new CustomEvent('gwp:error', {
|
|
276
|
+
detail: { action: 'remove', error: err.message },
|
|
277
|
+
bubbles: true,
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getState() {
|
|
284
|
+
return {
|
|
285
|
+
currentAmount: this.#currentAmount,
|
|
286
|
+
threshold: this.#threshold,
|
|
287
|
+
variantId: this.#variantId,
|
|
288
|
+
isActive: this.#isActive,
|
|
289
|
+
isAdded: this.#isAdded,
|
|
290
|
+
promoEnded: this.#promoEnded,
|
|
291
|
+
remainingAmount: Math.max(0, this.#threshold - this.#currentAmount),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get currentAmount() {
|
|
296
|
+
return this.#currentAmount;
|
|
297
|
+
}
|
|
298
|
+
get threshold() {
|
|
299
|
+
return this.#threshold;
|
|
300
|
+
}
|
|
301
|
+
get variantId() {
|
|
302
|
+
return this.#variantId;
|
|
303
|
+
}
|
|
304
|
+
get isActive() {
|
|
305
|
+
return this.#isActive;
|
|
306
|
+
}
|
|
307
|
+
get isAdded() {
|
|
308
|
+
return this.#isAdded;
|
|
309
|
+
}
|
|
310
|
+
get promoEnded() {
|
|
311
|
+
return this.#promoEnded;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Public setter methods for programmatic control
|
|
315
|
+
setCurrentAmount(amount) {
|
|
316
|
+
this.#currentAmount = parseFloat(amount) || 0;
|
|
317
|
+
this.#updateState({ items: [] }); // Pass empty cart to avoid cart operations
|
|
318
|
+
this.#updateMessages();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
setThreshold(threshold) {
|
|
322
|
+
this.#threshold = parseFloat(threshold) || 0;
|
|
323
|
+
this.#updateState({ items: [] }); // Pass empty cart to avoid cart operations
|
|
324
|
+
this.#updateMessages();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setVariantId(variantId) {
|
|
328
|
+
this.#variantId = variantId;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!customElements.get('gift-with-purchase')) {
|
|
333
|
+
customElements.define('gift-with-purchase', GiftWithPurchase);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export { GiftWithPurchase };
|
|
337
|
+
//# sourceMappingURL=gift-with-purchase.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gift-with-purchase.esm.js","sources":["../src/gift-with-purchase.js"],"sourcesContent":["import './gift-with-purchase.scss';\n\n/**\n * Gift With Purchase Component - automatically adds/removes gift when cart threshold is met\n * Emits gwp:added/gwp:removed/gwp:error events and broadcasts cart updates\n */\nclass GiftWithPurchase extends HTMLElement {\n\t// private fields\n\t#threshold = 0;\n\t#currentAmount = 0;\n\t#variantId = null;\n\t#isActive = false;\n\t#isAdded = false;\n\t#promoEnded = false;\n\t#cartDialog = null;\n\t#boundHandleCartDataChange = null; // pre-bound listener ref for clean-up\n\t#debounceTimer = null; // debouncing cart updates\n\t#messageAbove = null; // message when threshold is met\n\t#messageBelow = null; // message when below threshold\n\n\tstatic get observedAttributes() {\n\t\treturn ['threshold', 'current', 'variant-id', 'promo-ended', 'message-above', 'message-below'];\n\t}\n\n\tconstructor() {\n\t\tsuper();\n\t\t// read initial attributes once\n\t\tthis.#threshold = parseFloat(this.getAttribute('threshold')) || 0;\n\t\tthis.#currentAmount = parseFloat(this.getAttribute('current')) || 0;\n\t\tthis.#variantId = this.getAttribute('variant-id');\n\t\tthis.#promoEnded = this.hasAttribute('promo-ended');\n\t\tthis.#messageAbove = this.getAttribute('message-above');\n\t\tthis.#messageBelow = this.getAttribute('message-below');\n\t\tthis.#boundHandleCartDataChange = this.#handleCartDataChange.bind(this);\n\t}\n\n\tconnectedCallback() {\n\t\tthis.#render();\n\t\tthis.#attachListeners();\n\t}\n\n\tdisconnectedCallback() {\n\t\tif (this.#debounceTimer) clearTimeout(this.#debounceTimer);\n\t\tif (this.#cartDialog)\n\t\t\tthis.#cartDialog.removeEventListener(\n\t\t\t\t'cart-dialog:data-changed',\n\t\t\t\tthis.#boundHandleCartDataChange\n\t\t\t);\n\t}\n\n\t#render() {\n\t\tthis.classList.add('gift-with-purchase');\n\t\tthis.#renderMessages();\n\t}\n\n\t#renderMessages() {\n\t\t// Look for existing message element with data-content-gwp-message\n\t\tthis.#updateMessages();\n\t}\n\n\t#updateMessages() {\n\t\tconst messageEl = this.querySelector('[data-content-gwp-message]');\n\t\tif (!messageEl) return;\n\n\t\tlet message = '';\n\n\t\t// console.log('updateMessages - this.#isActive', this.#isActive);\n\n\t\tif (this.#isActive && this.#messageAbove) {\n\t\t\t// set message to above threshold message\n\t\t\tmessage = this.#messageAbove;\n\t\t} else if (!this.#isActive && this.#messageBelow) {\n\t\t\t// set message to below threshold message\n\t\t\tconst remaining = this.#threshold - this.#currentAmount;\n\t\t\tconst formattedAmount = remaining.toFixed(2).replace(/\\.00$/, '');\n\t\t\tmessage = this.#messageBelow\n\t\t\t\t.replace(/\\{\\s*amount\\s*\\}/g, formattedAmount)\n\t\t\t\t.replace(/\\{amount\\}/g, formattedAmount);\n\t\t}\n\n\t\tmessageEl.textContent = message;\n\t\tmessageEl.style.display = message ? 'block' : 'none';\n\t}\n\n\t#attachListeners() {\n\t\t// Look for cart-dialog element when attaching listeners (more reliable timing)\n\t\tthis.#cartDialog = this.closest('cart-dialog');\n\n\t\tif (this.#cartDialog) {\n\t\t\t// console.log('cartDialog exists and is attaching events');\n\t\t\tthis.#cartDialog.addEventListener(\n\t\t\t\t'cart-dialog:data-changed',\n\t\t\t\tthis.#boundHandleCartDataChange\n\t\t\t);\n\t\t} else {\n\t\t\t// Try again after a short delay in case the DOM isn't fully ready\n\t\t\tsetTimeout(() => {\n\t\t\t\t// console.log('cartDialog DIDNT exist and we waited to attach events');\n\t\t\t\tthis.#cartDialog = this.closest('cart-dialog');\n\t\t\t\tif (this.#cartDialog) {\n\t\t\t\t\tthis.#cartDialog.addEventListener(\n\t\t\t\t\t\t'cart-dialog:data-changed',\n\t\t\t\t\t\tthis.#boundHandleCartDataChange\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error('GWP - cart-dialog still not found after delay');\n\t\t\t\t}\n\t\t\t}, 100);\n\t\t}\n\t}\n\n\t#handleCartDataChange(event) {\n\t\tconst cart = event.detail;\n\t\t// console.log('GWP - handleCartDataChange cart: ', cart.calculated_subtotal, cart);\n\n\t\tif (!cart || typeof cart.calculated_subtotal === 'undefined') return;\n\t\tif (this.#debounceTimer) clearTimeout(this.#debounceTimer);\n\n\t\tthis.#debounceTimer = setTimeout(() => {\n\t\t\tthis.#debounceTimer = null;\n\t\t\tthis.#currentAmount = parseFloat(cart.calculated_subtotal / 100) || 0;\n\t\t\tthis.#checkGiftInCart(cart);\n\t\t\tthis.#updateState(cart);\n\t\t}, 300);\n\t}\n\n\t// checks to see if the gift is already in the cart\n\t#checkGiftInCart(cart) {\n\t\tif (!cart.items || !this.#variantId) {\n\t\t\tthis.#isAdded = false;\n\t\t\treturn;\n\t\t}\n\t\tconst giftLines = cart.items.filter(\n\t\t\t(lineItem) =>\n\t\t\t\tlineItem.variant_id.toString() === this.#variantId.toString() &&\n\t\t\t\tlineItem.properties?._gwp_item === 'true'\n\t\t);\n\t\tthis.#isAdded = giftLines.length > 0;\n\t\tif (this.#promoEnded && giftLines.length) this.#removeAllGiftItems(giftLines);\n\t}\n\n\t#updateState(cart) {\n\t\t// console.log('********** ---------- Updating state....');\n\t\tconst wasActive = this.#isActive;\n\t\tthis.#isActive = this.#currentAmount >= this.#threshold && !this.#promoEnded;\n\n\t\t// console.log('********** ---------- this.#isActive', this.#isActive);\n\n\t\tif (this.#promoEnded) {\n\t\t\t// remove GWP from cart\n\t\t\tthis.#removeGiftFromCart(cart);\n\t\t}\n\n\t\tif (this.#isActive && !wasActive && !this.#isAdded && this.#variantId) {\n\t\t\tthis.#addGiftToCart();\n\t\t} else if (!this.#isActive && wasActive && this.#isAdded && this.#variantId) {\n\t\t\tthis.#removeGiftFromCart(cart);\n\t\t}\n\n\t\tthis.#updateVisualState();\n\t\tthis.#updateMessages();\n\t}\n\n\t#updateVisualState() {\n\t\tif (this.#promoEnded) {\n\t\t\tthis.setAttribute('state', 'ended');\n\t\t\tthis.style.display = 'none';\n\t\t\treturn;\n\t\t}\n\n\t\tthis.style.display = '';\n\t\tif (this.#isAdded) {\n\t\t\tthis.setAttribute('state', 'added');\n\t\t} else if (this.#isActive) {\n\t\t\tthis.setAttribute('state', 'active');\n\t\t}\n\t\t// Note: no 'inactive' state since component wouldn't be loaded if inactive\n\t}\n\n\tasync #addGiftToCart() {\n\t\ttry {\n\t\t\tconst res = await fetch('/cart/add.js', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tcredentials: 'same-origin',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: this.#variantId,\n\t\t\t\t\t\t\tquantity: 1,\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t_gwp_item: 'true',\n\t\t\t\t\t\t\t\t_hide_in_cart: 'true',\n\t\t\t\t\t\t\t\t_ignore_price_in_subtotal: 'true',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t});\n\t\t\tif (!res.ok) throw new Error(`http ${res.status}`);\n\t\t\tawait res.json();\n\t\t\tthis.#isAdded = true;\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:added', {\n\t\t\t\t\tdetail: { variantId: this.#variantId },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error('giftwithpurchase: add error', err);\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:error', {\n\t\t\t\t\tdetail: { action: 'add', error: err.message },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t}\n\n\tasync #removeGiftFromCart(cart) {\n\t\ttry {\n\t\t\t// get all GWP items in the cart\n\t\t\tconst giftLines = cart.items.filter(\n\t\t\t\t(lineItem) =>\n\t\t\t\t\tlineItem.variant_id.toString() === this.#variantId.toString() &&\n\t\t\t\t\tlineItem.properties?._gwp_item === 'true'\n\t\t\t);\n\n\t\t\t// exit if no items in the cart\n\t\t\tif (!giftLines.length) {\n\t\t\t\tthis.#isAdded = false;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// remove all GWP items from the cart\n\t\t\tawait this.#removeAllGiftItems(giftLines);\n\t\t} catch (err) {\n\t\t\tconsole.error('giftwithpurchase: remove error', err);\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:error', {\n\t\t\t\t\tdetail: { action: 'remove', error: err.message },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t}\n\n\tasync #removeAllGiftItems(giftLines) {\n\t\ttry {\n\t\t\tawait Promise.all(\n\t\t\t\tgiftLines.map((giftItem) =>\n\t\t\t\t\tfetch('/cart/change.js', {\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\tcredentials: 'same-origin',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify({ id: giftItem.key, quantity: 0 }),\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t);\n\t\t\tthis.#isAdded = false;\n\t\t\t// note: you can broadcast cart again here if you want real-time re-render after remove\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:removed', {\n\t\t\t\t\tdetail: { variantId: this.#variantId },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error('giftwithpurchase: bulk remove error', err);\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:error', {\n\t\t\t\t\tdetail: { action: 'remove', error: err.message },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t}\n\n\tgetState() {\n\t\treturn {\n\t\t\tcurrentAmount: this.#currentAmount,\n\t\t\tthreshold: this.#threshold,\n\t\t\tvariantId: this.#variantId,\n\t\t\tisActive: this.#isActive,\n\t\t\tisAdded: this.#isAdded,\n\t\t\tpromoEnded: this.#promoEnded,\n\t\t\tremainingAmount: Math.max(0, this.#threshold - this.#currentAmount),\n\t\t};\n\t}\n\n\tget currentAmount() {\n\t\treturn this.#currentAmount;\n\t}\n\tget threshold() {\n\t\treturn this.#threshold;\n\t}\n\tget variantId() {\n\t\treturn this.#variantId;\n\t}\n\tget isActive() {\n\t\treturn this.#isActive;\n\t}\n\tget isAdded() {\n\t\treturn this.#isAdded;\n\t}\n\tget promoEnded() {\n\t\treturn this.#promoEnded;\n\t}\n\n\t// Public setter methods for programmatic control\n\tsetCurrentAmount(amount) {\n\t\tthis.#currentAmount = parseFloat(amount) || 0;\n\t\tthis.#updateState({ items: [] }); // Pass empty cart to avoid cart operations\n\t\tthis.#updateMessages();\n\t}\n\n\tsetThreshold(threshold) {\n\t\tthis.#threshold = parseFloat(threshold) || 0;\n\t\tthis.#updateState({ items: [] }); // Pass empty cart to avoid cart operations\n\t\tthis.#updateMessages();\n\t}\n\n\tsetVariantId(variantId) {\n\t\tthis.#variantId = variantId;\n\t}\n}\n\nif (!customElements.get('gift-with-purchase')) {\n\tcustomElements.define('gift-with-purchase', GiftWithPurchase);\n}\n\nexport { GiftWithPurchase };\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
gift-with-purchase{--gwp-border-radius:8px;--gwp-padding:1rem;--gwp-gap:1rem;--gwp-bg-active:#e8f5e8;--gwp-bg-added:#d4edda;--gwp-border-active:#28a745;--gwp-border-added:#155724;--gwp-text-active:#155724;--gwp-text-added:#155724;background-color:var(--gwp-bg-active);border:2px solid var(--gwp-border-active);border-radius:var(--gwp-border-radius);color:var(--gwp-text-active);display:block;padding:var(--gwp-padding)}gift-with-purchase .gwp-product{align-items:flex-start;display:flex;gap:var(--gwp-gap)}gift-with-purchase [data-gwp-image]{flex-shrink:0}gift-with-purchase .gwp-content{flex:1;min-width:0}gift-with-purchase[state=active]{background-color:var(--gwp-bg-active);border-color:var(--gwp-border-active);color:var(--gwp-text-active)}gift-with-purchase[state=added]{background-color:var(--gwp-bg-added);border-color:var(--gwp-border-added);color:var(--gwp-text-added)}gift-with-purchase[state=ended]{display:none}
|
|
2
|
+
/*# sourceMappingURL=gift-with-purchase.min.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["gift-with-purchase.scss"],"names":[],"mappings":"AAAA,mBACE,uBAAwB,CACxB,kBAAmB,CACnB,cAAe,CACf,uBAAwB,CACxB,sBAAuB,CACvB,2BAA4B,CAC5B,0BAA2B,CAC3B,yBAA0B,CAC1B,wBAAyB,CAKzB,qCAAsC,CAHtC,yCAA0C,CAC1C,sCAAuC,CAGvC,4BAA6B,CAL7B,aAAc,CAGd,0BAGF,CACA,gCAEE,sBAAuB,CADvB,YAAa,CAEb,kBACF,CACA,oCACE,aACF,CACA,gCACE,MAAO,CACP,WACF,CACA,iCACE,qCAAsC,CACtC,qCAAsC,CACtC,4BACF,CACA,gCACE,oCAAqC,CACrC,oCAAqC,CACrC,2BACF,CACA,gCACE,YACF","file":"gift-with-purchase.min.css","sourcesContent":["gift-with-purchase {\n --gwp-border-radius: 8px;\n --gwp-padding: 1rem;\n --gwp-gap: 1rem;\n --gwp-bg-active: #e8f5e8;\n --gwp-bg-added: #d4edda;\n --gwp-border-active: #28a745;\n --gwp-border-added: #155724;\n --gwp-text-active: #155724;\n --gwp-text-added: #155724;\n display: block;\n border: 2px solid var(--gwp-border-active);\n border-radius: var(--gwp-border-radius);\n padding: var(--gwp-padding);\n background-color: var(--gwp-bg-active);\n color: var(--gwp-text-active);\n}\ngift-with-purchase .gwp-product {\n display: flex;\n align-items: flex-start;\n gap: var(--gwp-gap);\n}\ngift-with-purchase [data-gwp-image] {\n flex-shrink: 0;\n}\ngift-with-purchase .gwp-content {\n flex: 1;\n min-width: 0;\n}\ngift-with-purchase[state=active] {\n background-color: var(--gwp-bg-active);\n border-color: var(--gwp-border-active);\n color: var(--gwp-text-active);\n}\ngift-with-purchase[state=added] {\n background-color: var(--gwp-bg-added);\n border-color: var(--gwp-border-added);\n color: var(--gwp-text-added);\n}\ngift-with-purchase[state=ended] {\n display: none;\n}"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).GiftWithPurchase={})}(this,function(t){"use strict";class e extends HTMLElement{#t=0;#e=0;#i=null;#s=!1;#a=!1;#r=!1;#n=null;#d=null;#o=null;#h=null;#l=null;static get observedAttributes(){return["threshold","current","variant-id","promo-ended","message-above","message-below"]}constructor(){super(),this.#t=parseFloat(this.getAttribute("threshold"))||0,this.#e=parseFloat(this.getAttribute("current"))||0,this.#i=this.getAttribute("variant-id"),this.#r=this.hasAttribute("promo-ended"),this.#h=this.getAttribute("message-above"),this.#l=this.getAttribute("message-below"),this.#d=this.#c.bind(this)}connectedCallback(){this.#u(),this.#m()}disconnectedCallback(){this.#o&&clearTimeout(this.#o),this.#n&&this.#n.removeEventListener("cart-dialog:data-changed",this.#d)}#u(){this.classList.add("gift-with-purchase"),this.#g()}#g(){this.#p()}#p(){const t=this.querySelector("[data-content-gwp-message]");if(!t)return;let e="";if(this.#s&&this.#h)e=this.#h;else if(!this.#s&&this.#l){const t=(this.#t-this.#e).toFixed(2).replace(/\.00$/,"");e=this.#l.replace(/\{\s*amount\s*\}/g,t).replace(/\{amount\}/g,t)}t.textContent=e,t.style.display=e?"block":"none"}#m(){this.#n=this.closest("cart-dialog"),this.#n?this.#n.addEventListener("cart-dialog:data-changed",this.#d):setTimeout(()=>{this.#n=this.closest("cart-dialog"),this.#n?this.#n.addEventListener("cart-dialog:data-changed",this.#d):console.error("GWP - cart-dialog still not found after delay")},100)}#c(t){const e=t.detail;e&&void 0!==e.calculated_subtotal&&(this.#o&&clearTimeout(this.#o),this.#o=setTimeout(()=>{this.#o=null,this.#e=parseFloat(e.calculated_subtotal/100)||0,this.#v(e),this.#b(e)},300))}#v(t){if(!t.items||!this.#i)return void(this.#a=!1);const e=t.items.filter(t=>t.variant_id.toString()===this.#i.toString()&&"true"===t.properties?._gwp_item);this.#a=e.length>0,this.#r&&e.length&&this.#A(e)}#b(t){const e=this.#s;this.#s=this.#e>=this.#t&&!this.#r,this.#r&&this.#f(t),this.#s&&!e&&!this.#a&&this.#i?this.#w():!this.#s&&e&&this.#a&&this.#i&&this.#f(t),this.#C(),this.#p()}#C(){if(this.#r)return this.setAttribute("state","ended"),void(this.style.display="none");this.style.display="",this.#a?this.setAttribute("state","added"):this.#s&&this.setAttribute("state","active")}async#w(){try{const t=await fetch("/cart/add.js",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify({items:[{id:this.#i,quantity:1,properties:{_gwp_item:"true",_hide_in_cart:"true",_ignore_price_in_subtotal:"true"}}]})});if(!t.ok)throw new Error(`http ${t.status}`);await t.json(),this.#a=!0,this.dispatchEvent(new CustomEvent("gwp:added",{detail:{variantId:this.#i},bubbles:!0}))}catch(t){console.error("giftwithpurchase: add error",t),this.dispatchEvent(new CustomEvent("gwp:error",{detail:{action:"add",error:t.message},bubbles:!0}))}}async#f(t){try{const e=t.items.filter(t=>t.variant_id.toString()===this.#i.toString()&&"true"===t.properties?._gwp_item);if(!e.length)return void(this.#a=!1);await this.#A(e)}catch(t){console.error("giftwithpurchase: remove error",t),this.dispatchEvent(new CustomEvent("gwp:error",{detail:{action:"remove",error:t.message},bubbles:!0}))}}async#A(t){try{await Promise.all(t.map(t=>fetch("/cart/change.js",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-Requested-With":"XMLHttpRequest"},body:JSON.stringify({id:t.key,quantity:0})}))),this.#a=!1,this.dispatchEvent(new CustomEvent("gwp:removed",{detail:{variantId:this.#i},bubbles:!0}))}catch(t){console.error("giftwithpurchase: bulk remove error",t),this.dispatchEvent(new CustomEvent("gwp:error",{detail:{action:"remove",error:t.message},bubbles:!0}))}}getState(){return{currentAmount:this.#e,threshold:this.#t,variantId:this.#i,isActive:this.#s,isAdded:this.#a,promoEnded:this.#r,remainingAmount:Math.max(0,this.#t-this.#e)}}get currentAmount(){return this.#e}get threshold(){return this.#t}get variantId(){return this.#i}get isActive(){return this.#s}get isAdded(){return this.#a}get promoEnded(){return this.#r}setCurrentAmount(t){this.#e=parseFloat(t)||0,this.#b({items:[]}),this.#p()}setThreshold(t){this.#t=parseFloat(t)||0,this.#b({items:[]}),this.#p()}setVariantId(t){this.#i=t}}customElements.get("gift-with-purchase")||customElements.define("gift-with-purchase",e),t.GiftWithPurchase=e});
|
|
2
|
+
//# sourceMappingURL=gift-with-purchase.min.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gift-with-purchase.min.js","sources":["../src/gift-with-purchase.js"],"sourcesContent":["import './gift-with-purchase.scss';\n\n/**\n * Gift With Purchase Component - automatically adds/removes gift when cart threshold is met\n * Emits gwp:added/gwp:removed/gwp:error events and broadcasts cart updates\n */\nclass GiftWithPurchase extends HTMLElement {\n\t// private fields\n\t#threshold = 0;\n\t#currentAmount = 0;\n\t#variantId = null;\n\t#isActive = false;\n\t#isAdded = false;\n\t#promoEnded = false;\n\t#cartDialog = null;\n\t#boundHandleCartDataChange = null; // pre-bound listener ref for clean-up\n\t#debounceTimer = null; // debouncing cart updates\n\t#messageAbove = null; // message when threshold is met\n\t#messageBelow = null; // message when below threshold\n\n\tstatic get observedAttributes() {\n\t\treturn ['threshold', 'current', 'variant-id', 'promo-ended', 'message-above', 'message-below'];\n\t}\n\n\tconstructor() {\n\t\tsuper();\n\t\t// read initial attributes once\n\t\tthis.#threshold = parseFloat(this.getAttribute('threshold')) || 0;\n\t\tthis.#currentAmount = parseFloat(this.getAttribute('current')) || 0;\n\t\tthis.#variantId = this.getAttribute('variant-id');\n\t\tthis.#promoEnded = this.hasAttribute('promo-ended');\n\t\tthis.#messageAbove = this.getAttribute('message-above');\n\t\tthis.#messageBelow = this.getAttribute('message-below');\n\t\tthis.#boundHandleCartDataChange = this.#handleCartDataChange.bind(this);\n\t}\n\n\tconnectedCallback() {\n\t\tthis.#render();\n\t\tthis.#attachListeners();\n\t}\n\n\tdisconnectedCallback() {\n\t\tif (this.#debounceTimer) clearTimeout(this.#debounceTimer);\n\t\tif (this.#cartDialog)\n\t\t\tthis.#cartDialog.removeEventListener(\n\t\t\t\t'cart-dialog:data-changed',\n\t\t\t\tthis.#boundHandleCartDataChange\n\t\t\t);\n\t}\n\n\t#render() {\n\t\tthis.classList.add('gift-with-purchase');\n\t\tthis.#renderMessages();\n\t}\n\n\t#renderMessages() {\n\t\t// Look for existing message element with data-content-gwp-message\n\t\tthis.#updateMessages();\n\t}\n\n\t#updateMessages() {\n\t\tconst messageEl = this.querySelector('[data-content-gwp-message]');\n\t\tif (!messageEl) return;\n\n\t\tlet message = '';\n\n\t\t// console.log('updateMessages - this.#isActive', this.#isActive);\n\n\t\tif (this.#isActive && this.#messageAbove) {\n\t\t\t// set message to above threshold message\n\t\t\tmessage = this.#messageAbove;\n\t\t} else if (!this.#isActive && this.#messageBelow) {\n\t\t\t// set message to below threshold message\n\t\t\tconst remaining = this.#threshold - this.#currentAmount;\n\t\t\tconst formattedAmount = remaining.toFixed(2).replace(/\\.00$/, '');\n\t\t\tmessage = this.#messageBelow\n\t\t\t\t.replace(/\\{\\s*amount\\s*\\}/g, formattedAmount)\n\t\t\t\t.replace(/\\{amount\\}/g, formattedAmount);\n\t\t}\n\n\t\tmessageEl.textContent = message;\n\t\tmessageEl.style.display = message ? 'block' : 'none';\n\t}\n\n\t#attachListeners() {\n\t\t// Look for cart-dialog element when attaching listeners (more reliable timing)\n\t\tthis.#cartDialog = this.closest('cart-dialog');\n\n\t\tif (this.#cartDialog) {\n\t\t\t// console.log('cartDialog exists and is attaching events');\n\t\t\tthis.#cartDialog.addEventListener(\n\t\t\t\t'cart-dialog:data-changed',\n\t\t\t\tthis.#boundHandleCartDataChange\n\t\t\t);\n\t\t} else {\n\t\t\t// Try again after a short delay in case the DOM isn't fully ready\n\t\t\tsetTimeout(() => {\n\t\t\t\t// console.log('cartDialog DIDNT exist and we waited to attach events');\n\t\t\t\tthis.#cartDialog = this.closest('cart-dialog');\n\t\t\t\tif (this.#cartDialog) {\n\t\t\t\t\tthis.#cartDialog.addEventListener(\n\t\t\t\t\t\t'cart-dialog:data-changed',\n\t\t\t\t\t\tthis.#boundHandleCartDataChange\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tconsole.error('GWP - cart-dialog still not found after delay');\n\t\t\t\t}\n\t\t\t}, 100);\n\t\t}\n\t}\n\n\t#handleCartDataChange(event) {\n\t\tconst cart = event.detail;\n\t\t// console.log('GWP - handleCartDataChange cart: ', cart.calculated_subtotal, cart);\n\n\t\tif (!cart || typeof cart.calculated_subtotal === 'undefined') return;\n\t\tif (this.#debounceTimer) clearTimeout(this.#debounceTimer);\n\n\t\tthis.#debounceTimer = setTimeout(() => {\n\t\t\tthis.#debounceTimer = null;\n\t\t\tthis.#currentAmount = parseFloat(cart.calculated_subtotal / 100) || 0;\n\t\t\tthis.#checkGiftInCart(cart);\n\t\t\tthis.#updateState(cart);\n\t\t}, 300);\n\t}\n\n\t// checks to see if the gift is already in the cart\n\t#checkGiftInCart(cart) {\n\t\tif (!cart.items || !this.#variantId) {\n\t\t\tthis.#isAdded = false;\n\t\t\treturn;\n\t\t}\n\t\tconst giftLines = cart.items.filter(\n\t\t\t(lineItem) =>\n\t\t\t\tlineItem.variant_id.toString() === this.#variantId.toString() &&\n\t\t\t\tlineItem.properties?._gwp_item === 'true'\n\t\t);\n\t\tthis.#isAdded = giftLines.length > 0;\n\t\tif (this.#promoEnded && giftLines.length) this.#removeAllGiftItems(giftLines);\n\t}\n\n\t#updateState(cart) {\n\t\t// console.log('********** ---------- Updating state....');\n\t\tconst wasActive = this.#isActive;\n\t\tthis.#isActive = this.#currentAmount >= this.#threshold && !this.#promoEnded;\n\n\t\t// console.log('********** ---------- this.#isActive', this.#isActive);\n\n\t\tif (this.#promoEnded) {\n\t\t\t// remove GWP from cart\n\t\t\tthis.#removeGiftFromCart(cart);\n\t\t}\n\n\t\tif (this.#isActive && !wasActive && !this.#isAdded && this.#variantId) {\n\t\t\tthis.#addGiftToCart();\n\t\t} else if (!this.#isActive && wasActive && this.#isAdded && this.#variantId) {\n\t\t\tthis.#removeGiftFromCart(cart);\n\t\t}\n\n\t\tthis.#updateVisualState();\n\t\tthis.#updateMessages();\n\t}\n\n\t#updateVisualState() {\n\t\tif (this.#promoEnded) {\n\t\t\tthis.setAttribute('state', 'ended');\n\t\t\tthis.style.display = 'none';\n\t\t\treturn;\n\t\t}\n\n\t\tthis.style.display = '';\n\t\tif (this.#isAdded) {\n\t\t\tthis.setAttribute('state', 'added');\n\t\t} else if (this.#isActive) {\n\t\t\tthis.setAttribute('state', 'active');\n\t\t}\n\t\t// Note: no 'inactive' state since component wouldn't be loaded if inactive\n\t}\n\n\tasync #addGiftToCart() {\n\t\ttry {\n\t\t\tconst res = await fetch('/cart/add.js', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tcredentials: 'same-origin',\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\titems: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: this.#variantId,\n\t\t\t\t\t\t\tquantity: 1,\n\t\t\t\t\t\t\tproperties: {\n\t\t\t\t\t\t\t\t_gwp_item: 'true',\n\t\t\t\t\t\t\t\t_hide_in_cart: 'true',\n\t\t\t\t\t\t\t\t_ignore_price_in_subtotal: 'true',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t}),\n\t\t\t});\n\t\t\tif (!res.ok) throw new Error(`http ${res.status}`);\n\t\t\tawait res.json();\n\t\t\tthis.#isAdded = true;\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:added', {\n\t\t\t\t\tdetail: { variantId: this.#variantId },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error('giftwithpurchase: add error', err);\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:error', {\n\t\t\t\t\tdetail: { action: 'add', error: err.message },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t}\n\n\tasync #removeGiftFromCart(cart) {\n\t\ttry {\n\t\t\t// get all GWP items in the cart\n\t\t\tconst giftLines = cart.items.filter(\n\t\t\t\t(lineItem) =>\n\t\t\t\t\tlineItem.variant_id.toString() === this.#variantId.toString() &&\n\t\t\t\t\tlineItem.properties?._gwp_item === 'true'\n\t\t\t);\n\n\t\t\t// exit if no items in the cart\n\t\t\tif (!giftLines.length) {\n\t\t\t\tthis.#isAdded = false;\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// remove all GWP items from the cart\n\t\t\tawait this.#removeAllGiftItems(giftLines);\n\t\t} catch (err) {\n\t\t\tconsole.error('giftwithpurchase: remove error', err);\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:error', {\n\t\t\t\t\tdetail: { action: 'remove', error: err.message },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t}\n\n\tasync #removeAllGiftItems(giftLines) {\n\t\ttry {\n\t\t\tawait Promise.all(\n\t\t\t\tgiftLines.map((giftItem) =>\n\t\t\t\t\tfetch('/cart/change.js', {\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\tcredentials: 'same-origin',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t'X-Requested-With': 'XMLHttpRequest',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify({ id: giftItem.key, quantity: 0 }),\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t);\n\t\t\tthis.#isAdded = false;\n\t\t\t// note: you can broadcast cart again here if you want real-time re-render after remove\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:removed', {\n\t\t\t\t\tdetail: { variantId: this.#variantId },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tconsole.error('giftwithpurchase: bulk remove error', err);\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent('gwp:error', {\n\t\t\t\t\tdetail: { action: 'remove', error: err.message },\n\t\t\t\t\tbubbles: true,\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\t}\n\n\tgetState() {\n\t\treturn {\n\t\t\tcurrentAmount: this.#currentAmount,\n\t\t\tthreshold: this.#threshold,\n\t\t\tvariantId: this.#variantId,\n\t\t\tisActive: this.#isActive,\n\t\t\tisAdded: this.#isAdded,\n\t\t\tpromoEnded: this.#promoEnded,\n\t\t\tremainingAmount: Math.max(0, this.#threshold - this.#currentAmount),\n\t\t};\n\t}\n\n\tget currentAmount() {\n\t\treturn this.#currentAmount;\n\t}\n\tget threshold() {\n\t\treturn this.#threshold;\n\t}\n\tget variantId() {\n\t\treturn this.#variantId;\n\t}\n\tget isActive() {\n\t\treturn this.#isActive;\n\t}\n\tget isAdded() {\n\t\treturn this.#isAdded;\n\t}\n\tget promoEnded() {\n\t\treturn this.#promoEnded;\n\t}\n\n\t// Public setter methods for programmatic control\n\tsetCurrentAmount(amount) {\n\t\tthis.#currentAmount = parseFloat(amount) || 0;\n\t\tthis.#updateState({ items: [] }); // Pass empty cart to avoid cart operations\n\t\tthis.#updateMessages();\n\t}\n\n\tsetThreshold(threshold) {\n\t\tthis.#threshold = parseFloat(threshold) || 0;\n\t\tthis.#updateState({ items: [] }); // Pass empty cart to avoid cart operations\n\t\tthis.#updateMessages();\n\t}\n\n\tsetVariantId(variantId) {\n\t\tthis.#variantId = variantId;\n\t}\n}\n\nif (!customElements.get('gift-with-purchase')) {\n\tcustomElements.define('gift-with-purchase', GiftWithPurchase);\n}\n\nexport { GiftWithPurchase };\n"],"names":["GiftWithPurchase","HTMLElement","threshold","currentAmount","variantId","isActive","isAdded","promoEnded","cartDialog","boundHandleCartDataChange","debounceTimer","messageAbove","messageBelow","observedAttributes","constructor","super","this","parseFloat","getAttribute","hasAttribute","handleCartDataChange","bind","connectedCallback","render","attachListeners","disconnectedCallback","clearTimeout","removeEventListener","classList","add","renderMessages","updateMessages","messageEl","querySelector","message","formattedAmount","toFixed","replace","textContent","style","display","closest","addEventListener","setTimeout","console","error","event","cart","detail","calculated_subtotal","checkGiftInCart","updateState","items","giftLines","filter","lineItem","variant_id","toString","properties","_gwp_item","length","removeAllGiftItems","wasActive","removeGiftFromCart","addGiftToCart","updateVisualState","setAttribute","res","fetch","method","credentials","headers","body","JSON","stringify","id","quantity","_hide_in_cart","_ignore_price_in_subtotal","ok","Error","status","json","dispatchEvent","CustomEvent","bubbles","err","action","Promise","all","map","giftItem","key","getState","remainingAmount","Math","max","setCurrentAmount","amount","setThreshold","setVariantId","customElements","get","define"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SCSS Variables (can be overridden before import)
|
|
2
|
+
$gwp-border-radius: 8px !default;
|
|
3
|
+
$gwp-padding: 1rem !default;
|
|
4
|
+
$gwp-gap: 1rem !default;
|
|
5
|
+
|
|
6
|
+
// Colors
|
|
7
|
+
$gwp-bg-active: #e8f5e8 !default;
|
|
8
|
+
$gwp-bg-added: #d4edda !default;
|
|
9
|
+
$gwp-border-active: #28a745 !default;
|
|
10
|
+
$gwp-border-added: #155724 !default;
|
|
11
|
+
$gwp-text-active: #155724 !default;
|
|
12
|
+
$gwp-text-added: #155724 !default;
|
|
13
|
+
|
|
14
|
+
// Gift with purchase component styles
|
|
15
|
+
gift-with-purchase {
|
|
16
|
+
// CSS Custom Properties for customization
|
|
17
|
+
--gwp-border-radius: #{$gwp-border-radius};
|
|
18
|
+
--gwp-padding: #{$gwp-padding};
|
|
19
|
+
--gwp-gap: #{$gwp-gap};
|
|
20
|
+
--gwp-bg-active: #{$gwp-bg-active};
|
|
21
|
+
--gwp-bg-added: #{$gwp-bg-added};
|
|
22
|
+
--gwp-border-active: #{$gwp-border-active};
|
|
23
|
+
--gwp-border-added: #{$gwp-border-added};
|
|
24
|
+
--gwp-text-active: #{$gwp-text-active};
|
|
25
|
+
--gwp-text-added: #{$gwp-text-added};
|
|
26
|
+
|
|
27
|
+
display: block;
|
|
28
|
+
border: 2px solid var(--gwp-border-active);
|
|
29
|
+
border-radius: var(--gwp-border-radius);
|
|
30
|
+
padding: var(--gwp-padding);
|
|
31
|
+
background-color: var(--gwp-bg-active);
|
|
32
|
+
color: var(--gwp-text-active);
|
|
33
|
+
|
|
34
|
+
// Basic product layout - image left, content right
|
|
35
|
+
.gwp-product {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: flex-start;
|
|
38
|
+
gap: var(--gwp-gap);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Basic image styling
|
|
42
|
+
[data-gwp-image] {
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Content takes remaining space
|
|
47
|
+
.gwp-content {
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// State: Active (threshold reached, gift not yet added)
|
|
53
|
+
&[state='active'] {
|
|
54
|
+
background-color: var(--gwp-bg-active);
|
|
55
|
+
border-color: var(--gwp-border-active);
|
|
56
|
+
color: var(--gwp-text-active);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// State: Added (gift in cart)
|
|
60
|
+
&[state='added'] {
|
|
61
|
+
background-color: var(--gwp-bg-added);
|
|
62
|
+
border-color: var(--gwp-border-added);
|
|
63
|
+
color: var(--gwp-text-added);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// State: Ended (promo ended - component hidden)
|
|
67
|
+
&[state='ended'] {
|
|
68
|
+
display: none;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@magic-spells/gift-with-purchase",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Gift with purchase web component for e-commerce threshold promotions.",
|
|
5
|
+
"author": "Cory Schulz",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/gift-with-purchase.cjs.js",
|
|
9
|
+
"module": "dist/gift-with-purchase.esm.js",
|
|
10
|
+
"unpkg": "dist/gift-with-purchase.min.js",
|
|
11
|
+
"style": "dist/gift-with-purchase.min.css",
|
|
12
|
+
"sass": "dist/gift-with-purchase.scss",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/gift-with-purchase.esm.js",
|
|
16
|
+
"require": "./dist/gift-with-purchase.cjs.js",
|
|
17
|
+
"default": "./dist/gift-with-purchase.esm.js"
|
|
18
|
+
},
|
|
19
|
+
"./css": "./dist/gift-with-purchase.css",
|
|
20
|
+
"./css/min": "./dist/gift-with-purchase.min.css",
|
|
21
|
+
"./scss": "./dist/gift-with-purchase.scss"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": true,
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/magic-spells/gift-with-purchase"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/magic-spells/gift-with-purchase#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/magic-spells/gift-with-purchase/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"gift-with-purchase",
|
|
34
|
+
"e-commerce",
|
|
35
|
+
"web-components",
|
|
36
|
+
"shopify",
|
|
37
|
+
"custom-elements",
|
|
38
|
+
"threshold-promotion",
|
|
39
|
+
"cart-promotion"
|
|
40
|
+
],
|
|
41
|
+
"files": [
|
|
42
|
+
"dist/",
|
|
43
|
+
"src/"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rollup -c",
|
|
47
|
+
"lint": "eslint src/ rollup.config.mjs",
|
|
48
|
+
"format": "prettier --write .",
|
|
49
|
+
"prepublishOnly": "npm run build",
|
|
50
|
+
"serve": "rollup -c --watch",
|
|
51
|
+
"dev": "rollup -c --watch"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"registry": "https://registry.npmjs.org/"
|
|
56
|
+
},
|
|
57
|
+
"browserslist": [
|
|
58
|
+
"last 2 versions",
|
|
59
|
+
"not dead",
|
|
60
|
+
"not ie <= 11"
|
|
61
|
+
],
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@eslint/js": "^8.57.0",
|
|
64
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
65
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
66
|
+
"eslint": "^8.0.0",
|
|
67
|
+
"globals": "^13.24.0",
|
|
68
|
+
"prettier": "^3.3.3",
|
|
69
|
+
"rollup": "^3.0.0",
|
|
70
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
71
|
+
"rollup-plugin-postcss": "^4.0.2",
|
|
72
|
+
"rollup-plugin-serve": "^1.1.1",
|
|
73
|
+
"sass": "^1.86.3"
|
|
74
|
+
}
|
|
75
|
+
}
|