Carefree Summer Haircare | Hairstory (2025)

Carefree Summer Haircare| Hairstory (1)

Carefree Summer Haircare

Published on October 21, 2024 — 5 min read

Share this article

Home / The Archive / Carefree Summer Haircare

Caring for your hair during the summer months should be as easy and breezy as the season itself. A carefree summer hair kick-off includes selecting a low-maintenance, versatile cut and color that can grow out well into the “dog-days” letting you relax and worry less. We have enough to be worried about these days, right? Here are our top tips to help your hair weather summer in good health.

Be mindful of drying shampoos.

That wind and sun feel great, but it can be tough on hair. Add salt water, chlorine, the constant tugging into ponytails and buns, and hair can require major repair come Fall. So don’t add insult to injury by further parching your hair with traditional shampoo. Now is a good time to rethink your cleansing regimen.

Proper cleansing in any weather should be your number-one priority, and by proper, we mean keeping your natural moisture balance intact so hair isn’t parched by arid weather, and conditioning generously. That means you might want to take traditional shampoo off the menu entirely, especially clarifying shampoos that strip moisture and make color treatments fade faster.

Summer may be the time to adopt a co-washing (conditioner-only) regimen or experiment with cleansing products that dispense with detergent altogether and offers an oil-based method instead. With three choices of cleansing and conditioning levels, the New Wash family of products is the summer lover’s dream clean: one product both cleanses and conditions, and melts away tangles. Peppermint oil has a welcome cooling effect, and the natural fragrance has rose notes, perfect for the summer season.

Amp up the moisture.

Next, you’ll need an all-purpose moisturizing creme on hand to help prevent frizz in humid weather, soothe the sun damage that may occur, and offer easy styling options when you need to look more groomed (and let that bun down for once): Hair Balm is the perfect choice to direct hair where you want it to go without resorting to a blow-dryer (who wants more heat?) and keeps curls frizz-less without weight or greasiness. Plus, you can use it for a sleek, combed-back resort look for the beach – a la St. Tropez.

Grab a texture spray.

With summer comes permission to loosen up a little and wear imperfect, casual textures that look like you’ve been outdoors even if you haven’t. Try Undressed – a texturizing spray with a name (Déshabille en Français) that captures the seaside spirit, even in the office if you ever get back there.

With summer comes permission to loosen up a little and wear imperfect, casual textures that look like you’ve been outdoors even if you haven’t.

Inspired by surf sprays that add grit – but tend to dry out hair in the process – Undressed is a salt-free alternative that builds body, creates a low-glow finish, and helps natural wave patterns take shape. You can also use its holding properties to create waves by twisting or braiding.

Embrace air-drying.

Many women have rediscovered their natural texture out of necessity during the quarantine, so get ready for much more naturalness in the coming summer months. There will be limited use of the blow-dryer in salons; it stirs up inside air along with potential pathogens.

This may mean changing your styling mindset; now is a great time to work on loving what nature gave you – just reach for those products above to help you make the most of it.

If you must blow-dry, keep the heat reasonable (400 degrees, tops), work quickly to limit heat exposure, and try not to do it more than once a week. Always use a thermal hair protector that also offers some hold such as Dressed Up that contains heat-protective ingredients that also screen the sun’s UV rays, and make safe heat styling that much safer.

Wear a hat.

Always venture out into the sun with a hat or a scarf, especially if your hair is color-treated. Don’t think of it as merely a safety precaution to keep hair color vibrant and to prevent sun damage to the scalp; it’s also a fashion opportunity to show off your summer accessories (along with your mask).

Avoid chlorine.

Pool rules: Never let chlorine touch your hair if you can help it! Wear a cap. If you’re a bareheaded daredevil, wet hair before diving in so it will be saturated and less able to absorb chlorine. And afterward, rinse hair while it’s still wet; dried chlorine is stubbornly difficult to remove.

Remember to handle wet hair with care; this is when it’s most elastic and fragile.

• • •

But most of all, our summer prescription is to go out and have some fun! When you’ve taken all the worry out of summer haircare, you’re better able to do just that.

shop the collection

View More Information Suited For Flat Hair, Oily Hair, Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is PRE-WASH AND NEW WASH ORIGINAL Benefits Clarify , cleanse and condition FOR ALL HAIR TYPES Learn More
Save 27%

New Wash Method for All Hair Types

PRE-WASH AND NEW WASH ORIGINAL

