The repo for Purrform's main BigCommerce store.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #6 from Teser301/merge-scripts

Moved external js back into script, some more fixes for delivery dates.

authored by

Rogerio Romao and committed by
GitHub
5f307e9d bb644856

+542 -495
-391
assets/js/custom.js
··· 1 - // This is a custom function to check if an element is ready in the DOM with MutationObserver (no timers) 2 - (function (win) { 3 - 'use strict'; 4 - 5 - var listeners = [], 6 - doc = win.document, 7 - MutationObserver = win.MutationObserver || win.WebKitMutationObserver, 8 - observer; 9 - 10 - function ready(selector, fn) { 11 - // Store the selector and callback to be monitored 12 - listeners.push({ 13 - selector: selector, 14 - fn: fn, 15 - }); 16 - if (!observer) { 17 - // Watch for changes in the document 18 - observer = new MutationObserver(check); 19 - observer.observe(doc.documentElement, { 20 - childList: true, 21 - subtree: true, 22 - }); 23 - } 24 - // Check if the element is currently in the DOM 25 - check(); 26 - } 27 - 28 - function check() { 29 - // Check the DOM for elements matching a stored selector 30 - for ( 31 - var i = 0, len = listeners.length, listener, elements; 32 - i < len; 33 - i++ 34 - ) { 35 - listener = listeners[i]; 36 - // Query for elements matching the specified selector 37 - elements = doc.querySelectorAll(listener.selector); 38 - for (var j = 0, jLen = elements.length, element; j < jLen; j++) { 39 - element = elements[j]; 40 - // Make sure the callback isn't invoked with the 41 - // same element more than once 42 - if (!element.ready) { 43 - element.ready = true; 44 - // Invoke the callback with the element 45 - listener.fn.call(element, element); 46 - } 47 - } 48 - } 49 - } 50 - 51 - // Expose `ready` 52 - win.ready = ready; 53 - })(this); 54 - 55 - // ------------------------------------------------------// 56 - // change the text on the mobile modal 57 - const mobileModalSelector = '.cartDrawer.optimizedCheckout-orderSummary'; 58 - ready(mobileModalSelector, (mobileModalElement) => { 59 - mobileModalElement.querySelector('.cartDrawer-body a').textContent = 60 - 'Show Details & Rewards'; 61 - }); 62 - 63 - // ------------------------------------------------------// 64 - // This handles 'datepicker' in 'shipping' 65 - const shippingSelector = '#checkout-shipping-options'; 66 - ready(shippingSelector, (shippingOptionsElement) => { 67 - createCalendar(shippingOptionsElement); 68 - }); 69 - 70 - // Attach the datepicker to the shipping options 71 - function createCalendar(parent) { 72 - const existingInstructions = document.getElementById('datepicker-hoster'); 73 - if (existingInstructions) { 74 - return; 75 - } 76 - 77 - // Define the HTML code for the elements 78 - const datePickerHTML = ` 79 - <div id="datepicker-hoster" class="datepicker"> 80 - <div class="cart-total-label"> 81 - <strong style="color: #687d6a;">Delivery Date *</strong> 82 - </div> 83 - <div class="voucher-section"> 84 - <input type="text" id="datepicker" name="datepicker" readonly autocomplete="off" aria-autocomplete="none" class="form-input" style="background: white; width: calc(100%); border-radius: 45px; border-color: #687d6a; color: #687d6a; font-size: 16px; text-align: center;" onkeydown="return false;"> 85 - <p id="datepicker_err" style="display: none; color: red; font-size: 16px; font-weight: normal;"></p> 86 - </div> 87 - <p style="font-size: 16px;">*Next Day delivery is usually available on any orders placed before 12 noon, Monday to Friday. Excluding some postcodes in Scotland.</p> 88 - </div> 89 - `; 90 - parent.insertAdjacentHTML('beforeend', datePickerHTML); 91 - getCalendarData(); 92 - } 93 - 94 - // Get data for the calendar 95 - function getCalendarData() { 96 - // Create calendar and attach date picker logic here 97 - let dailyDeliveryLimit = 250; 98 - const dailySlotValues = {}; 99 - const disabledDates = []; 100 - 101 - // get delivery dates from middleware 102 - fetch('https://purrform-apps-027e.onrender.com/deliveryDates') 103 - .then((response) => { 104 - if (!response.ok) { 105 - throw new Error('Network response was not ok'); 106 - } 107 - return response.json(); 108 - }) 109 - .then((data) => { 110 - disabledDates.push(...data.unavailableDates); 111 - for (const [date, slots] of Object.entries(data.dateSlots)) { 112 - dailySlotValues[date] = slots; 113 - } 114 - if (data.perDay) { 115 - dailyDeliveryLimit = data.perDay; 116 - } 117 - attachDatePicker( 118 - dailyDeliveryLimit, 119 - dailySlotValues, 120 - disabledDates 121 - ); 122 - }) 123 - .catch((error) => { 124 - console.log( 125 - 'Error calling delivery dates middleware endpoint:', 126 - error 127 - ); 128 - // Handle error if necessary 129 - }); 130 - } 131 - 132 - // Attach the calendar to the datepicker and enable/disable dates 133 - function attachDatePicker(dailyDeliveryLimit, dailySlotValues, disabledDates) { 134 - function disableDates(day) { 135 - // day gets implicitly passed by jQuery Calendar 136 - const selectedShippingMethod = document.querySelector( 137 - '.form-checklist-item--selected' 138 - ); 139 - 140 - const isSaturdayDelivery = selectedShippingMethod 141 - .querySelector('.shippingOption-desc') 142 - .textContent.toLowerCase() 143 - .includes('saturday'); 144 - 145 - return isSaturdayDelivery ? enableSaturday(day) : enableWeekdays(day); 146 - } 147 - 148 - // Disable everything except Saturdays 149 - function enableSaturday(day) { 150 - const dayBeingCheckedFormatted = $.datepicker.formatDate( 151 - 'yy-mm-dd', 152 - day 153 - ); 154 - 155 - const dayOfTheWeek = day.getDay(); 156 - const saturdayDay = 6; 157 - 158 - if (dayOfTheWeek !== saturdayDay) { 159 - return [false, '', '']; // disabled boolean, css class, tooltip 160 - } 161 - 162 - let slotsForThisDay = 163 - dailySlotValues[dayBeingCheckedFormatted] ?? dailyDeliveryLimit; 164 - if (slotsForThisDay > dailyDeliveryLimit) { 165 - // this should never happen, but just in case 166 - slotsForThisDay = dailyDeliveryLimit; 167 - } 168 - 169 - // Enable check 170 - const isSaturdayEnabled = 171 - slotsForThisDay > 0 && 172 - !disabledDates.includes(dayBeingCheckedFormatted); 173 - 174 - return [isSaturdayEnabled, '', `Slots available: ${slotsForThisDay}`]; // enabled boolean, css class, tooltip 175 - } 176 - 177 - // Enable weekdays 178 - function enableWeekdays(day) { 179 - const dayBeingCheckedFormatted = $.datepicker.formatDate( 180 - 'yy-mm-dd', 181 - day 182 - ); 183 - 184 - const dayOfTheWeek = day.getDay(); 185 - // Disable if 6 (Saturday) 0 (Sunday) or 1 (Monday) 186 - if (dayOfTheWeek === 6 || dayOfTheWeek === 0 || dayOfTheWeek === 1) { 187 - return [false, '', '']; // disabled boolean, css class, tooltip 188 - } 189 - 190 - let slotsForThisDay = 191 - dailySlotValues[dayBeingCheckedFormatted] ?? dailyDeliveryLimit; 192 - if (slotsForThisDay > dailyDeliveryLimit) { 193 - // this should never happen, but just in case 194 - slotsForThisDay = dailyDeliveryLimit; 195 - } 196 - 197 - const isEnabledDay = 198 - slotsForThisDay > 0 && 199 - !disabledDates.includes(dayBeingCheckedFormatted); 200 - 201 - return [isEnabledDay, '', `Slots available: ${slotsForThisDay}`]; // enabled boolean, css class, tooltip 202 - } 203 - 204 - $('#datepicker').datepicker({ 205 - beforeShowDay: disableDates, 206 - onClose: function () { 207 - const datepicker = document.querySelector('#datepicker'); 208 - const deliveryDate = datepicker.value; 209 - if (deliveryDate) { 210 - sessionStorage.setItem('deliveryDate', deliveryDate); 211 - } 212 - }, 213 - defaultDate: '+1D', 214 - minDate: '+1D', 215 - maxDate: '+21D', 216 - dateFormat: 'd MM, yy', 217 - showOtherMonths: true, 218 - firstDay: 1, 219 - placeholder: 'Select Date', 220 - }); 221 - 222 - $('#datepicker').attr('placeholder', 'Select Date'); 223 - } 224 - 225 - // ------------------------------------------------------// 226 - // handle proceed button 227 - const proceedButtonSelector = '#proceedButton'; 228 - ready(proceedButtonSelector, (proceedButton) => { 229 - proceedButton.addEventListener('click', function (e) { 230 - e.preventDefault(); 231 - const deliveryDate = sessionStorage.getItem('deliveryDate'); 232 - const customerMessage = sessionStorage.getItem('customerMessage'); 233 - const realContinueButton = document.querySelector( 234 - '#checkout-shipping-continue' 235 - ); 236 - 237 - const datepickerErrorElement = 238 - document.getElementById('datepicker_err'); 239 - 240 - if (!deliveryDate) { 241 - datepickerErrorElement.textContent = 242 - 'Please select a delivery date before proceeding.'; 243 - datepickerErrorElement.style.display = 'block'; 244 - return; 245 - } 246 - // Proceed with the form submission 247 - datepickerErrorElement.style.display = 'none'; 248 - 249 - const selectElement = document.querySelector('#delivery_inst_tag'); 250 - const selectedValue = selectElement.value; 251 - const deliveryInstructions = 252 - selectedValue === 'user_instruction' && customerMessage.length > 0 253 - ? customerMessage 254 - : 'Will be in'; 255 - 256 - const fullInstructions = `${deliveryDate} | ${deliveryInstructions}`; 257 - 258 - // Update the customer message with the delivery instructions 259 - const checkoutId = jsContext.checkoutId; 260 - fetch(`/api/storefront/checkouts/${checkoutId}`, { 261 - method: 'PUT', 262 - headers: { 263 - 'Content-Type': 'application/json', 264 - }, 265 - body: JSON.stringify({ 266 - customerMessage: fullInstructions, 267 - }), 268 - }) 269 - .then((response) => { 270 - if (!response.ok) { 271 - throw new Error('PUT request failed'); 272 - } 273 - return response.json(); // Parse the JSON response 274 - }) 275 - .then((data) => { 276 - sessionStorage.removeItem('deliveryDate'); 277 - sessionStorage.removeItem('customerMessage'); 278 - realContinueButton.click(); 279 - }) 280 - .catch((error) => { 281 - console.error('Error:', error); 282 - }); 283 - }); 284 - }); 285 - 286 - // ------------------------------------------------------// 287 - // This handles the delivery details / customer message 288 - const deliveryDetailsSelector = 289 - 'fieldset.form-fieldset[data-test="checkout-shipping-comments"]'; 290 - ready(deliveryDetailsSelector, (deliveryDetailsElement) => { 291 - const existingInstructions = document.getElementById('instruction-wrap'); 292 - if (existingInstructions) { 293 - return; 294 - } 295 - 296 - // generate custom HTML 297 - const instructionsDiv = ` 298 - <div id="instruction-wrap" class="delivery_instruction_wrapper"> 299 - <div class="del_cont"> 300 - <label class="form-legend optimizedCheckout-headingSecondary">Delivery Details</label> 301 - <select class="form-select form-select--small" id="delivery_inst_tag"> 302 - <option value="Will be in">Will be in</option> 303 - <option value="user_instruction">User instruction</option> 304 - </select> 305 - <textarea style="display:none" name="delivery_user_text" id="delivery_user_text" rows="3" cols="40" class="form-input"></textarea> 306 - </div> 307 - <div id="proceedButton">Continue</div> 308 - </div> 309 - `; 310 - 311 - deliveryDetailsElement.insertAdjacentHTML('afterend', instructionsDiv); 312 - 313 - const selectElement = document.getElementById('delivery_inst_tag'); 314 - const textarea = document.getElementById('delivery_user_text'); 315 - 316 - selectElement.addEventListener('change', function () { 317 - const selectedValue = selectElement.value; 318 - if (selectedValue === 'user_instruction') { 319 - textarea.style.display = 'block'; 320 - textarea.style.margin = '10px 0'; 321 - textarea.focus(); 322 - } else { 323 - textarea.style.display = 'none'; 324 - } 325 - }); 326 - 327 - textarea.addEventListener('input', function () { 328 - const deliveryInstructions = textarea.value; 329 - sessionStorage.setItem('customerMessage', deliveryInstructions); 330 - }); 331 - }); 332 - 333 - // ------------------------------------------------------// 334 - // This handles the payment continue button 335 - const paymentContinueButtonSelector = '#checkout-payment-continue'; 336 - ready(paymentContinueButtonSelector, (paymentContinueButton) => { 337 - const fakeButton = document.createElement('div'); 338 - const missingWarn = document.createElement('p'); 339 - const parent = document.querySelector( 340 - '.checkout-step--payment .checkout-form .form-actions' 341 - ); 342 - 343 - // hide the real button 344 - paymentContinueButton.style.display = 'none'; 345 - 346 - // Generate placeholder button 347 - fakeButton.innerHTML = 'Place Order'; 348 - fakeButton.id = 'fakeButton'; 349 - fakeButton.style.display = 'block'; 350 - 351 - // Generate warning text 352 - missingWarn.classList.add('warning'); 353 - missingWarn.textContent = 354 - "Cannot find delivery date. Please review your 'shipping' details."; 355 - missingWarn.style.display = 'none'; 356 - 357 - // Append the button to the body of the HTML document 358 - parent.appendChild(missingWarn); 359 - parent.appendChild(fakeButton); 360 - 361 - fakeButton.addEventListener('click', () => { 362 - const checkoutId = jsContext.checkoutId; 363 - fetch(`/api/storefront/checkouts/${checkoutId}`, { 364 - method: 'GET', 365 - headers: { 366 - 'Content-Type': 'application/json', 367 - }, 368 - }) 369 - .then((response) => { 370 - if (!response.ok) { 371 - throw new Error('GET request failed'); 372 - } 373 - return response.json(); // Parse the JSON response 374 - }) 375 - .then((data) => { 376 - const deliveryInstructions = data.customerMessage; 377 - 378 - if (deliveryInstructions?.trim()) { 379 - paymentContinueButton.click(); 380 - } else { 381 - missingWarn.style.display = 'block'; 382 - } 383 - }) 384 - .catch((error) => { 385 - console.error('Error:', error); 386 - missingWarn.textContent = 387 - 'Something went wrong. Check if you have set a delivery date in shipping and try again.'; 388 - missingWarn.style.display = 'block'; 389 - }); 390 - }); 391 - });
+1 -1
config.json
··· 1 1 { 2 - "name": "DEV Theme - Fix loyalty points", 2 + "name": "DEV Theme - delivery dates refactored", 3 3 "version": "6.10.0", 4 4 "template_engine": "handlebars_v4", 5 5 "meta": {
+541 -103
templates/pages/checkout.html
··· 40 40 41 41 {{inject "checkoutId" cart_id}} 42 42 43 - <script src="{{cdn 'assets/js/custom.js'}}"></script> 43 + <script> 44 + // This is a custom function to check if an element is ready in the DOM with MutationObserver (no timers) 45 + (function (win) { 46 + 'use strict'; 47 + 48 + var listeners = [], 49 + doc = win.document, 50 + MutationObserver = 51 + win.MutationObserver || win.WebKitMutationObserver, 52 + observer; 53 + 54 + function ready(selector, fn) { 55 + // Store the selector and callback to be monitored 56 + listeners.push({ 57 + selector: selector, 58 + fn: fn, 59 + }); 60 + if (!observer) { 61 + // Watch for changes in the document 62 + observer = new MutationObserver(check); 63 + observer.observe(doc.documentElement, { 64 + childList: true, 65 + subtree: true, 66 + }); 67 + } 68 + // Check if the element is currently in the DOM 69 + check(); 70 + } 71 + 72 + function check() { 73 + // Check the DOM for elements matching a stored selector 74 + for ( 75 + var i = 0, len = listeners.length, listener, elements; 76 + i < len; 77 + i++ 78 + ) { 79 + listener = listeners[i]; 80 + // Query for elements matching the specified selector 81 + elements = doc.querySelectorAll(listener.selector); 82 + for ( 83 + var j = 0, jLen = elements.length, element; 84 + j < jLen; 85 + j++ 86 + ) { 87 + element = elements[j]; 88 + // Make sure the callback isn't invoked with the 89 + // same element more than once 90 + if (!element.ready) { 91 + element.ready = true; 92 + // Invoke the callback with the element 93 + listener.fn.call(element, element); 94 + } 95 + } 96 + } 97 + } 98 + 99 + // Expose `ready` 100 + win.ready = ready; 101 + })(this); 102 + </script> 44 103 45 104 {{#if customer_group_name '!==' 'trade_blocked'}} 46 105 <div id="optimised_main_chout"> ··· 93 152 </div> 94 153 {{/if}} {{#if customer}} {{#unless customer_group_name '===' 'Trade'}} 95 154 <script> 96 - // Handles the loyalty points 97 - var jsContext = JSON.parse({{jsContext}}); 98 - const checkoutId = jsContext.checkoutId; 155 + // Handles the loyalty points 99 156 100 - const parentSelectorDesktop = '.cart.optimizedCheckout-orderSummary' 101 - const parentSelectorMobile = '.cart-modal-body.optimizedCheckout-orderSummary' 157 + const parentSelectorDesktop = '.cart.optimizedCheckout-orderSummary'; 158 + const parentSelectorMobile = 159 + '.cart-modal-body.optimizedCheckout-orderSummary'; 102 160 103 - ready(parentSelectorDesktop, (parentElement) => { 104 - loyaltyInitialize(parentElement); 105 - }) 161 + ready(parentSelectorDesktop, (parentElement) => { 162 + loyaltyInitialize(parentElement); 163 + }); 106 164 107 - ready(parentSelectorMobile, (parentElement) => { 108 - loyaltyInitialize(parentElement); 109 - }) 165 + ready(parentSelectorMobile, (parentElement) => { 166 + loyaltyInitialize(parentElement); 167 + }); 110 168 111 - function loyaltyInitialize(elementToAppend) { 169 + function loyaltyInitialize(elementToAppend) { 112 170 // Fetch loyalty points 113 - fetchLoyalty().then(points => { 171 + fetchLoyalty().then((points) => { 114 172 // Create a new div element for displaying loyalty points 115 173 const pointsDiv = document.createElement('section'); 116 174 const pointsDesc = document.createElement('p'); 117 - pointsDesc.id = 'pointsDesc' 175 + pointsDesc.id = 'pointsDesc'; 118 176 pointsDesc.innerHTML = `Loyalty points balance ${points}`; 119 177 pointsDiv.appendChild(pointsDesc); 120 - pointsDiv.className = 'cart-section optimizedCheckout-orderSummary-cartSection loyalty-points'; 178 + pointsDiv.className = 179 + 'cart-section optimizedCheckout-orderSummary-cartSection loyalty-points'; 121 180 // Create a label and input for entering points to use 122 181 const label = document.createElement('label'); 123 182 label.setAttribute('for', 'points-input'); 124 - label.classList.add('loyaltyLabel') 183 + label.classList.add('loyaltyLabel'); 125 184 pointsDiv.appendChild(label); 126 185 127 186 const pointsInput = document.createElement('input'); ··· 132 191 pointsInput.placeholder = 'How many?'; 133 192 pointsDiv.appendChild(pointsInput); 134 193 135 - const confirmButton = document.createElement('button') 136 - confirmButton.textContent = "Apply discount"; 137 - confirmButton.id = "loyaltyPointsBtn"; 138 - confirmButton.className = "btn btn-primary"; 194 + const confirmButton = document.createElement('button'); 195 + confirmButton.textContent = 'Apply discount'; 196 + confirmButton.id = 'loyaltyPointsBtn'; 197 + confirmButton.className = 'btn btn-primary'; 139 198 pointsDiv.appendChild(confirmButton); 140 199 141 - const cancelButton = document.createElement('button') 142 - cancelButton.textContent = "Cancel discount"; 143 - cancelButton.id = "cancelLoyaltyPointsBtn"; 144 - cancelButton.className = "btn btn-secondary"; 145 - cancelButton.style.display = document.querySelector('[data-test="cart-discount"] [data-test="cart-price-value"]') ? 'block' : 'none'; 200 + const cancelButton = document.createElement('button'); 201 + cancelButton.textContent = 'Cancel discount'; 202 + cancelButton.id = 'cancelLoyaltyPointsBtn'; 203 + cancelButton.className = 'btn btn-secondary'; 204 + cancelButton.style.display = document.querySelector( 205 + '[data-test="cart-discount"] [data-test="cart-price-value"]' 206 + ) 207 + ? 'block' 208 + : 'none'; 146 209 pointsDiv.appendChild(cancelButton); 147 210 148 211 const errorMessage = document.createElement('p'); 149 212 errorMessage.id = 'usedPointsError'; 150 213 errorMessage.classList.add('warning'); 151 - errorMessage.textContent = 'There was an error applying the discount. Please try again later.'; 214 + errorMessage.textContent = 215 + 'There was an error applying the discount. Please try again later.'; 152 216 errorMessage.style.display = 'none'; 153 217 pointsDiv.appendChild(errorMessage); 154 218 155 219 // Mount the entire loyalty points div to the parent element 156 220 elementToAppend.appendChild(pointsDiv); 157 221 158 - generateNewSummary(points) 222 + generateNewSummary(points); 159 223 160 224 loyaltyPointsBtn.addEventListener('click', () => { 161 225 if (pointsInput.value === '') { 162 226 return; 163 227 } 164 228 165 - loyaltyPointsBtn.setAttribute('disabled', 'true') 166 - loyaltyPointsBtn.textContent = 'Processing' 229 + loyaltyPointsBtn.setAttribute('disabled', 'true'); 230 + loyaltyPointsBtn.textContent = 'Processing'; 167 231 168 - fetch(`https://purrform-apps-027e.onrender.com/applyDiscountToCheckout?checkoutId=${checkoutId}&loyaltyPointsUsed=${pointsInput.value}`) 232 + fetch( 233 + `https://purrform-apps-027e.onrender.com/applyDiscountToCheckout?checkoutId={{cart_id}}&loyaltyPointsUsed=${pointsInput.value}` 234 + ) 169 235 .then((response) => { 170 236 if (!response.ok) { 171 237 // do something here to let the user know the discount failed 172 - throw new Error('Failed to apply discount') 238 + throw new Error('Failed to apply discount'); 173 239 } 174 240 175 - loyaltyPointsBtn.removeAttribute('disabled') 241 + loyaltyPointsBtn.removeAttribute('disabled'); 176 242 errorMessage.style.display = 'none'; 177 243 cancelButton.style.display = 'block'; 178 244 window.location.reload(); 179 - 180 245 }) 181 - .catch(err => { 246 + .catch((err) => { 182 247 // do something here to let the user know the discount failed 183 - loyaltyPointsBtn.removeAttribute('disabled') 184 - loyaltyPointsBtn.textContent = 'Apply discount' 185 - pointsInput.value = '' 248 + loyaltyPointsBtn.removeAttribute('disabled'); 249 + loyaltyPointsBtn.textContent = 'Apply discount'; 250 + pointsInput.value = ''; 186 251 errorMessage.style.display = 'block'; 187 - }) 188 - }) 252 + }); 253 + }); 189 254 190 255 cancelButton.addEventListener('click', () => { 191 - cancelButton.setAttribute('disabled', 'true') 192 - cancelButton.textContent = 'Removing...' 193 - fetch(`https://purrform-apps-027e.onrender.com/applyDiscountToCheckout?checkoutId=${checkoutId}&loyaltyPointsUsed=0`) 256 + cancelButton.setAttribute('disabled', 'true'); 257 + cancelButton.textContent = 'Removing...'; 258 + fetch( 259 + `https://purrform-apps-027e.onrender.com/applyDiscountToCheckout?checkoutId={{cart_id}}&loyaltyPointsUsed=0` 260 + ) 194 261 .then((response) => { 195 262 if (!response.ok) { 196 - throw new Error('Failed to remove discount') 263 + throw new Error('Failed to remove discount'); 197 264 } 198 - cancelButton.removeAttribute('disabled') 265 + cancelButton.removeAttribute('disabled'); 199 266 errorMessage.style.display = 'none'; 200 267 cancelButton.style.display = 'none'; 201 268 window.location.reload(); 202 - 203 - }) 204 - .catch(err => { 205 - // do something here to let the user know the discount failed 206 - cancelButton.removeAttribute('disabled') 207 - cancelButton.textContent = 'Remove Discount' 208 - pointsInput.value = '' 209 - errorMessage.style.display = 'block'; 210 - }) 211 - 212 - }) 213 - 214 - }) 269 + }) 270 + .catch((err) => { 271 + // do something here to let the user know the discount failed 272 + cancelButton.removeAttribute('disabled'); 273 + cancelButton.textContent = 'Remove Discount'; 274 + pointsInput.value = ''; 275 + errorMessage.style.display = 'block'; 276 + }); 277 + }); 278 + }); 215 279 } 216 280 217 281 function generateNewSummary(currentUserPoints) { 218 282 const masterContainer = document.createElement('div'); 219 283 const masterText = document.createElement('p'); 220 - masterText.classList.add("loyalty-summary") 221 - masterText.textContent = "Loyalty Points Summary" 222 - masterContainer.appendChild(masterText) 284 + masterText.classList.add('loyalty-summary'); 285 + masterText.textContent = 'Loyalty Points Summary'; 286 + masterContainer.appendChild(masterText); 223 287 224 - const discountEl = document.querySelector('[data-test="cart-discount"] [data-test="cart-price-value"]') 225 - const subtotalEl = document.querySelector('[data-test="cart-subtotal"] [data-test="cart-price-value"]') 226 - const couponDiscountEl = document.querySelector('[data-test="cart-coupon"] [data-test="cart-price-value"]') 288 + const discountEl = document.querySelector( 289 + '[data-test="cart-discount"] [data-test="cart-price-value"]' 290 + ); 291 + const subtotalEl = document.querySelector( 292 + '[data-test="cart-subtotal"] [data-test="cart-price-value"]' 293 + ); 294 + const couponDiscountEl = document.querySelector( 295 + '[data-test="cart-coupon"] [data-test="cart-price-value"]' 296 + ); 227 297 const pointsInput = document.getElementById('points-input'); 228 298 229 299 let discountPriceValue = 0; ··· 231 301 let earningPriceValue = 0; 232 302 let earningLoyalPointValue = 0; 233 303 234 - 235 304 if (discountEl) { 236 305 // Discount Div 237 306 const discountContainer = document.createElement('div'); 238 - discountContainer.classList.add('cart-summaryItem') 307 + discountContainer.classList.add('cart-summaryItem'); 239 308 240 309 const discountDescription = document.createElement('div'); 241 310 const discountValue = document.createElement('div'); 242 311 243 - discountDescription.textContent = "Points redeemed" 312 + discountDescription.textContent = 'Points redeemed'; 244 313 245 - discountPriceValue = discountEl.innerHTML.replace(/[^\d.-]/g, '') 314 + discountPriceValue = discountEl.innerHTML.replace(/[^\d.-]/g, ''); 246 315 discountPriceValue = Math.abs(Number(discountPriceValue)); 247 - discountLoyalPointValue = Math.floor(discountPriceValue * 100) 316 + discountLoyalPointValue = Math.floor(discountPriceValue * 100); 248 317 249 - discountValue.textContent = `-${discountLoyalPointValue}` 318 + discountValue.textContent = `-${discountLoyalPointValue}`; 250 319 251 - discountContainer.appendChild(discountDescription) 252 - discountContainer.appendChild(discountValue) 253 - masterContainer.appendChild(discountContainer) 320 + discountContainer.appendChild(discountDescription); 321 + discountContainer.appendChild(discountValue); 322 + masterContainer.appendChild(discountContainer); 254 323 } 255 324 if (subtotalEl) { 256 325 // Earning Div 257 326 const earningContainer = document.createElement('div'); 258 - earningContainer.classList.add('cart-summaryItem') 327 + earningContainer.classList.add('cart-summaryItem'); 259 328 260 329 const earningDescription = document.createElement('div'); 261 330 const earningValue = document.createElement('div'); 262 331 263 - earningDescription.textContent = "Points earned" 332 + earningDescription.textContent = 'Points earned'; 264 333 earningPriceValue = subtotalEl.innerHTML.replace(/[^\d.-]/g, ''); 265 334 earningPriceValue = Number(earningPriceValue); // Use parseFloat to handle decimal values 266 335 if (discountEl) { 267 - earningPriceValue = earningPriceValue - discountPriceValue 336 + earningPriceValue = earningPriceValue - discountPriceValue; 268 337 } 269 338 270 339 if (couponDiscountEl) { 271 - earningPriceValue -= Math.floor(Math.abs(Number(couponDiscountEl.innerHTML.replace(/[^\d.-]/g, '')))); 340 + earningPriceValue -= Math.floor( 341 + Math.abs( 342 + Number( 343 + couponDiscountEl.innerHTML.replace(/[^\d.-]/g, '') 344 + ) 345 + ) 346 + ); 272 347 } 273 348 274 349 earningLoyalPointValue = Math.floor(earningPriceValue / 2); 275 350 276 351 if (earningLoyalPointValue < 0) { 277 352 earningLoyalPointValue = 0; 278 - earningValue.textContent = `0` 353 + earningValue.textContent = `0`; 279 354 } else { 280 - earningValue.textContent = `+${earningLoyalPointValue}` 355 + earningValue.textContent = `+${earningLoyalPointValue}`; 281 356 } 282 357 283 - earningContainer.appendChild(earningDescription) 284 - earningContainer.appendChild(earningValue) 285 - masterContainer.appendChild(earningContainer) 358 + earningContainer.appendChild(earningDescription); 359 + earningContainer.appendChild(earningValue); 360 + masterContainer.appendChild(earningContainer); 286 361 } 287 362 288 363 const finalContainer = document.createElement('div'); ··· 291 366 292 367 const finalType = document.createElement('div'); 293 368 const finalTypeText = document.createElement('p'); 294 - finalTypeText.textContent = 'Estimated total after purchase:' 369 + finalTypeText.textContent = 'Estimated total after purchase:'; 295 370 296 371 if (discountEl) { 297 - finalResultText.textContent = `${(currentUserPoints + earningLoyalPointValue - discountLoyalPointValue)}` 372 + finalResultText.textContent = `${ 373 + currentUserPoints + 374 + earningLoyalPointValue - 375 + discountLoyalPointValue 376 + }`; 298 377 } else { 299 - finalResultText.textContent = `${currentUserPoints + earningLoyalPointValue}` 378 + finalResultText.textContent = `${ 379 + currentUserPoints + earningLoyalPointValue 380 + }`; 300 381 } 301 382 302 - finalType.appendChild(finalTypeText) 303 - finalResult.appendChild(finalResultText) 304 - finalContainer.appendChild(finalType) 305 - finalContainer.appendChild(finalResult) 306 - finalContainer.classList.add('cart-resultItem') 383 + finalType.appendChild(finalTypeText); 384 + finalResult.appendChild(finalResultText); 385 + finalContainer.appendChild(finalType); 386 + finalContainer.appendChild(finalResult); 387 + finalContainer.classList.add('cart-resultItem'); 307 388 308 - masterContainer.appendChild(finalContainer) 389 + masterContainer.appendChild(finalContainer); 309 390 pointsInput.insertAdjacentElement('afterend', masterContainer); 310 391 } 311 392 ··· 315 396 credentials: 'same-origin', 316 397 headers: { 317 398 'Content-Type': 'application/json', 318 - 'Authorization': 'Bearer {{ settings.storefront_api.token }}' 399 + Authorization: 'Bearer {{ settings.storefront_api.token }}', 319 400 }, 320 401 body: JSON.stringify({ 321 402 query: ` ··· 329 410 } 330 411 } 331 412 } 332 - ` 413 + `, 333 414 }), 334 415 }) 335 - .then(res => res.json()) 336 - .then(json => { 337 - const attribute = json.data.customer.attributes.attribute; 338 - const points = attribute ? attribute.value : '0'; // Default to '0' if attribute is not found 339 - return Number(points); 340 - }) 341 - .catch(error => { 342 - console.error('Error fetching loyalty points:', error); 343 - return 0;// Return '0' in case of error 344 - }); 416 + .then((res) => res.json()) 417 + .then((json) => { 418 + const attribute = json.data.customer.attributes.attribute; 419 + const points = attribute ? attribute.value : '0'; // Default to '0' if attribute is not found 420 + return Number(points); 421 + }) 422 + .catch((error) => { 423 + console.error('Error fetching loyalty points:', error); 424 + return 0; // Return '0' in case of error 425 + }); 345 426 } 346 427 </script> 347 428 {{/unless}} {{/if}} 429 + 430 + <script> 431 + // ------------------------------------------------------// 432 + // change the text on the mobile modal 433 + const mobileModalSelector = '.cartDrawer.optimizedCheckout-orderSummary'; 434 + ready(mobileModalSelector, (mobileModalElement) => { 435 + mobileModalElement.querySelector('.cartDrawer-body a').textContent = 436 + 'Show Details & Rewards'; 437 + }); 438 + 439 + // ------------------------------------------------------// 440 + // This handles 'datepicker' in 'shipping' 441 + const shippingSelector = '#checkout-shipping-options'; 442 + ready(shippingSelector, (shippingOptionsElement) => { 443 + createCalendar(shippingOptionsElement); 444 + }); 445 + 446 + // Attach the datepicker to the shipping options 447 + function createCalendar(parent) { 448 + const existingInstructions = 449 + document.getElementById('datepicker-hoster'); 450 + if (existingInstructions) { 451 + return; 452 + } 453 + 454 + // Define the HTML code for the elements 455 + const datePickerHTML = ` 456 + <div id="datepicker-hoster" class="datepicker"> 457 + <div class="cart-total-label"> 458 + <strong style="color: #687d6a;">Delivery Date *</strong> 459 + </div> 460 + <div class="voucher-section"> 461 + <input type="text" id="datepicker" name="datepicker" readonly autocomplete="off" aria-autocomplete="none" class="form-input" style="background: white; width: calc(100%); border-radius: 45px; border-color: #687d6a; color: #687d6a; font-size: 16px; text-align: center;" onkeydown="return false;"> 462 + <p id="datepicker_err" style="display: none; color: red; font-size: 16px; font-weight: normal;"></p> 463 + </div> 464 + <p style="font-size: 16px;">*Next Day delivery is usually available on any orders placed before 12 noon, Monday to Friday. Excluding some postcodes in Scotland.</p> 465 + </div> 466 + `; 467 + parent.insertAdjacentHTML('beforeend', datePickerHTML); 468 + getCalendarData(); 469 + } 470 + 471 + // Get data for the calendar 472 + function getCalendarData() { 473 + // Create calendar and attach date picker logic here 474 + let dailyDeliveryLimit = 250; 475 + const dailySlotValues = {}; 476 + const disabledDates = []; 477 + 478 + // get delivery dates from middleware 479 + fetch('https://purrform-apps-027e.onrender.com/deliveryDates') 480 + .then((response) => { 481 + if (!response.ok) { 482 + throw new Error('Network response was not ok'); 483 + } 484 + return response.json(); 485 + }) 486 + .then((data) => { 487 + disabledDates.push(...data.unavailableDates); 488 + for (const [date, slots] of Object.entries(data.dateSlots)) { 489 + dailySlotValues[date] = slots; 490 + } 491 + if (data.perDay) { 492 + dailyDeliveryLimit = data.perDay; 493 + } 494 + attachDatePicker( 495 + dailyDeliveryLimit, 496 + dailySlotValues, 497 + disabledDates 498 + ); 499 + }) 500 + .catch((error) => { 501 + console.log( 502 + 'Error calling delivery dates middleware endpoint:', 503 + error 504 + ); 505 + // Handle error if necessary 506 + }); 507 + } 508 + 509 + // Attach the calendar to the datepicker and enable/disable dates 510 + function attachDatePicker( 511 + dailyDeliveryLimit, 512 + dailySlotValues, 513 + disabledDates 514 + ) { 515 + function disableDates(day) { 516 + // day gets implicitly passed by jQuery Calendar 517 + const selectedShippingMethod = document.querySelector( 518 + '.form-checklist-item--selected' 519 + ); 520 + 521 + const isSaturdayDelivery = selectedShippingMethod 522 + .querySelector('.shippingOption-desc') 523 + .textContent.toLowerCase() 524 + .includes('saturday'); 525 + 526 + return isSaturdayDelivery 527 + ? enableSaturday(day) 528 + : enableWeekdays(day); 529 + } 530 + 531 + // Disable everything except Saturdays 532 + function enableSaturday(day) { 533 + const dayBeingCheckedFormatted = $.datepicker.formatDate( 534 + 'yy-mm-dd', 535 + day 536 + ); 537 + 538 + const dayOfTheWeek = day.getDay(); 539 + const saturdayDay = 6; 540 + 541 + if (dayOfTheWeek !== saturdayDay) { 542 + return [false, '', '']; // disabled boolean, css class, tooltip 543 + } 544 + 545 + let slotsForThisDay = 546 + dailySlotValues[dayBeingCheckedFormatted] ?? dailyDeliveryLimit; 547 + if (slotsForThisDay > dailyDeliveryLimit) { 548 + // this should never happen, but just in case 549 + slotsForThisDay = dailyDeliveryLimit; 550 + } 551 + 552 + // Enable check 553 + const isSaturdayEnabled = 554 + slotsForThisDay > 0 && 555 + !disabledDates.includes(dayBeingCheckedFormatted); 556 + 557 + return [ 558 + isSaturdayEnabled, 559 + '', 560 + `Slots available: ${slotsForThisDay}`, 561 + ]; // enabled boolean, css class, tooltip 562 + } 563 + 564 + // Enable weekdays 565 + function enableWeekdays(day) { 566 + const dayBeingCheckedFormatted = $.datepicker.formatDate( 567 + 'yy-mm-dd', 568 + day 569 + ); 570 + 571 + const dayOfTheWeek = day.getDay(); 572 + // Disable if 6 (Saturday) 0 (Sunday) or 1 (Monday) 573 + if ( 574 + dayOfTheWeek === 6 || 575 + dayOfTheWeek === 0 || 576 + dayOfTheWeek === 1 577 + ) { 578 + return [false, '', '']; // disabled boolean, css class, tooltip 579 + } 580 + 581 + let slotsForThisDay = 582 + dailySlotValues[dayBeingCheckedFormatted] ?? dailyDeliveryLimit; 583 + if (slotsForThisDay > dailyDeliveryLimit) { 584 + // this should never happen, but just in case 585 + slotsForThisDay = dailyDeliveryLimit; 586 + } 587 + 588 + const isEnabledDay = 589 + slotsForThisDay > 0 && 590 + !disabledDates.includes(dayBeingCheckedFormatted); 591 + 592 + return [isEnabledDay, '', `Slots available: ${slotsForThisDay}`]; // enabled boolean, css class, tooltip 593 + } 594 + 595 + $('#datepicker').datepicker({ 596 + beforeShowDay: disableDates, 597 + onClose: function () { 598 + const datepicker = document.querySelector('#datepicker'); 599 + const deliveryDate = datepicker.value; 600 + if (deliveryDate) { 601 + sessionStorage.setItem('deliveryDate', deliveryDate); 602 + } 603 + }, 604 + defaultDate: '+1D', 605 + minDate: '+1D', 606 + maxDate: '+21D', 607 + dateFormat: 'd MM, yy', 608 + showOtherMonths: true, 609 + firstDay: 1, 610 + placeholder: 'Select Date', 611 + }); 612 + 613 + $('#datepicker').attr('placeholder', 'Select Date'); 614 + } 615 + 616 + // ------------------------------------------------------// 617 + // handle proceed button 618 + const proceedButtonSelector = '#proceedButton'; 619 + ready(proceedButtonSelector, (proceedButton) => { 620 + proceedButton.addEventListener('click', function (e) { 621 + e.preventDefault(); 622 + const deliveryDate = sessionStorage.getItem('deliveryDate'); 623 + const customerMessage = sessionStorage.getItem('customerMessage'); 624 + const realContinueButton = document.querySelector( 625 + '#checkout-shipping-continue' 626 + ); 627 + 628 + const datepickerErrorElement = 629 + document.getElementById('datepicker_err'); 630 + 631 + if (!deliveryDate) { 632 + datepickerErrorElement.textContent = 633 + 'Please select a delivery date before proceeding.'; 634 + datepickerErrorElement.style.display = 'block'; 635 + return; 636 + } 637 + // Proceed with the form submission 638 + datepickerErrorElement.style.display = 'none'; 639 + 640 + const selectElement = document.querySelector('#delivery_inst_tag'); 641 + const selectedValue = selectElement.value; 642 + const deliveryInstructions = 643 + selectedValue === 'user_instruction' && 644 + customerMessage.length > 0 645 + ? customerMessage 646 + : 'Will be in'; 647 + 648 + const fullInstructions = `${deliveryDate} | ${deliveryInstructions}`; 649 + const realCommentsInput = document.querySelector( 650 + 'input[name="orderComment"]' 651 + ); 652 + 653 + // Update the customer message with the delivery instructions 654 + fetch(`/api/storefront/checkouts/{{cart_id}}`, { 655 + method: 'PUT', 656 + headers: { 657 + 'Content-Type': 'application/json', 658 + }, 659 + body: JSON.stringify({ 660 + customerMessage: fullInstructions, 661 + }), 662 + }) 663 + .then((response) => { 664 + if (!response.ok) { 665 + throw new Error('PUT request failed'); 666 + } 667 + sessionStorage.removeItem('deliveryDate'); 668 + sessionStorage.removeItem('customerMessage'); 669 + realCommentsInput.value = fullInstructions; 670 + realContinueButton.click(); 671 + }) 672 + .catch((error) => { 673 + console.error('Error:', error); 674 + realCommentsInput.value = fullInstructions; 675 + }); 676 + }); 677 + }); 678 + 679 + // ------------------------------------------------------// 680 + // This handles the delivery details / customer message 681 + const deliveryDetailsSelector = 682 + 'fieldset.form-fieldset[data-test="checkout-shipping-comments"]'; 683 + ready(deliveryDetailsSelector, (deliveryDetailsElement) => { 684 + const existingInstructions = 685 + document.getElementById('instruction-wrap'); 686 + if (existingInstructions) { 687 + return; 688 + } 689 + 690 + // generate custom HTML 691 + const instructionsDiv = ` 692 + <div id="instruction-wrap" class="delivery_instruction_wrapper"> 693 + <div class="del_cont"> 694 + <label class="form-legend optimizedCheckout-headingSecondary">Delivery Details</label> 695 + <select class="form-select form-select--small" id="delivery_inst_tag"> 696 + <option value="Will be in">Will be in</option> 697 + <option value="user_instruction">User instruction</option> 698 + </select> 699 + <textarea style="display:none" name="delivery_user_text" id="delivery_user_text" rows="3" cols="40" class="form-input"></textarea> 700 + </div> 701 + <div id="proceedButton">Continue</div> 702 + </div> 703 + `; 704 + 705 + deliveryDetailsElement.insertAdjacentHTML('afterend', instructionsDiv); 706 + 707 + const selectElement = document.getElementById('delivery_inst_tag'); 708 + const textarea = document.getElementById('delivery_user_text'); 709 + 710 + selectElement.addEventListener('change', function () { 711 + const selectedValue = selectElement.value; 712 + if (selectedValue === 'user_instruction') { 713 + textarea.style.display = 'block'; 714 + textarea.style.margin = '10px 0'; 715 + textarea.focus(); 716 + } else { 717 + textarea.style.display = 'none'; 718 + } 719 + }); 720 + 721 + textarea.addEventListener('input', function () { 722 + const deliveryInstructions = textarea.value; 723 + sessionStorage.setItem('customerMessage', deliveryInstructions); 724 + }); 725 + }); 726 + 727 + // ------------------------------------------------------// 728 + // This handles the payment continue button 729 + const paymentContinueButtonSelector = '#checkout-payment-continue'; 730 + ready(paymentContinueButtonSelector, (paymentContinueButton) => { 731 + const fakeButton = document.createElement('div'); 732 + const missingWarn = document.createElement('p'); 733 + const parent = document.querySelector( 734 + '.checkout-step--payment .checkout-form .form-actions' 735 + ); 736 + 737 + // hide the real button 738 + paymentContinueButton.style.display = 'none'; 739 + 740 + // Generate placeholder button 741 + fakeButton.innerHTML = 'Place Order'; 742 + fakeButton.id = 'fakeButton'; 743 + fakeButton.style.display = 'block'; 744 + 745 + // Generate warning text 746 + missingWarn.classList.add('warning'); 747 + missingWarn.textContent = 748 + "Cannot find delivery date. Please edit your 'shipping' details above and select a delivery date."; 749 + missingWarn.style.display = 'none'; 750 + 751 + // Append the button to the body of the HTML document 752 + parent.appendChild(missingWarn); 753 + parent.appendChild(fakeButton); 754 + 755 + fakeButton.addEventListener('click', () => { 756 + fetch(`/api/storefront/checkouts/{{cart_id}}`, { 757 + method: 'GET', 758 + headers: { 759 + 'Content-Type': 'application/json', 760 + }, 761 + }) 762 + .then((response) => { 763 + if (!response.ok) { 764 + throw new Error('GET request failed'); 765 + } 766 + return response.json(); // Parse the JSON response 767 + }) 768 + .then((data) => { 769 + const deliveryInstructions = data.customerMessage; 770 + 771 + if (deliveryInstructions?.trim()) { 772 + paymentContinueButton.click(); 773 + } else { 774 + missingWarn.style.display = 'block'; 775 + } 776 + }) 777 + .catch((error) => { 778 + console.error('Error:', error); 779 + missingWarn.textContent = 780 + 'Something went wrong. Check if you have set a delivery date in shipping and try again.'; 781 + missingWarn.style.display = 'block'; 782 + }); 783 + }); 784 + }); 785 + </script> 348 786 349 787 <style> 350 788 .optimizedCheckout-cart-modal .optimizedCheckout-orderSummary-cartSection {