import { type Lang } from "@/constants/languages" import { getFiltersFromHotels } from "@/stores/destination-data/helper" import { getIntl } from "@/i18n" import { getCityByCityIdentifier, getHotelIdsByCityIdentifier, getHotelIdsByCountry, getHotelsByHotelIds, } from "../../hotels/utils" import { ApiCountry } from "@/types/enums/country" import { RTETypeEnum } from "@/types/rte/enums" import type { MetadataInputSchema, RawMetadataSchema, } from "@/types/trpc/routers/contentstack/metadata" export const affix = "metadata" /** * Truncates the given text "intelligently" based on the last period found near the max length. * * - If a period exists within the extended range (`maxLength` to `maxLength + maxExtension`), * the function truncates after the closest period to `maxLength`. * - If no period is found in the range, it truncates the text after the last period found in the max length of the text. * - If no periods exist at all, it truncates at `maxLength` and appends ellipsis (`...`). * * @param {string} text - The input text to be truncated. * @param {number} [maxLength=150] - The desired maximum length of the truncated text. * @param {number} [minLength=120] - The minimum allowable length for the truncated text. * @param {number} [maxExtension=10] - The maximum number of characters to extend beyond `maxLength` to find a period. * @returns {string} - The truncated text. */ function truncateTextAfterLastPeriod( text: string, maxLength: number = 150, minLength: number = 120, maxExtension: number = 10 ): string { if (text.length <= maxLength) { return text } // Define the extended range const extendedEnd = Math.min(text.length, maxLength + maxExtension) const extendedText = text.slice(0, extendedEnd) // Find all periods within the extended range and filter after minLength to get valid periods const periodsInRange = [...extendedText.matchAll(/\./g)].map( ({ index }) => index ) const validPeriods = periodsInRange.filter((index) => index + 1 >= minLength) if (validPeriods.length > 0) { // Find the period closest to maxLength const closestPeriod = validPeriods.reduce((closest, index) => Math.abs(index + 1 - maxLength) < Math.abs(closest + 1 - maxLength) ? index : closest ) return extendedText.slice(0, closestPeriod + 1) } // Fallback: If no period is found within the valid range, look for the last period in the truncated text const maxLengthText = text.slice(0, maxLength) const lastPeriodIndex = maxLengthText.lastIndexOf(".") if (lastPeriodIndex !== -1) { return text.slice(0, lastPeriodIndex + 1) } // Final fallback: Return maxLength text including ellipsis return `${maxLengthText}...` } export async function getTitle(data: RawMetadataSchema) { const intl = await getIntl() const metadata = data.web?.seo_metadata if (metadata?.title) { return metadata.title } if (data.hotelData) { if (data.subpageUrl) { const restaurantSubPage = data.hotelRestaurants?.find( (restaurant) => restaurant.nameInUrl === data.subpageUrl ) if (restaurantSubPage) { return intl.formatMessage( { id: "Explore {restaurantName} at {hotelName} in {destination}" }, { restaurantName: restaurantSubPage.name, hotelName: data.hotelData.name, destination: data.hotelData.address.city, } ) } switch (data.subpageUrl) { case data.additionalHotelData?.hotelParking.nameInUrl: return intl.formatMessage( { id: "Parking information for {hotelName} in {destination}" }, { hotelName: data.hotelData.name, destination: data.hotelData.address.city, } ) case data.additionalHotelData?.healthAndFitness.nameInUrl: return intl.formatMessage( { id: "Gym & Health Facilities at {hotelName} in {destination}" }, { hotelName: data.hotelData.name, destination: data.hotelData.address.city, } ) case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl: return intl.formatMessage( { id: "Accessibility information for {hotelName} in {destination}", }, { hotelName: data.hotelData.name, destination: data.hotelData.address.city, } ) case data.additionalHotelData?.meetingRooms.nameInUrl: return intl.formatMessage( { id: "Meetings, Conferences & Events at {hotelName} in {destination}", }, { hotelName: data.hotelData.name, destination: data.hotelData.address.city, } ) default: break } } return intl.formatMessage( { id: "Stay at {hotelName} | Hotel in {destination}" }, { hotelName: data.hotelData.name, destination: data.hotelData.address.city, } ) } if ( data.system.content_type_uid === "destination_city_page" || data.system.content_type_uid === "destination_country_page" ) { const { location, filter, filterType } = data if (location) { if (filter) { if (filterType === "facility") { return intl.formatMessage( { id: "Hotels with {filter} in {location}" }, { location, filter } ) } else if (filterType === "surroundings") { return intl.formatMessage( { id: "Hotels near {filter} in {location}" }, { location, filter } ) } } return intl.formatMessage({ id: "Hotels in {location}" }, { location }) } } if (data.web?.breadcrumbs?.title) { return data.web.breadcrumbs.title } if (data.heading) { return data.heading } if (data.header?.heading) { return data.header.heading } return "" } export function getDescription(data: RawMetadataSchema) { const metadata = data.web?.seo_metadata if (metadata?.description) { return metadata.description } if (data.hotelData) { if (data.subpageUrl) { let subpageDescription: string | undefined const restaurantSubPage = data.hotelRestaurants?.find( (restaurant) => restaurant.nameInUrl === data.subpageUrl ) if (restaurantSubPage?.elevatorPitch) { subpageDescription = restaurantSubPage.elevatorPitch } switch (data.subpageUrl) { case data.additionalHotelData?.hotelParking.nameInUrl: subpageDescription = data.additionalHotelData?.hotelParking.elevatorPitch break case data.additionalHotelData?.healthAndFitness.nameInUrl: subpageDescription = data.additionalHotelData?.healthAndFitness.elevatorPitch break case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl: subpageDescription = data.additionalHotelData?.hotelSpecialNeeds.elevatorPitch break case data.additionalHotelData?.meetingRooms.nameInUrl: subpageDescription = data.additionalHotelData?.meetingRooms.elevatorPitch break default: break } if (subpageDescription) { return truncateTextAfterLastPeriod(subpageDescription) } } return data.hotelData.hotelContent.texts.descriptions?.short } if (data.preamble) { return truncateTextAfterLastPeriod(data.preamble) } if (data.header?.preamble) { return truncateTextAfterLastPeriod(data.header.preamble) } if (data.blocks?.length) { const jsonData = data.blocks[0].content?.content?.json // Finding the first paragraph with text const firstParagraph = jsonData?.children?.find( (child) => child.type === RTETypeEnum.p && child.children[0].text ) if (firstParagraph?.children?.length) { return firstParagraph.children[0].text ? truncateTextAfterLastPeriod(firstParagraph.children[0].text) : "" } } return "" } export function getImage(data: RawMetadataSchema) { const metadataImage = data.web?.seo_metadata?.seo_image const heroImage = data.hero_image || data.header?.hero_image // Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15) if (metadataImage) { return { url: metadataImage.url, alt: metadataImage.meta.alt || undefined, width: metadataImage.dimensions.width, height: metadataImage.dimensions.height, } } if (data.hotelData) { if (data.subpageUrl) { let subpageImage: { url: string; alt: string } | undefined const restaurantSubPage = data.hotelRestaurants?.find( (restaurant) => restaurant.nameInUrl === data.subpageUrl ) const restaurantImage = restaurantSubPage?.content?.images?.[0] if (restaurantImage) { subpageImage = { url: restaurantImage.imageSizes.small, alt: restaurantImage.metaData.altText || restaurantImage.metaData.altText_En || "", } } switch (data.subpageUrl) { case data.additionalHotelData?.hotelParking.nameInUrl: const parkingImage = data.additionalHotelData?.parkingImages?.heroImages[0] if (parkingImage) { subpageImage = { url: parkingImage.imageSizes.small, alt: parkingImage.metaData.altText || parkingImage.metaData.altText_En || "", } } break case data.additionalHotelData?.healthAndFitness.nameInUrl: const wellnessImage = data.hotelData.healthFacilities.find( (fac) => fac.content.images.length )?.content.images[0] if (wellnessImage) { subpageImage = { url: wellnessImage.imageSizes.small, alt: wellnessImage.metaData.altText || wellnessImage.metaData.altText_En || "", } } break case data.additionalHotelData?.hotelSpecialNeeds.nameInUrl: const accessibilityImage = data.additionalHotelData?.accessibility?.heroImages[0] if (accessibilityImage) { subpageImage = { url: accessibilityImage.imageSizes.small, alt: accessibilityImage.metaData.altText || accessibilityImage.metaData.altText_En || "", } } break case data.additionalHotelData?.meetingRooms.nameInUrl: const meetingImage = data.additionalHotelData?.conferencesAndMeetings?.heroImages[0] if (meetingImage) { subpageImage = { url: meetingImage.imageSizes.small, alt: meetingImage.metaData.altText || meetingImage.metaData.altText_En || "", } } break default: break } if (subpageImage) { return subpageImage } } const hotelImage = data.additionalHotelData?.gallery?.heroImages?.[0] || data.additionalHotelData?.gallery?.smallerImages?.[0] if (hotelImage) { return { url: hotelImage.imageSizes.small, alt: hotelImage.metaData.altText || undefined, } } } if (heroImage) { return { url: heroImage.url, alt: heroImage.meta.alt || undefined, width: heroImage.dimensions.width, height: heroImage.dimensions.height, } } return undefined } export async function getCityData( data: RawMetadataSchema, input: MetadataInputSchema, serviceToken: string, lang: Lang ) { const destinationSettings = data.destination_settings const filter = input.filterFromUrl if (destinationSettings) { const { city_sweden, city_norway, city_denmark, city_finland, city_germany, city_poland, } = destinationSettings const cities = [ city_denmark, city_finland, city_germany, city_poland, city_norway, city_sweden, ].filter((city): city is string => Boolean(city)) const cityIdentifier = cities[0] if (cityIdentifier) { const cityData = await getCityByCityIdentifier({ cityIdentifier, serviceToken, lang, }) const hotelIds = await getHotelIdsByCityIdentifier( cityIdentifier, serviceToken ) const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken }) let filterType if (filter) { const allFilters = getFiltersFromHotels(hotels) const facilityFilter = allFilters.facilityFilters.find( (f) => f.slug === filter ) const surroudingsFilter = allFilters.surroundingsFilters.find( (f) => f.slug === filter ) if (facilityFilter) { filterType = "facility" } else if (surroudingsFilter) { filterType = "surroundings" } } return { location: cityData?.name, filter, filterType } } } return null } export async function getCountryData( data: RawMetadataSchema, input: MetadataInputSchema, serviceToken: string, lang: Lang ) { const country = data.destination_settings?.country const filter = input.filterFromUrl if (country) { const translatedCountry = ApiCountry[lang][country] let filterType const hotelIds = await getHotelIdsByCountry({ country, serviceToken, }) const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken }) if (filter) { const allFilters = getFiltersFromHotels(hotels) const facilityFilter = allFilters.facilityFilters.find( (f) => f.slug === filter ) const surroudingsFilter = allFilters.surroundingsFilters.find( (f) => f.slug === filter ) if (facilityFilter) { filterType = "facility" } else if (surroudingsFilter) { filterType = "surroundings" } } return { location: translatedCountry, filter, filterType } } return null }