Feat/SW-1451 country page filtering and sorting * feat(SW-1451): implemented sorting and filtering on country pages * feat(SW-1451): Renamed hotel-data to destination-data because of its multi-purpose use * feat(SW-1451): Now filtering after change of url instead of inside the store after submit Approved-by: Fredrik Thorsson
309 lines
8.8 KiB
TypeScript
309 lines
8.8 KiB
TypeScript
import { ApiLang, type Lang } from "@/constants/languages"
|
|
import { env } from "@/env/server"
|
|
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 type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
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) {
|
|
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) {
|
|
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
|
|
const hotelImage =
|
|
data.hotelData?.gallery?.heroImages?.[0] ||
|
|
data.hotelData?.gallery?.smallerImages?.[0]
|
|
|
|
// 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 (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
|
|
)
|
|
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 options: RequestOptionsWithOutBody = {
|
|
// needs to clear default option as only
|
|
// cache or next.revalidate is permitted
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
const hotelIdsParams = new URLSearchParams({
|
|
language: ApiLang.En,
|
|
country,
|
|
})
|
|
const hotelIds = await getHotelIdsByCountry(
|
|
country,
|
|
options,
|
|
hotelIdsParams
|
|
)
|
|
|
|
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
|
|
}
|