diff --git a/lib/kits/core-ui/components/common/header.ts b/lib/kits/core-ui/components/common/header.ts index 211c2bf4..e646dbdc 100644 --- a/lib/kits/core-ui/components/common/header.ts +++ b/lib/kits/core-ui/components/common/header.ts @@ -21,14 +21,14 @@ const defaultTemplate = `\
{{translations.close}} @@ -60,16 +60,15 @@ const defaultTemplate = `\ +
+ {{/disableClose}} + {{/disableHeader}}
{{^disableHeader}}
@@ -105,14 +124,14 @@ const sidebarTemplate = `\
{{translations.close}} @@ -144,16 +163,15 @@ const sidebarTemplate = `\ + {{#label}} +
+
+
+ {{label}} +
+
-
-
- {{^disableShoppingList}} - - {{/disableShoppingList}} - {{#webshop_link}} - - {{/webshop_link}} -
-
-
- {{/offer}} - \ -`; - -const loaderTemplate = `\ -
-
-
-
-   -
-
-   -
-
-
-
-
-
-
-
-
-
-   + {{/label}} +
+
+
+ {{heading}} +
+
+ {{description}} +
+
-
-   +
+
+ {{#products}} +
+
+ {{#image}} + {{heading}} + {{/image}} + {{^image}} + + {{/image}} +
+
+
+ {{#link}} + + {{title}} + + {{/link}} + {{^link}} + {{title}} + {{/link}} +
+
{{description}}
+
+
+
+
+ + + +
+
+
{{formattedPrice}}
+
+
+ {{/products}} +
-
-   +
+ {{translations.validFrom}} + {{dateRange}}
-
-
-  
+ {{/loader}}
+ {{/offer}}
\ `; @@ -107,7 +122,9 @@ const OfferOverview = ({ localeCode: translate('locale_code'), currency: translate('publication_viewer_currency'), addToShoppingList: translate('publication_viewer_add_to_shopping_list'), - visitWebshopLink: translate('publication_viewer_visit_webshop_link') + visitWebshopLink: translate('publication_viewer_visit_webshop_link'), + priceFrom: translate('publication_viewer_offer_price_from'), + validFrom: translate('publication_viewer_offer_valid_from') }; const render = async () => { @@ -116,7 +133,16 @@ const OfferOverview = ({ createModal(container); - container.innerHTML = Mustache.render(loaderTemplate, {}); + container.innerHTML = Mustache.render(template, { + translations, + label: '', + disableShoppingList: document.querySelector('.sgn__offer-shopping') + ? false + : true, + offer: {}, + loader: true, + layoutWidth: sgnData?.incito?.root_view?.layout_width + }); const transformedOffer = type === 'paged' @@ -129,12 +155,46 @@ const OfferOverview = ({ disableShoppingList: document.querySelector('.sgn__offer-shopping') ? false : true, - offer: transformedOffer + offer: transformedOffer, + layoutWidth: sgnData?.incito?.root_view?.layout_width }); dispatchOfferClickEvent(transformedOffer); addEventListeners(); }; + const transformProducts = (offer, products) => { + const {localeCode, currency} = translations; + const storedPublicationOffers = + clientLocalStorage.get('publication-saved-offers') || []; + + const allPricesAreTheSame = products?.every( + (product, index, array) => product.price === array[0].price + ); + + const useOfferPrice = + allPricesAreTheSame && offer.price !== products[0].price; + + const transformedProducts = products?.map((product, index) => { + const matchingOffer = storedPublicationOffers.find( + (offer) => offer.id === product.id + ); + + return { + ...product, + link: product.link || offer.webshop_link, + formattedPrice: formatPrice( + useOfferPrice + ? offer.price || offer.pricing.price + : product?.price, + localeCode, + currency + ), + quantity: matchingOffer ? matchingOffer.quantity : 0 + }; + }); + + return transformedProducts; + }; const transformIncitoOffer = async ({ fetchOffer, @@ -142,14 +202,37 @@ const OfferOverview = ({ publicationId, products }) => { - const {localeCode, currency} = translations; + const {localeCode, currency, priceFrom} = translations; const {offer: incitoOffer} = await fetchOffer({viewId, publicationId}); offer = incitoOffer; + offer.products = transformProducts(offer, products); + + if (products?.length > 1) { + offer.products = transformProducts(offer, products); + } else { + const products = transformProducts(offer, [ + { + id: offer.id, + title: offer.name, + description: offer.description, + price: offer.price, + link: offer.webshop_link + } + ]); + + offer.products = products; + offer.hideOfferDetails = true; + } + + const hasPriceFrom = products.some((product, i, arr) => { + if (i === 0) return false; + return product.price !== arr[i - 1].price; + }); return { ...offer, - products, heading: offer.name, + priceFrom: hasPriceFrom ? priceFrom : '', price: formatPrice( offer?.price, localeCode, @@ -199,12 +282,44 @@ const OfferOverview = ({ const fetchOffer = async (id: string) => { const {localeCode, currency} = translations; - const offer = await request({ + const offerData = await request({ apiKey: configs.apiKey, coreUrl: configs.coreUrl, url: `/v2/offers/${id}` }); + offer = offerData; + + const rawProducts = await fetchProducts(id); + + if (rawProducts.offer_products?.length) { + const dirtyProducts = rawProducts.offer_products.map((product) => ({ + id: product.product.id, + title: product.name, + description: null, + image: product.product.images?.[0]?.assets?.[0]?.url, + price: offer.pricing.price, + link: offer.links.webshop + })); + + const products = transformProducts(offer, dirtyProducts); + + offer.products = products; + } else { + const products = transformProducts(offer, [ + { + id: offer.id, + title: offer.heading, + description: offer.description, + price: offer.pricing.price, + link: offer.links.webshop + } + ]); + + offer.products = products; + offer.hideOfferDetails = true; + } + if (offer.id && configs.eventTracker) { configs.eventTracker?.trackOfferOpened({ 'of.id': offer.id, @@ -214,6 +329,7 @@ const OfferOverview = ({ return { ...offer, + // products: products, price: formatPrice( offer?.pricing?.price, localeCode, @@ -241,11 +357,138 @@ const OfferOverview = ({ ); }; + const addProductListener = () => { + const productEls = document.querySelectorAll('.sgn-product-details'); + + productEls.forEach((productEl: HTMLElement) => { + const productId = productEl.dataset.offerProductId; + const minusBtn = productEl.querySelector( + '.sgn-offer-product-quantity-minus' + ); + const plusBtn = productEl.querySelector( + '.sgn-offer-product-quantity-plus' + ); + const quantityTxt = productEl.querySelector( + '.sgn-offer-product-quantity-text' + ); + const basketBtn = productEl.querySelector( + '.sgn-offer-product-add-basket' + ); + + let quantity = +(quantityTxt?.value ?? 0); + + plusBtn?.addEventListener('click', () => { + if (quantityTxt) quantityTxt.value = `${++quantity}`; + + productEl.dataset.offerProductQuantity = `${quantity}`; + + updateShoppingList( + { + ...offer, + basket: { + productId, + quantity + } + }, + 'plus' + ); + }); + + minusBtn?.addEventListener('click', () => { + if (quantityTxt && quantity >= 1) + quantityTxt.value = `${--quantity}`; + + productEl.dataset.offerProductQuantity = `${quantity}`; + + updateShoppingList( + { + ...offer, + basket: { + productId, + quantity + } + }, + 'minus' + ); + }); + + basketBtn?.addEventListener('click', () => { + addToShoppingList({ + ...offer, + basket: { + productId, + quantity + } + }); + }); + }); + }; + + const fetchProducts = async (offerId: string) => { + const res = await request<{ + offer_products: { + product: { + __typename: string; + id: string; + gtin: null | string; + images: { + assets: { + width: number; + url: string; + }[]; + }[]; + country_code: string; + brand: { + __typename: string; + id: string; + slug: string; + name: string; + description: null | string; + positive_logotype: null | string; + negative_logotype: null | string; + positive_logomark: null | string; + negative_logomark: null | string; + is_active: boolean; + country_code: string; + locale_code: string; + website_link: null | string; + business: null | string; + }; + }; + name: string; + external_id: string; + }[]; + }>({ + apiKey: configs.apiKey, + coreUrl: configs.coreUrl, + url: '/v4/rpc/get_offer_products', + method: 'post', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + id: offerId + }) + }); + + if (!res) throw new Error(); + + return res; + }; + + const closeModalListener = () => { + container + ?.querySelector('.sgn-modal-close') + ?.addEventListener('click', () => { + destroyModal(); + }); + }; + const addEventListeners = () => { document.querySelector('.sgn-modal-container')?.focus(); addOpenWebshopListener(); addShoppingListListener(); + addProductListener(); + closeModalListener(); }; return {render}; diff --git a/lib/kits/core-ui/components/common/shopping-list.styl b/lib/kits/core-ui/components/common/shopping-list.styl index a42d11d6..dd557c20 100644 --- a/lib/kits/core-ui/components/common/shopping-list.styl +++ b/lib/kits/core-ui/components/common/shopping-list.styl @@ -1,6 +1,6 @@ .sgn-shopping-popup position relative - background #F3F3F9 + background #ffffff color #202020 border-radius 12px padding 10px @@ -12,10 +12,10 @@ text-align left button - background-color #d3d3d3 - padding 4px 10px + background-color #f1f1f1 + padding 10px font-size 12px - border-radius 6px + border-radius 10px border 0 min-width 70px transition .2s @@ -41,23 +41,13 @@ > svg width 25px -.sgn-shopping-dd - display block - z-index 9 - position absolute - background #F3F3F9 - .sgn-shopping-list-items-container height 450px - overflow-y scroll + overflow-y auto list-style-type none - padding 10px + padding 0 color #202020 - > li - &:hover - cursor pointer - .sgn-shopping-list-item-container-crossed text-align center padding-top 15px @@ -65,7 +55,9 @@ .sgn-shopping-list-content-container border-bottom 1px solid #d5d5d5 padding 10px - align-items center + +.sgn-shopping-list-content-container-total + border-bottom unset !important .sgn-popup-header border-bottom 1px solid #eee @@ -74,13 +66,72 @@ font-size 18px .sgn-shopping-list-content + display flex + flex 1 + align-items center + gap 0.5rem font-size 16px line-height 1.6 overflow hidden text-overflow ellipsis + .sgn-shopping-list-content-heading + flex 1 + + .sgn-product-price-container + display flex + flex-direction row + justify-content center + align-items center + + .sgn-shopping-list-content-price + font-weight 500 + font-size 1.2rem + margin-left 0.3rem + + .sgn-offer-product-quantity + width 110px + + .sgn-offer-product-quantity-content + display flex + border 1px solid rgba(0, 0, 0, 0.12) + border-radius 19.5px + overflow hidden + padding 2px + margin-right .1rem + + button + min-width 0 + padding 0 + background transparent + color #4e4e4e + border-radius 100% + border 1px solid #7b7b7b + text-align center + width 36px + height 36px + + svg + vertical-align middle + width 16px + height 16px + + input + color #202020 + border unset + background unset + width 30px + padding 8px 0 + text-align center + + .sgn-shopping-list-content-ticker-box + &:hover + cursor pointer + .sgn-shopping-list-item-container-ticked - .sgn-shopping-list-content + opacity 0.5 + + .sgn-shopping-list-content-heading text-decoration line-through color rgb(139, 140, 143) @@ -107,6 +158,38 @@ .sgn-shopping-list-items-container height 400px +@media (max-width 840px) + .sgn-shopping-popup + .sgn-product-price-container + flex-direction column-reverse + align-items flex-end + + .sgn-offer-product-quantity + width auto + margin-top .833rem + + .sgn-product-price + flex 1 + font-size 1.2rem + margin-top 1rem + + .sgn-offer-product-quantity-content + button + cursor pointer + border 0.5px solid #7b7b7b + + svg + width 10px + height 10px + + input + color #202020 + border unset + background unset + width 30px + padding 8px 0 + text-align center + @media print .sgn__header overflow visible @@ -162,4 +245,11 @@ .sgn-shopping-list-content width 100% - white-space normal \ No newline at end of file + white-space normal + display flex + + .sgn-shopping-list-content-heading + flex 1 + + .sgn-product-price-container + flex-direction row diff --git a/lib/kits/core-ui/components/common/shopping-list.ts b/lib/kits/core-ui/components/common/shopping-list.ts index b2c793c9..880f9468 100644 --- a/lib/kits/core-ui/components/common/shopping-list.ts +++ b/lib/kits/core-ui/components/common/shopping-list.ts @@ -1,6 +1,11 @@ import Mustache from 'mustache'; import * as clientLocalStorage from '../../../../storage/client-local'; -import {createModal, formatPrice, translate} from '../helpers/component'; +import { + createModal, + formatPrice, + translate, + updateShoppingList +} from '../helpers/component'; import './shopping-list.styl'; const defaultTemplate = `\ @@ -11,30 +16,39 @@ const defaultTemplate = `\
    {{#offers}} -
  1. +
  2. +
    + +
    - {{#price}}{{price}} - {{/price}}{{name}}
    + {{name}} +
    +
    +
    +
    + + + +
    +
    + {{#price}} +
    + {{price}} +
    + {{/price}}
    @@ -44,13 +58,48 @@ const defaultTemplate = `\
  3. +
    + +
    - {{#price}}{{price}} - {{/price}}{{name}}
    + {{name}} +
    +
    +
    +
    + + + +
    +
    + {{#price}} +
    + {{price}} +
    + {{/price}}
  4. {{/tickedOffers}} + {{#totalPrice}} +
  5. +
    +
    +
    + Total +
    +
    + {{totalPrice}} +
    +
    +
    +
  6. + {{/totalPrice}} {{#hasTicked}}
  7. @@ -106,7 +155,8 @@ const ShoppingList = ({template}) => { hasTicked: transformSavedOffers(storedPublicationOffers)?.filter( (offer) => offer.is_ticked - ).length > 0 + ).length > 0, + totalPrice: getTotalPrice() }); createModal(container, destroyModal); @@ -125,7 +175,7 @@ const ShoppingList = ({template}) => { ...offer, price: offer?.pricing?.price ? formatPrice( - offer?.pricing?.price, + offer?.pricing?.price * (offer?.quantity || 1), localeCode, offer?.pricing?.currency || currency ) @@ -135,9 +185,9 @@ const ShoppingList = ({template}) => { const addTickerListener = () => { container - ?.querySelectorAll('.sgn-shopping-list-item-container') + ?.querySelectorAll('.sgn-shopping-list-content-ticker-box') .forEach((itemEl) => { - itemEl.addEventListener('click', tickOffer, false); + itemEl.addEventListener('change', tickOffer); }); }; @@ -145,7 +195,7 @@ const ShoppingList = ({template}) => { const storedPublicationOffers = clientLocalStorage.get( 'publication-saved-offers' ); - const index = e.currentTarget.dataset?.id; + const index = e.currentTarget.value; storedPublicationOffers[index].is_ticked = !storedPublicationOffers[index].is_ticked; @@ -167,7 +217,8 @@ const ShoppingList = ({template}) => { hasTicked: transformSavedOffers(storedPublicationOffers)?.filter( (offer) => offer.is_ticked - ).length > 0 + ).length > 0, + totalPrice: getTotalPrice() }); addEventListeners(); @@ -215,7 +266,8 @@ const ShoppingList = ({template}) => { if (container) container.innerHTML = Mustache.render(template, { translations, - offers: transformSavedOffers(validOffers) + offers: transformSavedOffers(validOffers), + totalPrice: getTotalPrice() }); addEventListeners(); @@ -276,14 +328,111 @@ const ShoppingList = ({template}) => { }); }; + const getTotalPrice = () => { + const {localeCode, currency} = translations; + const storedPublicationOffers = clientLocalStorage.get( + 'publication-saved-offers' + ); + + const totalPrice = storedPublicationOffers?.reduce((acc, product) => { + return acc + product.pricing.price * product.quantity; + }, 0); + + return totalPrice ? formatPrice(totalPrice, localeCode, currency) : ''; + }; + + const updateQuantityHandler = ( + productEl: HTMLElement, + action: 'plus' | 'minus' + ) => { + const {localeCode, currency} = translations; + const productId = productEl.dataset.offerProductId; + const priceEl = productEl.querySelector( + '.sgn-shopping-list-content-price' + ); + const quantityTxt = productEl.querySelector( + '.sgn-offer-product-quantity-text' + ); + const totalPriceContainer = container?.querySelector( + '.sgn-shopping-list-content-container-total' + ); + const totalPriceEL = container?.querySelector( + '.sgn-shopping-list-content-price-total' + ); + + const storedPublicationOffers = clientLocalStorage.get( + 'publication-saved-offers' + ); + + let quantity = Number(quantityTxt?.value ?? 1); + + const product = storedPublicationOffers.find( + (product) => product.id === productId + ); + + if (quantityTxt) { + quantityTxt.value = + action === 'plus' ? `${++quantity}` : `${--quantity}`; + + if (quantityTxt?.value === '0' && action === 'minus') { + productEl.remove(); + } + + if (priceEl && product?.pricing?.price && quantity) { + const priceNum = product?.pricing?.price * (quantity || 1); + + priceEl.innerHTML = formatPrice(priceNum, localeCode, currency); + } + + updateShoppingList( + { + ...product, + basket: { + productId, + quantity + } + }, + action + ); + + if (totalPriceEL && getTotalPrice()) { + totalPriceEL.innerHTML = getTotalPrice(); + } else { + totalPriceContainer?.remove(); + } + } + }; + + const addQuantityListener = () => { + const productEls = document.querySelectorAll( + '.sgn-shopping-list-item-container' + ); + + productEls.forEach((productEl: HTMLElement) => { + const minusBtn = productEl.querySelector( + '.sgn-offer-product-quantity-minus' + ); + const plusBtn = productEl.querySelector( + '.sgn-offer-product-quantity-plus' + ); + + plusBtn?.addEventListener('click', () => + updateQuantityHandler(productEl, 'plus') + ); + minusBtn?.addEventListener('click', () => + updateQuantityHandler(productEl, 'minus') + ); + }); + }; + const formatListToShare = (data, newLineDelimiter = `\n`) => { let offerStr = ''; data?.forEach((offer) => { if (!offer.is_ticked) { offerStr += offer.price - ? `${offer.price} - ${offer.name}` - : `${offer.name}`; + ? `${offer.price} - ${offer.name} (${offer.quantity || 1})` + : `${offer.name} (${offer.quantity || 1})`; offerStr += newLineDelimiter; } }); @@ -299,6 +448,7 @@ const ShoppingList = ({template}) => { addClearListListener(); addPrintListener(); addShareListener(); + addQuantityListener(); }; return {render}; diff --git a/lib/kits/core-ui/components/common/sidebar.styl b/lib/kits/core-ui/components/common/sidebar.styl index 58169f4a..5460713c 100644 --- a/lib/kits/core-ui/components/common/sidebar.styl +++ b/lib/kits/core-ui/components/common/sidebar.styl @@ -63,6 +63,7 @@ height 2px background #ffffff margin-bottom 8px + border-radius 1px .sgn__sidebar-control-bars list-style none @@ -70,6 +71,9 @@ width 25px background rgba(0, 0, 0, 0.4) padding 10px 10px 2px + border-radius 6px + opacity 0 + visibility hidden & .sgn__sidebar-control-bar1 -webkit-transition all 0.3s ease-in-out @@ -122,16 +126,15 @@ position absolute display none z-index 99 - height 80px + height 100vh margin auto width 100% text-align center animation-iteration-count 1 animation-name sgn-animate-sidebar animation-timing-function ease + box-shadow rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.2) 0px 0px 1px - .sgn__sidebar-content-container - box-shadow rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.2) 0px 0px 1px > .sgn__nav position relative !important @@ -141,7 +144,7 @@ color inherit -webkit-transform none !important transform none !important - box-shadow rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.2) 0px 0px 1px + box-shadow rgba(0, 0, 0, 0.4) 2px 0px 4px .sgn__nav-content padding 8px 8px 2px @@ -273,21 +276,29 @@ width unset !important margin-left unset!important -.sgn-animate-sidebar-left - display block - animation-name sgn-animate-sidebar-left - animation-timing-function ease - transition transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0) !important +.sgn__nav-content-mobile + display none -.sgn-animate-sidebar-right - display block - animation-name sgn-animate-sidebar-right - animation-timing-function ease - transition transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0) !important + .sgn__close-publication + visibility hidden + opacity 0 + + +@media (max-width 1200px) + .sgn-animate-sidebar-controls + visibility visible !important + opacity 1 !important + animation fadeIn 0.3s -@media (max-width 840px) .sgn__sidebar--open + .sgn__nav-content-mobile + display none + + .sgn__nav-content + .sgn__close-publication + display none !important + .sgn-sections-list-items-container, .sgn-pages-list-items-container, .sgn-page-decoration-list-items-container text-align center @@ -301,6 +312,50 @@ width unset !important margin-left unset!important + .sgn__nav-content-mobile + display block + + .sgn-animate-home-close + visibility visible + animation halfFadeIn 0.3s + + > .sgn__close-publication + opacity 0.5 + position absolute + top 0 + left 0 + margin 18px 10px + z-index 99 + color inherit + vertical-align middle + padding 8px + border 0 + cursor pointer + width 50px + background rgba(0, 0, 0, 0.4) + border-radius 6px + color #ffffff + + > svg + width 25px + height 25px + + &:hover, + &:active + opacity 1 + +.sgn-animate-sidebar-left + display block + animation-name sgn-animate-sidebar-left + animation-timing-function ease + transition transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0) !important + +.sgn-animate-sidebar-right + display block + animation-name sgn-animate-sidebar-right + animation-timing-function ease + transition transform 0.5s cubic-bezier(0.77,0.2,0.05,1.0) !important + @keyframes sgn-animate-sidebar-left 0% transform translateX(-290px) @@ -324,3 +379,15 @@ transform translateX(0) 100% transform translateX(-290px) + +@keyframes halfFadeIn + 0% + opacity 0 + 100% + opacity 0.5 + +@keyframes fadeOut + 0% + opacity 0.5 + 100% + opacity 0 \ No newline at end of file diff --git a/lib/kits/core-ui/components/helpers/component.ts b/lib/kits/core-ui/components/helpers/component.ts index 427956a2..d1b54768 100644 --- a/lib/kits/core-ui/components/helpers/component.ts +++ b/lib/kits/core-ui/components/helpers/component.ts @@ -1,6 +1,7 @@ import Mustache from 'mustache'; import * as locales from '../../../../../locales'; import {ESC as EscKey} from '../../../../key-codes'; +import * as clientLocalStorage from '../../../../storage/client-local'; export const insertAfter = (referenceNode, newNode) => { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); @@ -62,7 +63,11 @@ export const formatPrice = (price, localeCode = 'en_US', currency = 'USD') => { return new Intl.NumberFormat(localeCode.replace('_', '-'), { style: 'currency', currency - }).format(price); + }) + .format(price) + .replace(currency, '') + .replace('kr.', '') + .trim(); }; export const parseDateStr = (dateStr: string) => { @@ -281,3 +286,127 @@ export const transformWebshopLink = (url) => { return url; }; + +export const dispatchProductClickEvent = (detail) => { + const scriptEl = document.getElementById('sgn-sdk'); + const dataset = scriptEl?.dataset; + const mainContainer = + dataset?.componentListPublicationsContainer || + dataset?.componentPublicationContainer || + ''; + const mainContainerEl = document.querySelector(mainContainer); + + mainContainerEl?.dispatchEvent( + new CustomEvent('publication:product_clicked', { + detail + }) + ); +}; + +export const animateShoppingListCounter = () => { + const shoppingListCounter = document.querySelector( + '.sgn__offer-shopping-list-counter' + ); + + shoppingListCounter?.classList.remove('sgn-animate-bounce'); + shoppingListCounter?.classList.add('sgn-animate-bounce'); +}; + +export const updateShoppingList = (offer, action: 'plus' | 'minus') => { + const storedPublicationOffers = + clientLocalStorage.get('publication-saved-offers') || []; + + let isNew = false; + + let shopListOffer = { + id: offer.id, + name: offer.name, + pricing: {price: offer.price, currency: offer.currency_code}, + quantity: 1, + is_ticked: false + }; + + const allPricesAreTheSame = offer.products?.every( + (product, index, array) => product.price === array[0].price + ); + + const useOfferPrice = + allPricesAreTheSame && offer.price !== offer.products[0].price; + + if (offer.basket?.productId) { + const product = offer.products?.find( + ({id}) => id == offer.basket?.productId + ); + if (product) { + shopListOffer = { + id: product.id, + name: product.title, + pricing: { + price: useOfferPrice + ? offer.price || offer.pricing.price + : product?.price, + currency: offer.currency_code + }, + quantity: 1, + is_ticked: false + }; + } + } + + const isOfferInList = storedPublicationOffers.some( + (storedOffer) => storedOffer.id === shopListOffer.id + ); + + if (!isOfferInList && action !== 'minus') { + storedPublicationOffers.push(shopListOffer); + isNew = true; + dispatchProductClickEvent({product: shopListOffer}); + } else { + const updatedOffers = storedPublicationOffers + .map((storedOffer) => { + if (storedOffer.id === shopListOffer.id) { + if (action === 'plus') { + storedOffer.quantity += 1; + } else if (action === 'minus') { + storedOffer.quantity -= 1; + } + + dispatchProductClickEvent({product: storedOffer}); + + if (storedOffer.quantity <= 0) { + return null; + } + } + + return storedOffer; + }) + .filter(Boolean); + + storedPublicationOffers.splice(0, storedPublicationOffers.length); + storedPublicationOffers.push(...updatedOffers); + } + + clientLocalStorage.setWithEvent( + 'publication-saved-offers', + storedPublicationOffers, + 'tjek_shopping_list_update' + ); + + if (isNew) { + animateShoppingListCounter(); + } +}; + +export const closeSidebar = () => { + const sidebarControl = document?.querySelector( + '.sgn__sidebar-control' + ); + + if (sidebarControl) { + const matchedMedia = window.matchMedia('(max-width: 1200px)'); + + if (matchedMedia.matches) { + sidebarControl?.click(); + } + } +}; diff --git a/lib/kits/core-ui/components/incito-publication/section-list.ts b/lib/kits/core-ui/components/incito-publication/section-list.ts index 5d13e2c1..d9413568 100644 --- a/lib/kits/core-ui/components/incito-publication/section-list.ts +++ b/lib/kits/core-ui/components/incito-publication/section-list.ts @@ -1,5 +1,5 @@ import Mustache from 'mustache'; -import {destroyModal} from '../helpers/component'; +import {destroyModal, closeSidebar} from '../helpers/component'; import './section-list.styl'; const defaultTemplate = `\ @@ -81,6 +81,7 @@ const SectionList = ({sgnData, template, scriptEls}) => { const sectionOffset = sectionCell.offsetTop - headerOffset || 0; destroyModal(); + closeSidebar(); incitoEl?.scrollTo({top: sectionOffset, behavior: 'smooth'}); }; diff --git a/lib/kits/core-ui/components/paged-publication/main-container.styl b/lib/kits/core-ui/components/paged-publication/main-container.styl index 6a4e4162..ee691d61 100644 --- a/lib/kits/core-ui/components/paged-publication/main-container.styl +++ b/lib/kits/core-ui/components/paged-publication/main-container.styl @@ -22,6 +22,7 @@ z-index 99 &[data-component-template-disable-header="true"] + &[data-component-template-enable-sidebar="true"] .verso__scroller top 0 diff --git a/lib/kits/core-ui/components/paged-publication/main-container.ts b/lib/kits/core-ui/components/paged-publication/main-container.ts index 49505e60..72256554 100644 --- a/lib/kits/core-ui/components/paged-publication/main-container.ts +++ b/lib/kits/core-ui/components/paged-publication/main-container.ts @@ -68,7 +68,7 @@ const MainContainer = ({ const render = () => { // @ts-expect-error el.innerHTML = Mustache.render(template?.innerHTML || defaultTemplate, { - disableHeader: scriptEls.disableHeader || scriptEls.enableSidebar, + disableHeader: scriptEls.disableHeader, enableSidebar: scriptEls.enableSidebar, sidebarPosition: scriptEls.sidebarPosition }); diff --git a/lib/kits/core-ui/components/paged-publication/page-list.ts b/lib/kits/core-ui/components/paged-publication/page-list.ts index efcaa512..8b345fb6 100644 --- a/lib/kits/core-ui/components/paged-publication/page-list.ts +++ b/lib/kits/core-ui/components/paged-publication/page-list.ts @@ -1,7 +1,7 @@ import Mustache from 'mustache'; import {request, V2Page} from '../../../core'; import {Viewer} from '../../../paged-publication'; -import {destroyModal, pushQueryParam} from '../helpers/component'; +import {destroyModal, pushQueryParam, closeSidebar} from '../helpers/component'; import {transformScriptData} from '../helpers/transformers'; import './page-list.styl'; @@ -77,6 +77,7 @@ const PageList = ({ const {pageId, pageNum} = e.currentTarget.dataset; destroyModal(); + closeSidebar(); sgnViewer?.navigateToPageId(pageId); if (scriptEls.displayUrlParams?.toLowerCase() === 'query') { diff --git a/lib/kits/core-ui/incito-publication.ts b/lib/kits/core-ui/incito-publication.ts index 9e0b1882..1e341e3d 100644 --- a/lib/kits/core-ui/incito-publication.ts +++ b/lib/kits/core-ui/incito-publication.ts @@ -15,7 +15,9 @@ import { transformFilter, getHashFragments, pushQueryParam, - transformWebshopLink + transformWebshopLink, + animateShoppingListCounter, + dispatchProductClickEvent } from './components/helpers/component'; import MainContainer from './components/incito-publication/main-container'; import SectionList from './components/incito-publication/section-list'; @@ -164,15 +166,6 @@ const IncitoPublication = ( }).render(); }); - const animateShoppingListCounter = () => { - const shoppingListCounter = document.querySelector( - '.sgn__offer-shopping-list-counter' - ); - - shoppingListCounter?.classList.remove('sgn-animate-bounce'); - shoppingListCounter?.classList.add('sgn-animate-bounce'); - }; - const setOptions = async (opts?: any) => { options = { el: document.querySelector('.sgn__incito'), @@ -269,7 +262,12 @@ const IncitoPublication = ( configs: options, scriptEls, sgnData, - offer: {fetchOffer, viewId, publicationId, products}, + offer: { + fetchOffer, + viewId, + publicationId, + products + }, type: 'incito', addToShoppingList }).render(); @@ -333,30 +331,60 @@ const IncitoPublication = ( }; const addToShoppingList = (offer) => { - const storedPublicationOffers = clientLocalStorage.get( - 'publication-saved-offers' - ); - const shopListOffer = { + const storedPublicationOffers = + clientLocalStorage.get('publication-saved-offers') || []; + let shopListOffer = { + id: offer.id, name: offer.name, pricing: {price: offer.price, currency: offer.currency_code}, + quantity: 1, is_ticked: false }; - if (!storedPublicationOffers) { - clientLocalStorage.setWithEvent( - 'publication-saved-offers', - [shopListOffer], - 'tjek_shopping_list_update' - ); - } else { - storedPublicationOffers.push(shopListOffer); - clientLocalStorage.setWithEvent( - 'publication-saved-offers', - storedPublicationOffers, - 'tjek_shopping_list_update' + if (offer.basket?.productId) { + const product = offer.products?.find( + ({id}) => id == offer.basket?.productId ); + if (product) { + shopListOffer = { + id: product.id, + name: product.title, + pricing: { + price: offer.price, + currency: offer.currency_code + }, + quantity: offer.basket?.quantity || 1, + is_ticked: false + }; + } } + storedPublicationOffers.push(shopListOffer); + + const mergedOffers = storedPublicationOffers.reduce( + (acc, currentOffer) => { + const existingOffer = acc.find( + (offer) => offer.id === currentOffer.id + ); + + if (existingOffer) { + existingOffer.quantity += currentOffer.quantity; + } else { + acc.push({...currentOffer}); + } + + return acc; + }, + [] + ); + + clientLocalStorage.setWithEvent( + 'publication-saved-offers', + mergedOffers, + 'tjek_shopping_list_update' + ); + + dispatchProductClickEvent({product: shopListOffer}); animateShoppingListCounter(); }; diff --git a/lib/kits/core-ui/paged-publication.ts b/lib/kits/core-ui/paged-publication.ts index 5075c205..f758e2e7 100644 --- a/lib/kits/core-ui/paged-publication.ts +++ b/lib/kits/core-ui/paged-publication.ts @@ -14,7 +14,9 @@ import { getHashFragments, pushQueryParam, transformFilter, - translate + translate, + animateShoppingListCounter, + dispatchProductClickEvent } from './components/helpers/component'; import {transformScriptData} from './components/helpers/transformers'; import MainContainer from './components/paged-publication/main-container'; @@ -146,15 +148,6 @@ const PagedPublication = ( }).render(); }); - const animateShoppingListCounter = () => { - const shoppingListCounter = document.querySelector( - '.sgn__offer-shopping-list-counter' - ); - - shoppingListCounter?.classList.remove('sgn-animate-bounce'); - shoppingListCounter?.classList.add('sgn-animate-bounce'); - }; - const setOptions = async (opts?: any) => { options = { el: document.querySelector('.sgn__pp'), @@ -325,30 +318,42 @@ const PagedPublication = ( }; const addToShoppingList = (hotspot: V2Hotspot) => { - const storedPublicationOffers = clientLocalStorage.get( - 'publication-saved-offers' - ); + const storedPublicationOffers = + clientLocalStorage.get('publication-saved-offers') || []; const shopListOffer = { + id: hotspot.id, name: hotspot.heading, pricing: hotspot.offer?.pricing, + quantity: 1, is_ticked: false }; - if (!storedPublicationOffers) { - clientLocalStorage.setWithEvent( - 'publication-saved-offers', - [shopListOffer], - 'tjek_shopping_list_update' - ); - } else { - storedPublicationOffers.push(shopListOffer); - clientLocalStorage.setWithEvent( - 'publication-saved-offers', - storedPublicationOffers, - 'tjek_shopping_list_update' - ); - } + storedPublicationOffers.push(shopListOffer); + + const mergedOffers = storedPublicationOffers.reduce( + (acc, currentOffer) => { + const existingOffer = acc.find( + (offer) => offer.id === currentOffer.id + ); + + if (existingOffer) { + existingOffer.quantity += currentOffer.quantity; + } else { + acc.push({...currentOffer}); + } + + return acc; + }, + [] + ); + + clientLocalStorage.setWithEvent( + 'publication-saved-offers', + mergedOffers, + 'tjek_shopping_list_update' + ); + dispatchProductClickEvent({product: shopListOffer}); animateShoppingListCounter(); }; diff --git a/locales/da_DK.ts b/locales/da_DK.ts index 83a5eb22..f648aa4e 100644 --- a/locales/da_DK.ts +++ b/locales/da_DK.ts @@ -20,5 +20,7 @@ export default { publication_viewer_close_label: 'Tilbage', publication_viewer_add_to_shopping_list: 'Tilføj til indkøbsliste', publication_viewer_visit_webshop_link: 'Besøg webshoplink', - publication_viewer_upcoming: 'Kommende' + publication_viewer_upcoming: 'Kommende', + publication_viewer_offer_price_from: 'Fra', + publication_viewer_offer_valid_from: 'Gælder kun fra d. ' }; diff --git a/locales/en_US.ts b/locales/en_US.ts index ac45464e..8ed94b3b 100644 --- a/locales/en_US.ts +++ b/locales/en_US.ts @@ -20,5 +20,7 @@ export default { publication_viewer_close_label: 'Close', publication_viewer_add_to_shopping_list: 'Add to Shopping List', publication_viewer_visit_webshop_link: 'Visit Webshop Link', - publication_viewer_upcoming: 'Upcoming' + publication_viewer_upcoming: 'Upcoming', + publication_viewer_offer_price_from: 'From', + publication_viewer_offer_valid_from: 'Valid from ' }; diff --git a/locales/nb_NO.ts b/locales/nb_NO.ts index 97114248..5a95eff4 100644 --- a/locales/nb_NO.ts +++ b/locales/nb_NO.ts @@ -21,5 +21,7 @@ export default { publication_viewer_close_label: 'Tilbake', publication_viewer_add_to_shopping_list: 'Legg til handleliste', publication_viewer_visit_webshop_link: 'Besøk nettbutikklink', - publication_viewer_upcoming: 'Påkommende' + publication_viewer_upcoming: 'Påkommende', + publication_viewer_offer_price_from: 'Fra', + publication_viewer_offer_valid_from: 'Gjelder kun fra ' }; diff --git a/locales/sv_SE.ts b/locales/sv_SE.ts index 14167688..60268d63 100644 --- a/locales/sv_SE.ts +++ b/locales/sv_SE.ts @@ -20,5 +20,7 @@ export default { publication_viewer_close_label: 'Tillbaka', publication_viewer_add_to_shopping_list: 'Lägg till inköpslista', publication_viewer_visit_webshop_link: 'Besök webbshoplänk', - publication_viewer_upcoming: 'Kommende' + publication_viewer_upcoming: 'Kommende', + publication_viewer_offer_price_from: 'Från', + publication_viewer_offer_valid_from: 'Gäller endast fr.o.m ' };