Files
web/apps/scandic-web/utils/url.ts
Anton Gunnarsson cbf9e7b7c2 Merged in chore/next15 (pull request #1999)
chore (SW-834): Upgrade to Next 15

* wip: apply codemod and upgrade swc plugin

* wip: design-system to react 19, fix issues from async (search)params

* wip: fix remaining issues from codemod

serverClient is now async because context use headers()
getLang is now async because it uses headers()

* Minor cleanup

* Inline react-material-symbols package

Package is seemingly not maintained any more and doesn't support
React 19. This copies the package source into `design-system`,
makes the necessary changes for 19 and export it for others to use.

* Fix missing awaits

* Disable modal exit animations

Enabling modal exit animations via isExiting prop is causing
modals to be rendered in "hidden" state and never unmount.
Seems to be an issue with react-aria-components,
see https://github.com/adobe/react-spectrum/issues/7563.
Can probably be fixed by rewriting to a solution similar to
https://react-spectrum.adobe.com/react-aria/examples/framer-modal-sheet.html

* Remove unstable cache implementation and use in memory cache locally

* Fix ref type in SelectFilter

* Use cloneElement to add key prop to element


Approved-by: Linus Flood
2025-06-02 11:11:50 +00:00

218 lines
6.2 KiB
TypeScript

import { Lang } from "@/constants/languages"
import type { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type {
Child,
Room,
} from "@/types/components/hotelReservation/selectRate/selectRate"
export function removeMultipleSlashes(pathname: string) {
return pathname.replaceAll(/\/\/+/g, "/")
}
export function removeTrailingSlash(pathname: string) {
if (pathname.endsWith("/")) {
// Remove the trailing slash
return pathname.slice(0, -1)
}
return pathname
}
type PartialRoom = { rooms?: Partial<Room>[] }
const keyedSearchParams = new Map([
["room", "rooms"],
["ratecode", "rateCode"],
["counterratecode", "counterRateCode"],
["roomtype", "roomTypeCode"],
["fromdate", "fromDate"],
["todate", "toDate"],
["hotel", "hotelId"],
["child", "childrenInRoom"],
["searchtype", "searchType"],
])
export type SelectHotelParams<T> = Omit<T, "hotel"> & {
hotelId: string
} & PartialRoom
export function getKeyFromSearchParam(key: string): string {
return keyedSearchParams.get(key) || key
}
export function getSearchParamFromKey(key: string): string {
for (const [mapKey, mapValue] of keyedSearchParams.entries()) {
if (mapValue === key) {
return mapKey
}
}
return key
}
export function searchParamsToRecord(searchParams: URLSearchParams) {
return Object.fromEntries(searchParams.entries())
}
export function convertSearchParamsToObj<T extends PartialRoom>(
searchParams: Record<string, string>
): SelectHotelParams<T> {
const searchParamsObject = Object.entries(searchParams).reduce<
SelectHotelParams<T>
>((acc, [key, value]) => {
// The params are sometimes indexed with a number (for ex: `room[0].adults`),
// so we need to split them by . or []
const keys = key.replace(/\]/g, "").split(/\[|\./)
const firstKey = getKeyFromSearchParam(keys[0])
// Room is a special case since it is an array, so we need to handle it separately
if (firstKey === "rooms") {
// Rooms are always indexed with a number, so we need to extract the index
const index = Number(keys[1])
const roomObject =
acc.rooms && Array.isArray(acc.rooms) ? acc.rooms : (acc.rooms = [])
const roomObjectKey = getKeyFromSearchParam(keys[2]) as keyof Room
if (!roomObject[index]) {
roomObject[index] = {}
}
// Adults should be converted to a number
if (roomObjectKey === "adults") {
roomObject[index].adults = Number(value)
// Child is an array, so we need to handle it separately
} else if (roomObjectKey === "childrenInRoom") {
const childIndex = Number(keys[3])
const childKey = keys[4] as keyof Child
if (
!("childrenInRoom" in roomObject[index]) ||
!Array.isArray(roomObject[index].childrenInRoom)
) {
roomObject[index].childrenInRoom = []
}
roomObject[index].childrenInRoom![childIndex] = {
...roomObject[index].childrenInRoom![childIndex],
[childKey]: Number(value),
}
} else if (roomObjectKey === "packages") {
roomObject[index].packages = value.split(",") as RoomPackageCodeEnum[]
} else {
roomObject[index][roomObjectKey] = value
}
} else {
return { ...acc, [firstKey]: value }
}
return acc
}, {} as SelectHotelParams<T>)
return searchParamsObject
}
export function convertObjToSearchParams<T>(
bookingData: T & PartialRoom,
intitalSearchParams = {} as URLSearchParams
) {
const bookingSearchParams = new URLSearchParams(intitalSearchParams)
Object.entries(bookingData).forEach(([key, value]) => {
if (key === "rooms") {
value.forEach((item, index) => {
if (item?.adults) {
bookingSearchParams.set(
`room[${index}].adults`,
item.adults.toString()
)
}
if (item?.childrenInRoom) {
item.childrenInRoom.forEach((child, childIndex) => {
bookingSearchParams.set(
`room[${index}].child[${childIndex}].age`,
child.age.toString()
)
bookingSearchParams.set(
`room[${index}].child[${childIndex}].bed`,
child.bed.toString()
)
})
}
if (item?.roomTypeCode) {
bookingSearchParams.set(`room[${index}].roomtype`, item.roomTypeCode)
}
if (item?.rateCode) {
bookingSearchParams.set(`room[${index}].ratecode`, item.rateCode)
}
if (item?.counterRateCode) {
bookingSearchParams.set(
`room[${index}].counterratecode`,
item.counterRateCode
)
}
if (item.packages && item.packages.length > 0) {
bookingSearchParams.set(
`room[${index}].packages`,
item.packages.join(",")
)
}
})
} else {
bookingSearchParams.set(getSearchParamFromKey(key), value.toString())
}
})
return bookingSearchParams
}
/**
* Returns the TLD (top-level domain) for a given language.
* @param lang - The language to get the TLD for
* @returns The TLD for the given language
*/
export function getTldForLanguage(lang: Lang): string {
switch (lang) {
case Lang.sv:
return "se"
case Lang.no:
return "no"
case Lang.da:
return "dk"
case Lang.fi:
return "fi"
case Lang.de:
return "de"
default:
return "com"
}
}
/**
* Constructs a URL with the correct TLD (top-level domain) based on lang, for current web.
* @param params - Object containing path, lang, and baseUrl
* @param params.path - The path to append to the URL
* @param params.lang - The language to use for TLD
* @param params.baseUrl - The base URL to use (e.g. https://www.scandichotels.com)
* @returns The complete URL with language-specific TLD
*/
export function getCurrentWebUrl({
path,
lang,
baseUrl = "https://www.scandichotels.com", // Fallback for ephemeral environments (e.g. deploy previews).
}: {
path: string
lang: Lang
baseUrl?: string
}): string {
const tld = getTldForLanguage(lang)
const url = new URL(path, baseUrl)
if (tld !== "com") {
url.host = url.host.replace(".com", `.${tld}`)
}
return url.toString()
}