{"version":3,"file":"product-cards-CmQ9ilJp.js","sources":["../../../../scripts/components/products/product-cards.vue","../../../../scripts/services/product-cards.ts"],"sourcesContent":["<template>\r\n <section v-if=\"content.productCards.length\" ref=\"product-cards\" class=\"slab mb-0\">\r\n <div class=\"container\">\r\n <h2 v-if=\"content.header\" class=\"text--serif mb-3\" v-html=\"content.header\"></h2>\r\n <ul :class=\"['product-cards', {'-mobile-horizontal-scroll' : enableMobileHorizontalScroll}]\">\r\n <li v-for=\"(productCard, index) in content.productCards\" :id=\"'product-card-'+ productCard.id\" :key=\"index\" v-track-product-card-list-item=\"{ productCard: productCard, productType: content.productType }\" :data-idx=\"index\">\r\n <div v-if=\"productCard.isSponsored\" class=\"flag__overlay\" data-flag-title=\"Sponsored\" role=\"img\" title=\"Sponsored\"></div>\r\n <div v-if=\"productCard.imageUrl\" class=\"-image-container\">\r\n <a :href=\"productCard.link\" :title=\"productCard.name\" :class=\"[productCard.isSponsored ? 'js-sponsored': ''] \" @click=\"trackProductCardSelectItem(productCard, content.productType)\">\r\n <img :src=\"productCard.imageUrl\" :class=\"['img-fit--cover', productCard.isSponsored ? 'js-sponsored-img': '']\" :alt=\"productCard.name\" loading=\"lazy\" />\r\n </a>\r\n </div>\r\n <div class=\"-content\">\r\n <button aria-label=\"Save to Wanderlist\"\r\n class=\"wl-heartable -top-right\"\r\n :data-wl-type=\"heartCategory\"\r\n :data-wl-id=\"getWanderlistId(productCard)\"\r\n :data-wl-title=\"getTitle(productCard)\"\r\n :data-wl-list-name=\"productCard.wanderlistName\"></button>\r\n <h3 v-if=\"productCard.name\">\r\n <a :href=\"productCard.link\" :title=\"productCard.name\" :class=\"['-no-decoration-idle', productCard.isSponsored ? 'js-sponsored': ''] \" @click=\"trackProductCardSelectItem(productCard, content.productType)\">\r\n <span class=\"weglot-exclude\">{{ productCard.name }}</span><span v-if=\"productCard.travelLength\" class=\"-length\">({{ productCard.travelLength }})</span>\r\n </a>\r\n </h3>\r\n <template v-if=\"content.productType === ProductType.CRUISES\">\r\n <div v-if=\"productCard.company\">Cruise Line: <a :href=\"productCard.brandPageUrl\" class=\"-no-decoration-idle -neutral weglot-exclude\">{{ productCard.company }}</a></div>\r\n <div v-if=\"productCard.shipName\">\r\n Ship: <a v-if=\"productCard.shipUrl\" class=\"-no-decoration-idle -neutral weglot-exclude\" :href=\"productCard.shipUrl\" v-html=\"productCard.shipName\"></a>\r\n <span v-else class=\"weglot-exclude\" v-html=\"productCard.shipName\"></span>\r\n </div>\r\n <div v-if=\"productCard.location\" v-html=\"productCard.location\"></div>\r\n <div v-if=\"productCard.dates\" v-html=\"productCard.dates\"></div>\r\n </template>\r\n <template v-if=\"content.productType === ProductType.HOTELS\">\r\n <div v-if=\"productCard.location\" v-html=\"productCard.location\"></div>\r\n <div v-if=\"productCard.neighborhood\">Neighborhood: {{ productCard.neighborhood }}</div>\r\n </template>\r\n <template v-if=\"content.productType === ProductType.TOURS\">\r\n <div v-if=\"productCard.brandPageUrl\"><a :href=\"productCard.brandPageUrl\" class=\"-no-decoration-idle -neutral weglot-exclude\" v-html=\"productCard.company\"></a></div>\r\n <div v-else class=\"weglot-exclude\" v-html=\"productCard.company\"></div>\r\n <div v-if=\"productCard.location\" v-html=\"productCard.location\"></div>\r\n <div v-if=\"productCard.dates\" v-html=\"productCard.dates\"></div>\r\n </template>\r\n </div>\r\n </li>\r\n </ul>\r\n <div v-if=\"content.viewAllLink && content.viewAllText\" class=\"text-center\">\r\n <a :href=\"content.viewAllLink\" class=\"text--default\" @click=\"handleViewAllClick\" v-html=\"content.viewAllText\"></a>\r\n </div>\r\n </div>\r\n </section>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { ProductCard, ProductCards } from \"interfaces/card\";\r\n import { ProductType } from \"interfaces/enums\";\r\n import { getFormattedPageName } from \"services/layout/metadata\";\r\n import { trackEvent, trackProductCardSelectItem, trackTheProductCardViewItemList } from \"services/analytics\";\r\n import { enableHearts } from \"services/wanderlist\";\r\n import { capitalizeFirst, getVanillaPath, slugify } from \"virtuoso-shared-web-ui\";\r\n import { DirectiveBinding, nextTick, PropType, useTemplateRef } from \"vue\";\r\n\r\n const props = defineProps({\r\n content: {\r\n type: Object as PropType<ProductCards>,\r\n default: undefined\r\n },\r\n enableMobileHorizontalScroll: {\r\n type: Boolean,\r\n default: false\r\n }\r\n });\r\n\r\n const heartCategory = (props.content.productType || \"\").slice(0, -1); // De-pluralize hotels/cruises/tours\r\n const productCardsRef = useTemplateRef(\"product-cards\");\r\n const vTrackProductCardListItem = (el: HTMLElement, binding: DirectiveBinding) => {\r\n if (binding.oldValue === undefined) { // To prevent duplicate tracking\r\n trackTheProductCardViewItemList(el, binding.value.productCard, binding.value.productType);\r\n }\r\n };\r\n\r\n function getTitle(product: ProductCard): string {\r\n return (product.travelLength) ? `${product.name} (${product.travelLength})` : product.name;\r\n }\r\n\r\n function getWanderlistId(product: ProductCard): string {\r\n let idOrUrl = product.id;\r\n if (props.content.productType === ProductType.CRUISES) {\r\n idOrUrl = `https://www.virtuoso.com/travel/luxury-cruises/cruises/${product.cruiseId}/${slugify(product.name)}`;\r\n } else if (props.content.productType === ProductType.TOURS) {\r\n idOrUrl = `https://www.virtuoso.com${getVanillaPath(product.link)}`;\r\n }\r\n return idOrUrl;\r\n }\r\n\r\n function handleViewAllClick(): void {\r\n trackEvent(\"view_more\", {\r\n item_list_name: getFormattedPageName(),\r\n item_name: props.content.viewAllText,\r\n item_category: capitalizeFirst(props.content.productType).slice(0, -1)\r\n });\r\n }\r\n\r\n nextTick(() => {\r\n enableHearts(productCardsRef.value);\r\n });\r\n</script>\r\n","import { CruisesSearchResponse, HotelsSearchResponse, ToursSearchResponse } from \"interfaces/responses/product-search-responses\";\r\nimport { Subset } from \"interfaces/types/app-types\";\r\nimport { ProductCard, ProductCardSearchResult } from \"interfaces/card\";\r\nimport { ProductQuery } from \"interfaces/cms\";\r\nimport { ProductType } from \"interfaces/enums\";\r\nimport { getProducts } from \"services/api/search\";\r\nimport { transformCruiseSearchResponseToProductCard, transformHotelSearchResponseToProductCard, transformTourSearchResponseToProductCard } from \"services/transformers/content-transformers\";\r\n\r\n/**\r\n * Pulls products from the API for the supplied product type and query. Always resolves, will be an empty array in error scenarios\r\n * @param productType\r\n * @param query\r\n */\r\nexport function getProductCards(productType: ProductType, query: ProductQuery, isSponsored = false): Promise<ProductCardSearchResult> {\r\n const productPath =\r\n productType === ProductType.CRUISES || productType === ProductType.TOURS\r\n ? productType\r\n : \"properties\";\r\n\r\n return new Promise((resolve) => {\r\n getProducts(productPath, query).then((productResults) => {\r\n\r\n if (productResults.totalResults > 0) {\r\n const productCards = [] as ProductCard[];\r\n for (const prod of productResults.results) {\r\n let thisProd: ProductCard = {} as ProductCard;\r\n if (productType === ProductType.CRUISES) {\r\n thisProd = transformCruiseSearchResponseToProductCard(prod as CruisesSearchResponse);\r\n } else if (productType === ProductType.HOTELS) {\r\n thisProd = transformHotelSearchResponseToProductCard(prod as HotelsSearchResponse);\r\n } else if (productType === ProductType.TOURS) {\r\n thisProd = transformTourSearchResponseToProductCard(prod as ToursSearchResponse);\r\n }\r\n\r\n thisProd.isSponsored = isSponsored;\r\n productCards.push(thisProd);\r\n }\r\n resolve({ productCards: productCards, totalResults: productResults.totalResults });\r\n } else {\r\n resolve({ productCards: [], totalResults: 0 });\r\n }\r\n }, () => {\r\n console.error(`Error retrieving ${productType}`);\r\n resolve({ productCards: [], totalResults: 0 });\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * Returns a merged array of random and sponsored products, with the sponsored ones first (in a random order)\r\n * @param productType - hotels, cruises, or tours\r\n * @param productQuery - rows to return, query values, etc\r\n * @param sponsoredIds - comma delimited MEIDs, likely from dotCMS\r\n * @param skipRandomProducts - if true, doesn't run the random product query at all (presumably because we already know there aren't any); default false.\r\n * @param suppressSponsoredTag - if true, the passed on sponsoredIds will not display with the Sponsored tag; default false\r\n */\r\nexport function getSponsoredAndRandomProducts(\r\n productType: ProductType,\r\n productQuery: ProductQuery,\r\n sponsoredIds: string,\r\n skipRandomProducts = false,\r\n suppressSponsoredTag = false\r\n): Promise<ProductCardSearchResult> {\r\n return new Promise((resolve) => {\r\n const numCards = productQuery.rowsLimit || 3;\r\n let totalResults = 0;\r\n let sponsoredProductsPromise, randomProductsPromise;\r\n let sponsoredProducts = [] as ProductCard[];\r\n let randomProducts = [] as ProductCard[];\r\n let sponsoredArray: number[] = [];\r\n\r\n if (sponsoredIds) {\r\n sponsoredArray = sponsoredIds\r\n .split(\",\")\r\n .map((productId: string) => {\r\n return productId && productId.length ? parseInt(productId, 10) : 0;\r\n })\r\n .filter((item) => (item ? true : false)); // Filter to remove empty items if someone left extra commas and/or spaces\r\n\r\n if (sponsoredArray.length) {\r\n const isSponsored = suppressSponsoredTag ? false : true;\r\n sponsoredProductsPromise = getProductCards(\r\n productType,\r\n { productsIds: sponsoredArray, rowsLimit: numCards },\r\n isSponsored\r\n );\r\n sponsoredProductsPromise.then((sponsoredProductCards) => {\r\n if (sponsoredProductCards.productCards.length) {\r\n sponsoredProducts = sponsoredProductCards.productCards;\r\n totalResults = sponsoredProductCards.totalResults;\r\n }\r\n });\r\n }\r\n }\r\n\r\n if (!skipRandomProducts) {\r\n randomProductsPromise = getProductCards(productType, productQuery);\r\n randomProductsPromise.then((cards) => {\r\n if (cards.productCards.length) {\r\n randomProducts = cards.productCards;\r\n totalResults = cards.totalResults;\r\n }\r\n });\r\n }\r\n\r\n Promise.all([randomProductsPromise, sponsoredProductsPromise]).then(() => {\r\n // Merge them, de-dupe them, truncate them\r\n let deDupedRandomCards: ProductCard[] = [];\r\n if (sponsoredArray.length) {\r\n deDupedRandomCards = randomProducts.filter(\r\n (card) => !sponsoredArray.includes(parseInt(card.id, 10))\r\n );\r\n } else {\r\n deDupedRandomCards = randomProducts;\r\n }\r\n\r\n const mergedProductCards = sponsoredProducts.concat(deDupedRandomCards);\r\n\r\n if (mergedProductCards.length > numCards) {\r\n mergedProductCards.length = numCards;\r\n }\r\n resolve({ productCards: mergedProductCards, totalResults: totalResults });\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * Generates the label for the view all link based on the product type and total results count.\r\n * @param {Subset<ProductType, \"hotels\" | \"cruises\" | \"tours\" | \"ships\">} productType - The type of product (hotels, cruises, tours, or ships).\r\n * @param {number} totalResults - The total number of results for the given product type.\r\n * @param {number} [rowsQueryLimit=3] - The default number of rows to query.\r\n * @returns {string} The generated view all label.\r\n */\r\nexport function getViewAllLabel(\r\n productType: Subset<ProductType, ProductType.HOTELS | ProductType.CRUISES | ProductType.TOURS | ProductType.SHIPS>,\r\n totalResults: number,\r\n rowsQueryLimit: number = 3\r\n): string {\r\n const productSpecLabel = (): string => {\r\n switch (productType) {\r\n case ProductType.TOURS: {\r\n return \"Tours & Experiences\";\r\n }\r\n case ProductType.SHIPS: {\r\n return \"Sailings\";\r\n }\r\n default: {\r\n return productType[0].toUpperCase() + productType.slice(1);\r\n }\r\n }\r\n };\r\n\r\n const viewLabel = (totalResults > rowsQueryLimit) ? \"More\" : \"All\";\r\n\r\n return `See ${viewLabel} ${productSpecLabel()}`;\r\n\r\n}\r\n"],"names":["props","__props","heartCategory","productCardsRef","useTemplateRef","vTrackProductCardListItem","el","binding","trackTheProductCardViewItemList","getTitle","product","getWanderlistId","idOrUrl","ProductType","slugify","getVanillaPath","handleViewAllClick","trackEvent","getFormattedPageName","capitalizeFirst","nextTick","enableHearts","getProductCards","productType","query","isSponsored","productPath","resolve","getProducts","productResults","productCards","prod","thisProd","transformCruiseSearchResponseToProductCard","transformHotelSearchResponseToProductCard","transformTourSearchResponseToProductCard","getSponsoredAndRandomProducts","productQuery","sponsoredIds","skipRandomProducts","suppressSponsoredTag","numCards","totalResults","sponsoredProductsPromise","randomProductsPromise","sponsoredProducts","randomProducts","sponsoredArray","productId","item","sponsoredProductCards","cards","deDupedRandomCards","card","mergedProductCards","getViewAllLabel","rowsQueryLimit","productSpecLabel"],"mappings":"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"}