`; } await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets(); createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(productId); const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } /** * @description Function that is used to initially mount the recharge widget * @returns { boolean } based on if the product is found in recharge or not. If false, then the widget will not mount. * In addition if it cannot find the product, it short circuits and returns early. */ function shouldRechargeMount() { const checkReChargeWidget = setInterval(() => { if (window.ReChargeWidget && window.ReChargeWidget.api) { clearInterval(checkReChargeWidget); window.ReChargeWidget.api.fetchProduct(productId) .then(rechargeProduct => { if (!rechargeProduct.in_recharge) { return false; } const config = { productId: productId, injectionParent: `#subscription-selector-${productId}` }; window.ReChargeWidget.createWidget(config); return true; }) } }, 100); } async function waitForRechargeWidgetToBeMounted() { return new Promise(resolve => { const checkForRechargeContainer = setInterval(() => { if (document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`)) { rechargeWidgetContainer = document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`) clearInterval(checkForRechargeContainer) resolve() } }, 100) }) } /** * @description Function that is used to determine the initial price of the product, and then properly assign * the otpPriceText and subscriptionPriceText * @param {HTMLElement} originalPriceElement HTML Element that is used to determine the base price, and then calculate the subscription discount */ function derivePriceOptions(originalPriceElements) { let priceElementTextContent = originalPriceElements[0].innerText; let priceRegex = /([£$€¥])(\d+(?:\.\d{1,2})?)/; let match = priceElementTextContent.match(priceRegex); if (match) { let currencySymbol = match[1]; let priceValue = match[2]; otpPriceText = currencySymbol + priceValue; let subscriptionPrice if (productsWithTenPercentOff.includes(productId)) { subscriptionPrice = Number(priceValue) * 0.9; } else { subscriptionPrice = Number(priceValue) * 0.95; } subscriptionPriceText = `${currencySymbol}${subscriptionPrice.toFixed(2)}`; } } /** * @description Function to add or remove Recharge event listeners * @param {string} action - 'add' to add listeners, 'remove' to remove listeners */ function handleRechargeEventListeners(action) { function manageListeners() { const oneTimePurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-onetime]`); const subscriptionPurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-subsave]`); if (oneTimePurchaseInput && subscriptionPurchaseInput) { if (originalPriceElements.length > 0 && action === 'add') { derivePriceOptions(originalPriceElements) } if (currentSubscriptionSelector) { // Because recharge is responsbile for adding the .rc-option--active class when a user clicks a radio, we must set it manually for the accent-color black // to be present if we switch the radio manually, after the recharge widget remounts if (currentSubscriptionSelector === 'subscription') { subscriptionPurchaseInput.classList.add('rc-option--active') subscriptionPurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'One time purchase', adding name=''. // If switching products and subscribe and save is selected, the select with name='' will still mount, causing the cart item to show a OTP rechargeSelectElement.setAttribute('name', 'selling_plan') let rechargeSellingPlans = deliveryOptionsContainer.querySelector('.rc-selling-plans') // By default our config for recharge is set to OTP. If we remount with a subscription option selected via our event handlers // The select with have a display: none; hardcoded onto the style, so we must remove it in order for it to be visible. if (rechargeSellingPlans) { if (rechargeSellingPlans.style.display === 'none') { rechargeSellingPlans.style.removeProperty('display'); } } } else { deliveryOptionsContainer.classList.add('hidden') oneTimePurchaseInput.classList.add('rc-option--active') oneTimePurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'subscribe and save', adding name='selling_plan'. // If switching products and OTP is selected, the input with name='selling_plan' will still mount, causing the cart item to show as a subscription, even if it's OTP. rechargeSelectElement.setAttribute('name', '') } } // Check to see which input is checked on dom mount, then properly set the price based on the product and variant selected. if (subscriptionPurchaseInput.checked === true) { // reassign the perks container to the new product id on being switched, or set the intitial element if it doesn't exist on initial mount if ((!subscriptionPerksContainer && action === 'add') || (action === 'add' && subscriptionPerksContainer.id !== document.querySelector(`[id="${productId}-subscription-perks"]`).id)) { subscriptionPerksContainer = document.querySelector(`[id="${productId}-subscription-perks"]`); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = subscriptionPriceText; }); } if (shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } } else { if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = otpPriceText; }); } } if (action === 'add') { oneTimePurchaseInput.addEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.addEventListener('click', onSubscriptionPurchaseClick); // At the time of this writing. Recharge after mounting sets the selling plan to 0. so we need to set the name manually // otherwise on PLP's radio options will only allow one option to be set oneTimePurchaseInput.name = `${productId}_selling_option` subscriptionPurchaseInput.name = `${productId}_selling_option` } else if (action === 'remove') { oneTimePurchaseInput.removeEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.removeEventListener('click', onSubscriptionPurchaseClick); } handleSellingPlanSelect(action) return true; } return false; } if (manageListeners()) return; const waitForElements = setInterval(() => { if (manageListeners()) { clearInterval(waitForElements); } }, 100); } /** * @description binds the proper event listerns to the proper selling plan select, in addition to pre-selecting the existing plan if * the previously selected option is present in the newly mounted select. */ function handleSellingPlanSelect(action) { const checkForSellingPlanSelect = setInterval(() => { const sellingPlanSelect = document.querySelector(`#selling_plan_${productId}`); if (sellingPlanSelect) { clearInterval(checkForSellingPlanSelect); if (action === 'add') { sellingPlanSelect.addEventListener('change', onSellingPlanChange); } else if (action === 'remove') { sellingPlanSelect.removeEventListener('change', onSellingPlanChange); } if (lastSelectedSellingPlan) { // Convert array like object into a proper array const matchingSellingPlanOption = [...sellingPlanSelect.options].find(option => option.textContent.trim() === lastSelectedSellingPlan ); if (matchingSellingPlanOption) { sellingPlanSelect.value = matchingSellingPlanOption.value; } } } }, 100); } /** * @description Function that is called when the OTP input is clicked */ function onOneTimePurchaseClick() { onRechargeOptionClick('otp'); } /** * @description Function that is called when the subscription input is clicked */ function onSubscriptionPurchaseClick() { onRechargeOptionClick('subscription'); } /** * @description Function that is called whenever the selling plan selected option has changed */ function onSellingPlanChange(event) { lastSelectedSellingPlan = event.target.options[event.target.selectedIndex].textContent.trim(); } /** * @description Function that is called when a recharge option input is clicked. Primarly used to add bullet points, and make sure the select has an option mounted * @param {string} optionType The type of option that was clicked, either OTP, or subscription */ function onRechargeOptionClick(optionType) { const selectedPriceText = optionType === 'subscription' ? subscriptionPriceText : otpPriceText; // Save the current selected selector, so when we remount, we can default to that option being checked currentSubscriptionSelector = optionType if (originalPriceElements[0].getAttribute('data-price-id') !== `price-${productId}`) { let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = selectedPriceText; }); } if (optionType === 'otp') { deliveryOptionsContainer.classList.add('hidden') } // If the widget has been remounted, and OTP was selected, we need to remove hidden from the container so a user can see the selling plans if (deliveryOptionsContainer && optionType === 'subscription' && deliveryOptionsContainer.classList.contains('hidden')) { deliveryOptionsContainer.classList.remove('hidden') } if (subscriptionPerksContainer) { if (optionType === 'subscription' && shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } else { subscriptionPerksContainer.classList.add('hidden'); } } if (optionType === 'subscription') { if (rechargeSelectElement) { // Make sure the select element as the first child element selected after mount // otherwise the select shows as empty rechargeSelectElement.children[0].selected = 'selected'; // on remount, if OTP is selected, we remove the name from the selling plan, this messes up recharge's internal logic, and then the select name always remains empty // to stop this from happening, we just manually set it everytime subscription is clicked rechargeSelectElement.setAttribute('name', 'selling_plan') } } if (optionType === 'otp') { if (rechargeSelectElement) { // When we select the OTP, but subscription was selected previously, it will still have the selling plan, name, we need to remove this or it falsely gets set as a subscription product rechargeSelectElement.setAttribute('name', '') } } } /** * @description Waits for the data price subsave attribute to be loaded, and then moves it down to the add cart button, in additio to cleaning up any remnants from recharge */ async function relocateSubscriptionPriceAndCleanup() { return new Promise((resolve) => { const waitForPriceElements = setInterval(() => { const subscriptionPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`); const otpPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`); const discountSpan = document.querySelector(`#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]`) let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); if (subscriptionPriceElement && otpPriceElement && originalPriceElements.length > 0 && discountSpan) { // Left over elements from recharge that need to be hidden // I.E Subscription price vs OTP price, and percentage discounted const unusedRechargeElementSelectors = [ `#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`, `#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]` ]; unusedRechargeElementSelectors.forEach(selector => { const element = document.querySelector(selector); if (element) { element.classList.add('hidden'); } }); clearInterval(waitForPriceElements); resolve(); } }, 100); }) } /** * @description Waits for the recharge select element to be loaded into the dom, and then moves it down to below the variant selector */ function waitForRechargeSelect() { return new Promise((resolve) => { const checkForRechargeSelect = setInterval(() => { if (document.querySelector(`#selling_plan_${productId}`)) { rechargeSelectElement = document.querySelector(`#selling_plan_${productId}`); const checkForNewDeliveryOptionsContainer = setInterval(() => { if (document.querySelector(`#delivery-options-${productId}`)) { deliveryOptionsContainer = document.querySelector(`#delivery-options-${productId}`); deliveryOptionsContainer.append(rechargeSelectElement.parentElement); clearInterval(checkForNewDeliveryOptionsContainer); // We only want to add the additional styles on the PDP if its the refill collection if (collection?.id === 449976238310 || pageTemplate === 'page.refill-club' || isStyledForCarousel) { conditionallyStyleRechargeOnPLP(); } // Remove the strong tag between the span and the subscribe and save text const subSaveSpan = document.querySelector(`#subscription-selector-${productId} .rc-option__text[data-label-text-subsave]`); if (subSaveSpan.querySelector('strong')) { const strongElement = subSaveSpan.querySelector('strong'); const strongText = strongElement.textContent.trim(); const textNode = document.createTextNode(strongText); subSaveSpan.replaceChild(textNode, strongElement); } clearInterval(checkForRechargeSelect); resolve(); } }, 100); } }, 100); }); } /** * @description Checks to see if the recharge widget is on the PDP, if so, it conditionally applies CSS styles, since there will be some products on the PDP that don't have * a

element, but all selects on the PDP will need to have a transparent background. */ function conditionallyStyleRechargeOnPLP() { // This is the id associated with the refill collection. We need to conditionally rechargeSelectElement.classList.add('!bg-transparent') const parentElement = deliveryOptionsContainer.parentElement; const variantSelectCard = parentElement.querySelector(':scope > variant-select-card'); if (!variantSelectCard) { deliveryOptionsContainer.classList.add('border-t', 'border-t-brand-secondary-200') // Because of the lack of a variant card, we need to remove the bottom border, so the line between the delivery options, and the add to bag doesn't have an extra thick border deliveryOptionsContainer.firstChild.classList.add('border-b-0') } } /** * @description Dynamically creates a list of bullet points whenever the shouldRenderBulletPoints flag is true. Also creates a title for the bullet points. */ function createBulletPoints() { if (shouldRenderBulletPoints && subscriptionBullets.length > 0) { const subscriptionSelector = document.querySelector(`#subscription-selector-${productId}`); subscriptionPerksContainer = document.createElement('div'); subscriptionPerksContainer.id = `${productId}-subscription-perks`; subscriptionPerksContainer.className = 'hidden space-y-2 pb-4 pt-3'; const subscriptionPerksTitle = document.createElement('h4'); subscriptionPerksTitle.className = 'space-y-2 uppercase'; subscriptionPerksTitle.innerHTML = 'Subscription Benefits:'; subscriptionPerksContainer.appendChild(subscriptionPerksTitle); subscriptionBullets.forEach((bullet, index) => { const bulletPoint = document.createElement('p'); bulletPoint.className = 'flex items-center w-full text-brand-secondary-200'; bulletPoint.innerHTML = ` ${renderIconCircleCheckmark()} ${bullet} `; if (shouldApplyBottomMargin && index === subscriptionBullets.length - 1) { bulletPoint.style.marginBottom = '4px'; } subscriptionPerksContainer.appendChild(bulletPoint); }); subscriptionSelector.appendChild(subscriptionPerksContainer); } } function renderIconCircleCheckmark() { return ``; } function assignProductBullets() { if (window.subscriptonBulletsDict && window.subscriptonBulletsDict[productId]) { let newSubscriptionBulletList = [...window.subscriptonBulletsDict[productId]] subscriptionBullets = newSubscriptionBulletList } else { subscriptionBullets = defaultSubscriptionBullets; } } /** * @param {string} newProductId the new product id that will be used to update all exising HTML elements, and rebind the function on the window * @description Whenever the recharge widget is remounted, we need to update the initial bullet with the new product description */ async function onNewProductSelect(newProductId) { if (productId !== newProductId) { let rechargeContainer = document.querySelector(`#subscription-selector-${productId}`); let cardAtcContainer = document.querySelector(`#card-atc-${productId}`) // Show loading spinner rechargeContainer.innerHTML = ` `; // cleanup all event listeners and old elements from the old widget handleRechargeEventListeners('remove'); if (window.ReChargeWidget.getWidgetsByProductId(productId)[0]) { window.ReChargeWidget.getWidgetsByProductId(productId)[0].widgetInstance.unmount(); } if (subscriptionPerksContainer) { subscriptionPerksContainer.remove(); } // After cleaning up the previous widget, reassign the id to properly mount the new widget. rechargeContainer.id = `subscription-selector-${newProductId}`; productId = Number(newProductId); const config = { productId: newProductId, injectionParent: `#subscription-selector-${newProductId}` }; // Re-establish and mount new widget window.ReChargeWidget.createWidget(config); // Wait for all elements to mount await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets() createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(newProductId); // Hide spinner const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } } /** * @param {string} productId productId responsible for the unique key to be set on the window, so all instances of the widget can be accessed. * @description Function responsible for binding the onNewProduct select to the window. */ function configureRechargeForProductSelection(productId) { window.HairstoryRechargeWidget = window.HairstoryRechargeWidget || {}; window.HairstoryRechargeWidget[productId] = { onNewProductSelect: onNewProductSelect }; } // Initial mount of recharge widget initializeRechargeWidget().catch(error => { console.error("Error initializing ReCharge widget:", error); }); }); View More Information Suited For Flat Hair, Oily Hair, Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is 8OZ NEW WASH ORIGINAL, BOND BOOST AND BOND SERUM Benefits NEW Learn More
Save 19%

Damage Repair Method

8OZ NEW WASH ORIGINAL, BOND BOOST AND BOND SERUM

`; } await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets(); createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(productId); const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } /** * @description Function that is used to initially mount the recharge widget * @returns { boolean } based on if the product is found in recharge or not. If false, then the widget will not mount. * In addition if it cannot find the product, it short circuits and returns early. */ function shouldRechargeMount() { const checkReChargeWidget = setInterval(() => { if (window.ReChargeWidget && window.ReChargeWidget.api) { clearInterval(checkReChargeWidget); window.ReChargeWidget.api.fetchProduct(productId) .then(rechargeProduct => { if (!rechargeProduct.in_recharge) { return false; } const config = { productId: productId, injectionParent: `#subscription-selector-${productId}` }; window.ReChargeWidget.createWidget(config); return true; }) } }, 100); } async function waitForRechargeWidgetToBeMounted() { return new Promise(resolve => { const checkForRechargeContainer = setInterval(() => { if (document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`)) { rechargeWidgetContainer = document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`) clearInterval(checkForRechargeContainer) resolve() } }, 100) }) } /** * @description Function that is used to determine the initial price of the product, and then properly assign * the otpPriceText and subscriptionPriceText * @param {HTMLElement} originalPriceElement HTML Element that is used to determine the base price, and then calculate the subscription discount */ function derivePriceOptions(originalPriceElements) { let priceElementTextContent = originalPriceElements[0].innerText; let priceRegex = /([£$€¥])(\d+(?:\.\d{1,2})?)/; let match = priceElementTextContent.match(priceRegex); if (match) { let currencySymbol = match[1]; let priceValue = match[2]; otpPriceText = currencySymbol + priceValue; let subscriptionPrice if (productsWithTenPercentOff.includes(productId)) { subscriptionPrice = Number(priceValue) * 0.9; } else { subscriptionPrice = Number(priceValue) * 0.95; } subscriptionPriceText = `${currencySymbol}${subscriptionPrice.toFixed(2)}`; } } /** * @description Function to add or remove Recharge event listeners * @param {string} action - 'add' to add listeners, 'remove' to remove listeners */ function handleRechargeEventListeners(action) { function manageListeners() { const oneTimePurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-onetime]`); const subscriptionPurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-subsave]`); if (oneTimePurchaseInput && subscriptionPurchaseInput) { if (originalPriceElements.length > 0 && action === 'add') { derivePriceOptions(originalPriceElements) } if (currentSubscriptionSelector) { // Because recharge is responsbile for adding the .rc-option--active class when a user clicks a radio, we must set it manually for the accent-color black // to be present if we switch the radio manually, after the recharge widget remounts if (currentSubscriptionSelector === 'subscription') { subscriptionPurchaseInput.classList.add('rc-option--active') subscriptionPurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'One time purchase', adding name=''. // If switching products and subscribe and save is selected, the select with name='' will still mount, causing the cart item to show a OTP rechargeSelectElement.setAttribute('name', 'selling_plan') let rechargeSellingPlans = deliveryOptionsContainer.querySelector('.rc-selling-plans') // By default our config for recharge is set to OTP. If we remount with a subscription option selected via our event handlers // The select with have a display: none; hardcoded onto the style, so we must remove it in order for it to be visible. if (rechargeSellingPlans) { if (rechargeSellingPlans.style.display === 'none') { rechargeSellingPlans.style.removeProperty('display'); } } } else { deliveryOptionsContainer.classList.add('hidden') oneTimePurchaseInput.classList.add('rc-option--active') oneTimePurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'subscribe and save', adding name='selling_plan'. // If switching products and OTP is selected, the input with name='selling_plan' will still mount, causing the cart item to show as a subscription, even if it's OTP. rechargeSelectElement.setAttribute('name', '') } } // Check to see which input is checked on dom mount, then properly set the price based on the product and variant selected. if (subscriptionPurchaseInput.checked === true) { // reassign the perks container to the new product id on being switched, or set the intitial element if it doesn't exist on initial mount if ((!subscriptionPerksContainer && action === 'add') || (action === 'add' && subscriptionPerksContainer.id !== document.querySelector(`[id="${productId}-subscription-perks"]`).id)) { subscriptionPerksContainer = document.querySelector(`[id="${productId}-subscription-perks"]`); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = subscriptionPriceText; }); } if (shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } } else { if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = otpPriceText; }); } } if (action === 'add') { oneTimePurchaseInput.addEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.addEventListener('click', onSubscriptionPurchaseClick); // At the time of this writing. Recharge after mounting sets the selling plan to 0. so we need to set the name manually // otherwise on PLP's radio options will only allow one option to be set oneTimePurchaseInput.name = `${productId}_selling_option` subscriptionPurchaseInput.name = `${productId}_selling_option` } else if (action === 'remove') { oneTimePurchaseInput.removeEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.removeEventListener('click', onSubscriptionPurchaseClick); } handleSellingPlanSelect(action) return true; } return false; } if (manageListeners()) return; const waitForElements = setInterval(() => { if (manageListeners()) { clearInterval(waitForElements); } }, 100); } /** * @description binds the proper event listerns to the proper selling plan select, in addition to pre-selecting the existing plan if * the previously selected option is present in the newly mounted select. */ function handleSellingPlanSelect(action) { const checkForSellingPlanSelect = setInterval(() => { const sellingPlanSelect = document.querySelector(`#selling_plan_${productId}`); if (sellingPlanSelect) { clearInterval(checkForSellingPlanSelect); if (action === 'add') { sellingPlanSelect.addEventListener('change', onSellingPlanChange); } else if (action === 'remove') { sellingPlanSelect.removeEventListener('change', onSellingPlanChange); } if (lastSelectedSellingPlan) { // Convert array like object into a proper array const matchingSellingPlanOption = [...sellingPlanSelect.options].find(option => option.textContent.trim() === lastSelectedSellingPlan ); if (matchingSellingPlanOption) { sellingPlanSelect.value = matchingSellingPlanOption.value; } } } }, 100); } /** * @description Function that is called when the OTP input is clicked */ function onOneTimePurchaseClick() { onRechargeOptionClick('otp'); } /** * @description Function that is called when the subscription input is clicked */ function onSubscriptionPurchaseClick() { onRechargeOptionClick('subscription'); } /** * @description Function that is called whenever the selling plan selected option has changed */ function onSellingPlanChange(event) { lastSelectedSellingPlan = event.target.options[event.target.selectedIndex].textContent.trim(); } /** * @description Function that is called when a recharge option input is clicked. Primarly used to add bullet points, and make sure the select has an option mounted * @param {string} optionType The type of option that was clicked, either OTP, or subscription */ function onRechargeOptionClick(optionType) { const selectedPriceText = optionType === 'subscription' ? subscriptionPriceText : otpPriceText; // Save the current selected selector, so when we remount, we can default to that option being checked currentSubscriptionSelector = optionType if (originalPriceElements[0].getAttribute('data-price-id') !== `price-${productId}`) { let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = selectedPriceText; }); } if (optionType === 'otp') { deliveryOptionsContainer.classList.add('hidden') } // If the widget has been remounted, and OTP was selected, we need to remove hidden from the container so a user can see the selling plans if (deliveryOptionsContainer && optionType === 'subscription' && deliveryOptionsContainer.classList.contains('hidden')) { deliveryOptionsContainer.classList.remove('hidden') } if (subscriptionPerksContainer) { if (optionType === 'subscription' && shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } else { subscriptionPerksContainer.classList.add('hidden'); } } if (optionType === 'subscription') { if (rechargeSelectElement) { // Make sure the select element as the first child element selected after mount // otherwise the select shows as empty rechargeSelectElement.children[0].selected = 'selected'; // on remount, if OTP is selected, we remove the name from the selling plan, this messes up recharge's internal logic, and then the select name always remains empty // to stop this from happening, we just manually set it everytime subscription is clicked rechargeSelectElement.setAttribute('name', 'selling_plan') } } if (optionType === 'otp') { if (rechargeSelectElement) { // When we select the OTP, but subscription was selected previously, it will still have the selling plan, name, we need to remove this or it falsely gets set as a subscription product rechargeSelectElement.setAttribute('name', '') } } } /** * @description Waits for the data price subsave attribute to be loaded, and then moves it down to the add cart button, in additio to cleaning up any remnants from recharge */ async function relocateSubscriptionPriceAndCleanup() { return new Promise((resolve) => { const waitForPriceElements = setInterval(() => { const subscriptionPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`); const otpPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`); const discountSpan = document.querySelector(`#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]`) let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); if (subscriptionPriceElement && otpPriceElement && originalPriceElements.length > 0 && discountSpan) { // Left over elements from recharge that need to be hidden // I.E Subscription price vs OTP price, and percentage discounted const unusedRechargeElementSelectors = [ `#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`, `#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]` ]; unusedRechargeElementSelectors.forEach(selector => { const element = document.querySelector(selector); if (element) { element.classList.add('hidden'); } }); clearInterval(waitForPriceElements); resolve(); } }, 100); }) } /** * @description Waits for the recharge select element to be loaded into the dom, and then moves it down to below the variant selector */ function waitForRechargeSelect() { return new Promise((resolve) => { const checkForRechargeSelect = setInterval(() => { if (document.querySelector(`#selling_plan_${productId}`)) { rechargeSelectElement = document.querySelector(`#selling_plan_${productId}`); const checkForNewDeliveryOptionsContainer = setInterval(() => { if (document.querySelector(`#delivery-options-${productId}`)) { deliveryOptionsContainer = document.querySelector(`#delivery-options-${productId}`); deliveryOptionsContainer.append(rechargeSelectElement.parentElement); clearInterval(checkForNewDeliveryOptionsContainer); // We only want to add the additional styles on the PDP if its the refill collection if (collection?.id === 449976238310 || pageTemplate === 'page.refill-club' || isStyledForCarousel) { conditionallyStyleRechargeOnPLP(); } // Remove the strong tag between the span and the subscribe and save text const subSaveSpan = document.querySelector(`#subscription-selector-${productId} .rc-option__text[data-label-text-subsave]`); if (subSaveSpan.querySelector('strong')) { const strongElement = subSaveSpan.querySelector('strong'); const strongText = strongElement.textContent.trim(); const textNode = document.createTextNode(strongText); subSaveSpan.replaceChild(textNode, strongElement); } clearInterval(checkForRechargeSelect); resolve(); } }, 100); } }, 100); }); } /** * @description Checks to see if the recharge widget is on the PDP, if so, it conditionally applies CSS styles, since there will be some products on the PDP that don't have * a element, but all selects on the PDP will need to have a transparent background. */ function conditionallyStyleRechargeOnPLP() { // This is the id associated with the refill collection. We need to conditionally rechargeSelectElement.classList.add('!bg-transparent') const parentElement = deliveryOptionsContainer.parentElement; const variantSelectCard = parentElement.querySelector(':scope > variant-select-card'); if (!variantSelectCard) { deliveryOptionsContainer.classList.add('border-t', 'border-t-brand-secondary-200') // Because of the lack of a variant card, we need to remove the bottom border, so the line between the delivery options, and the add to bag doesn't have an extra thick border deliveryOptionsContainer.firstChild.classList.add('border-b-0') } } /** * @description Dynamically creates a list of bullet points whenever the shouldRenderBulletPoints flag is true. Also creates a title for the bullet points. */ function createBulletPoints() { if (shouldRenderBulletPoints && subscriptionBullets.length > 0) { const subscriptionSelector = document.querySelector(`#subscription-selector-${productId}`); subscriptionPerksContainer = document.createElement('div'); subscriptionPerksContainer.id = `${productId}-subscription-perks`; subscriptionPerksContainer.className = 'hidden space-y-2 pb-4 pt-3'; const subscriptionPerksTitle = document.createElement('h4'); subscriptionPerksTitle.className = 'space-y-2 uppercase'; subscriptionPerksTitle.innerHTML = 'Subscription Benefits:'; subscriptionPerksContainer.appendChild(subscriptionPerksTitle); subscriptionBullets.forEach((bullet, index) => { const bulletPoint = document.createElement('p'); bulletPoint.className = 'flex items-center w-full text-brand-secondary-200'; bulletPoint.innerHTML = ` ${renderIconCircleCheckmark()} ${bullet} `; if (shouldApplyBottomMargin && index === subscriptionBullets.length - 1) { bulletPoint.style.marginBottom = '4px'; } subscriptionPerksContainer.appendChild(bulletPoint); }); subscriptionSelector.appendChild(subscriptionPerksContainer); } } function renderIconCircleCheckmark() { return ``; } function assignProductBullets() { if (window.subscriptonBulletsDict && window.subscriptonBulletsDict[productId]) { let newSubscriptionBulletList = [...window.subscriptonBulletsDict[productId]] subscriptionBullets = newSubscriptionBulletList } else { subscriptionBullets = defaultSubscriptionBullets; } } /** * @param {string} newProductId the new product id that will be used to update all exising HTML elements, and rebind the function on the window * @description Whenever the recharge widget is remounted, we need to update the initial bullet with the new product description */ async function onNewProductSelect(newProductId) { if (productId !== newProductId) { let rechargeContainer = document.querySelector(`#subscription-selector-${productId}`); let cardAtcContainer = document.querySelector(`#card-atc-${productId}`) // Show loading spinner rechargeContainer.innerHTML = ` `; // cleanup all event listeners and old elements from the old widget handleRechargeEventListeners('remove'); if (window.ReChargeWidget.getWidgetsByProductId(productId)[0]) { window.ReChargeWidget.getWidgetsByProductId(productId)[0].widgetInstance.unmount(); } if (subscriptionPerksContainer) { subscriptionPerksContainer.remove(); } // After cleaning up the previous widget, reassign the id to properly mount the new widget. rechargeContainer.id = `subscription-selector-${newProductId}`; productId = Number(newProductId); const config = { productId: newProductId, injectionParent: `#subscription-selector-${newProductId}` }; // Re-establish and mount new widget window.ReChargeWidget.createWidget(config); // Wait for all elements to mount await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets() createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(newProductId); // Hide spinner const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } } /** * @param {string} productId productId responsible for the unique key to be set on the window, so all instances of the widget can be accessed. * @description Function responsible for binding the onNewProduct select to the window. */ function configureRechargeForProductSelection(productId) { window.HairstoryRechargeWidget = window.HairstoryRechargeWidget || {}; window.HairstoryRechargeWidget[productId] = { onNewProductSelect: onNewProductSelect }; } // Initial mount of recharge widget initializeRechargeWidget().catch(error => { console.error("Error initializing ReCharge widget:", error); }); }); View More Information Suited For Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is 8OZ NEW WASH RICH, BOND BOOST AND BOND SERUM Benefits NEW Learn More
Save 19%

Richest Damage Repair Method

8OZ NEW WASH RICH, BOND BOOST AND BOND SERUM

Carefree Summer Haircare
| Hairstory (2025)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Margart Wisoky

Last Updated:

Views: 5999

Rating: 4.8 / 5 (58 voted)

Reviews: 81% of readers found this page helpful

Author information

Name: Margart Wisoky

Birthday: 1993-05-13

Address: 2113 Abernathy Knoll, New Tamerafurt, CT 66893-2169

Phone: +25815234346805

Job: Central Developer

Hobby: Machining, Pottery, Rafting, Cosplaying, Jogging, Taekwondo, Scouting

Introduction: My name is Margart Wisoky, I am a gorgeous, shiny, successful, beautiful, adventurous, excited, pleasant person who loves writing and wants to share my knowledge and understanding with you.