Merged in feat/SW-1454-hotel-listing-city-page (pull request #1250)
feat(SW-1454): added hotel listing * feat(SW-1454): added hotel listing Approved-by: Fredrik Thorsson
This commit is contained in:
@@ -19,7 +19,9 @@ export default async function CityListing({ cities }: CityListingProps) {
|
|||||||
<div className={styles.listHeader}>
|
<div className={styles.listHeader}>
|
||||||
<Subtitle type="two">
|
<Subtitle type="two">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: `{count} Locations` },
|
{
|
||||||
|
id: `{count, plural, one {{count} Location} other {{count} Locations}}`,
|
||||||
|
},
|
||||||
{ count: cities.length }
|
{ count: cities.length }
|
||||||
)}
|
)}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
.mainSection {
|
.mainSection {
|
||||||
grid-area: mainSection;
|
grid-area: mainSection;
|
||||||
padding-bottom: var(--Spacing-x7);
|
padding-bottom: var(--Spacing-x7);
|
||||||
min-height: 500px; /* This is a temporary value because of no content atm */
|
max-width: var(--max-width-page);
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/Bread
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
|
|
||||||
import ExperienceList from "../ExperienceList"
|
import ExperienceList from "../ExperienceList"
|
||||||
|
import HotelListing from "../HotelListing"
|
||||||
import SidebarContentWrapper from "../SidebarContentWrapper"
|
import SidebarContentWrapper from "../SidebarContentWrapper"
|
||||||
import DestinationPageSidePeek from "../Sidepeek"
|
import DestinationPageSidePeek from "../Sidepeek"
|
||||||
import StaticMap from "../StaticMap"
|
import StaticMap from "../StaticMap"
|
||||||
@@ -20,16 +20,13 @@ import styles from "./destinationCityPage.module.css"
|
|||||||
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
import { PageContentTypeEnum } from "@/types/requests/contentType"
|
||||||
|
|
||||||
export default async function DestinationCityPage() {
|
export default async function DestinationCityPage() {
|
||||||
const [intl, pageData] = await Promise.all([
|
const pageData = await getDestinationCityPage()
|
||||||
getIntl(),
|
|
||||||
getDestinationCityPage(),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!pageData) {
|
if (!pageData) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tracking, destinationCityPage } = pageData
|
const { tracking, destinationCityPage, hotels } = pageData
|
||||||
const {
|
const {
|
||||||
images,
|
images,
|
||||||
heading,
|
heading,
|
||||||
@@ -55,8 +52,7 @@ export default async function DestinationCityPage() {
|
|||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<main className={styles.mainSection}>
|
<main className={styles.mainSection}>
|
||||||
{/* TODO: Add hotel listing by cityIdentifier */}
|
<HotelListing hotels={hotels} />
|
||||||
{">>>> MAIN CONTENT <<<<"}
|
|
||||||
</main>
|
</main>
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<SidebarContentWrapper>
|
<SidebarContentWrapper>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
.container {
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||||
|
align-content: start;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amenityList {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: var(--UI-Text-Medium-contrast);
|
||||||
|
font-family: var(--typography-Body-Regular-fontFamily);
|
||||||
|
font-size: var(--typography-Caption-Underline-fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amenityItem {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrapper {
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(250px, 350px) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctaWrapper {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import Link from "next/link"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
|
import HotelLogo from "@/components/Icons/Logos"
|
||||||
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
|
import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import styles from "./hotelListingItem.module.css"
|
||||||
|
|
||||||
|
import type { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface HotelListingItemProps {
|
||||||
|
hotel: Hotel
|
||||||
|
url: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HotelListingItem({
|
||||||
|
hotel,
|
||||||
|
url,
|
||||||
|
}: HotelListingItemProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||||
|
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={styles.container}>
|
||||||
|
<ImageGallery
|
||||||
|
images={galleryImages}
|
||||||
|
title={intl.formatMessage(
|
||||||
|
{ id: "{title} - Image gallery" },
|
||||||
|
{ title: hotel.name }
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.intro}>
|
||||||
|
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||||
|
<Subtitle type="one" asChild>
|
||||||
|
<h3>{hotel.name}</h3>
|
||||||
|
</Subtitle>
|
||||||
|
<div className={styles.captions}>
|
||||||
|
<Caption color="uiTextPlaceholder">
|
||||||
|
{hotel.address.streetAddress}
|
||||||
|
</Caption>
|
||||||
|
<Divider variant="vertical" color="beige" />
|
||||||
|
<Caption color="uiTextPlaceholder">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{number} km to city center" },
|
||||||
|
{
|
||||||
|
number: getSingleDecimal(
|
||||||
|
hotel.location.distanceToCentre / 1000
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Body>{hotel.hotelContent.texts.descriptions.short}</Body>
|
||||||
|
<ul className={styles.amenityList}>
|
||||||
|
{amenities.map((amenity) => {
|
||||||
|
const IconComponent = mapFacilityToIcon(amenity.id)
|
||||||
|
return (
|
||||||
|
<li className={styles.amenityItem} key={amenity.id}>
|
||||||
|
{IconComponent && (
|
||||||
|
<IconComponent color="grey80" width={20} height={20} />
|
||||||
|
)}
|
||||||
|
{amenity.name}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<Button intent="text" variant="icon" theme="base">
|
||||||
|
{intl.formatMessage({ id: "See on map" })}
|
||||||
|
<ChevronRightSmallIcon />
|
||||||
|
</Button>
|
||||||
|
{url && (
|
||||||
|
<>
|
||||||
|
<Divider variant="horizontal" color="primaryLightSubtle" />
|
||||||
|
|
||||||
|
<div className={styles.ctaWrapper}>
|
||||||
|
<Button intent="tertiary" theme="base" size="small" asChild>
|
||||||
|
<Link href={url}>
|
||||||
|
{intl.formatMessage({ id: "See hotel details" })}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.container {
|
||||||
|
--scroll-margin-top: calc(
|
||||||
|
var(--booking-widget-mobile-height) + var(--Spacing-x2)
|
||||||
|
);
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
scroll-margin-top: var(--scroll-margin-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelList {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotelList:not(.allVisible) li:nth-child(n + 6) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
--scroll-margin-top: calc(
|
||||||
|
var(--booking-widget-desktop-height) + var(--Spacing-x2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useRef, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||||
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
import { useScrollToTop } from "@/hooks/useScrollToTop"
|
||||||
|
|
||||||
|
import HotelListingItem from "./HotelListingItem"
|
||||||
|
|
||||||
|
import styles from "./hotelListing.module.css"
|
||||||
|
|
||||||
|
import type { HotelData } from "@/types/hotel"
|
||||||
|
|
||||||
|
interface HotelListingProps {
|
||||||
|
hotels: (HotelData & { url: string | null })[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HotelListing({ hotels }: HotelListingProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
const showToggleButton = hotels.length > 5
|
||||||
|
const [allHotelsVisible, setAllHotelsVisible] = useState(!showToggleButton)
|
||||||
|
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||||
|
threshold: 300,
|
||||||
|
elementRef: scrollRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleShowMore() {
|
||||||
|
if (scrollRef.current && allHotelsVisible) {
|
||||||
|
scrollRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
setAllHotelsVisible((state) => !state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.container} ref={scrollRef}>
|
||||||
|
<div className={styles.listHeader}>
|
||||||
|
<Subtitle type="two">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: `{count, plural, one {{count} Hotel} other {{count} Hotels}}`,
|
||||||
|
},
|
||||||
|
{ count: hotels.length }
|
||||||
|
)}
|
||||||
|
</Subtitle>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className={`${styles.hotelList} ${allHotelsVisible ? styles.allVisible : ""}`}
|
||||||
|
>
|
||||||
|
{hotels.map(({ hotel, url }) => (
|
||||||
|
<li key={hotel.name}>
|
||||||
|
<HotelListingItem hotel={hotel} url={url} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{showToggleButton ? (
|
||||||
|
<ShowMoreButton
|
||||||
|
loadMoreData={handleShowMore}
|
||||||
|
showLess={allHotelsVisible}
|
||||||
|
textShowMore={intl.formatMessage({
|
||||||
|
id: "Show more",
|
||||||
|
})}
|
||||||
|
textShowLess={intl.formatMessage({
|
||||||
|
id: "Show less",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{showBackToTop && (
|
||||||
|
<BackToTopButton position="center" onClick={scrollToTop} />
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ export default function SelectHotelContent({
|
|||||||
const { showBackToTop, scrollToTop } = useScrollToTop({
|
const { showBackToTop, scrollToTop } = useScrollToTop({
|
||||||
threshold: 490,
|
threshold: 490,
|
||||||
elementRef: listingContainerRef,
|
elementRef: listingContainerRef,
|
||||||
|
refScrollable: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const coordinates = useMemo(
|
const coordinates = useMemo(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.triggerArea {
|
.triggerArea {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
|
|
||||||
.image {
|
.image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,11 @@
|
|||||||
right: 32px;
|
right: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.backToTopButtonText {
|
.backToTopButtonText {
|
||||||
display: initial;
|
display: initial;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function BackToTopButton({
|
|||||||
position,
|
position,
|
||||||
}: {
|
}: {
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
position: "left" | "right"
|
position: "left" | "right" | "center"
|
||||||
}) {
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const backToTopButtonVariants = cva(styles.backToTopButton, {
|
|||||||
position: {
|
position: {
|
||||||
left: styles.left,
|
left: styles.left,
|
||||||
right: styles.right,
|
right: styles.right,
|
||||||
|
center: styles.center,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -3,29 +3,43 @@ import { type RefObject, useEffect, useState } from "react"
|
|||||||
interface UseScrollToTopProps {
|
interface UseScrollToTopProps {
|
||||||
threshold: number
|
threshold: number
|
||||||
elementRef?: RefObject<HTMLElement>
|
elementRef?: RefObject<HTMLElement>
|
||||||
|
refScrollable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useScrollToTop({ threshold, elementRef }: UseScrollToTopProps) {
|
export function useScrollToTop({
|
||||||
|
threshold,
|
||||||
|
elementRef,
|
||||||
|
refScrollable,
|
||||||
|
}: UseScrollToTopProps) {
|
||||||
const [showBackToTop, setShowBackToTop] = useState(false)
|
const [showBackToTop, setShowBackToTop] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = elementRef?.current ?? window
|
const element =
|
||||||
|
refScrollable && elementRef?.current ? elementRef?.current : window
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
const scrollTop = elementRef?.current
|
let position = window.scrollY
|
||||||
? elementRef.current.scrollTop
|
if (elementRef?.current) {
|
||||||
: window.scrollY
|
position = refScrollable
|
||||||
setShowBackToTop(scrollTop > threshold)
|
? elementRef.current.scrollTop
|
||||||
|
: elementRef.current.getBoundingClientRect().top * -1
|
||||||
|
}
|
||||||
|
setShowBackToTop(position > threshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
element.addEventListener("scroll", handleScroll, { passive: true })
|
element.addEventListener("scroll", handleScroll, { passive: true })
|
||||||
return () => element.removeEventListener("scroll", handleScroll)
|
return () => element.removeEventListener("scroll", handleScroll)
|
||||||
}, [threshold, elementRef])
|
}, [threshold, elementRef, refScrollable])
|
||||||
|
|
||||||
function scrollToTop() {
|
function scrollToTop() {
|
||||||
if (elementRef?.current)
|
if (elementRef?.current) {
|
||||||
elementRef.current.scrollTo({ top: 0, behavior: "smooth" })
|
if (refScrollable) {
|
||||||
else window.scrollTo({ top: 0, behavior: "smooth" })
|
elementRef.current.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}
|
||||||
|
window.scrollTo({ top: elementRef.current.offsetTop, behavior: "smooth" })
|
||||||
|
} else {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { showBackToTop, scrollToTop }
|
return { showBackToTop, scrollToTop }
|
||||||
|
|||||||
@@ -565,7 +565,8 @@
|
|||||||
"{card} ending with {cardno}": "{card} slutter med {cardno}",
|
"{card} ending with {cardno}": "{card} slutter med {cardno}",
|
||||||
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
|
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
|
||||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
|
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
|
||||||
"{count} Locations": "{count} steder",
|
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotel} other {# hoteller}}",
|
||||||
|
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# sted} other {# steder}}",
|
||||||
"{count} lowercase letter": "{count} lille bogstav",
|
"{count} lowercase letter": "{count} lille bogstav",
|
||||||
"{count} number": "{count} nummer",
|
"{count} number": "{count} nummer",
|
||||||
"{count} special character": "{count} speciel karakter",
|
"{count} special character": "{count} speciel karakter",
|
||||||
|
|||||||
@@ -563,7 +563,8 @@
|
|||||||
"{card} ending with {cardno}": "{card} endet mit {cardno}",
|
"{card} ending with {cardno}": "{card} endet mit {cardno}",
|
||||||
"{checkInDate} from {checkInTime}": "{checkInDate} aus {checkInTime}",
|
"{checkInDate} from {checkInTime}": "{checkInDate} aus {checkInTime}",
|
||||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} aus {checkOutTime}",
|
"{checkOutDate} from {checkOutTime}": "{checkOutDate} aus {checkOutTime}",
|
||||||
"{count} Locations": "{count} Standorte",
|
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {{count} Hotel} other {{count} Hotels}}}",
|
||||||
|
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# Standort} other {# Standorte}}",
|
||||||
"{count} lowercase letter": "{count} Kleinbuchstabe",
|
"{count} lowercase letter": "{count} Kleinbuchstabe",
|
||||||
"{count} number": "{count} nummer",
|
"{count} number": "{count} nummer",
|
||||||
"{count} special character": "{count} sonderzeichen",
|
"{count} special character": "{count} sonderzeichen",
|
||||||
|
|||||||
@@ -615,7 +615,8 @@
|
|||||||
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
||||||
"{checkInDate} from {checkInTime}": "{checkInDate} from {checkInTime}",
|
"{checkInDate} from {checkInTime}": "{checkInDate} from {checkInTime}",
|
||||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} from {checkOutTime}",
|
"{checkOutDate} from {checkOutTime}": "{checkOutDate} from {checkOutTime}",
|
||||||
"{count} Locations": "{count} Locations",
|
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {{count} Hotel} other {{count} Hotels}}",
|
||||||
|
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {{count} Location} other {{count} Locations}}",
|
||||||
"{count} lowercase letter": "{count} lowercase letter",
|
"{count} lowercase letter": "{count} lowercase letter",
|
||||||
"{count} number": "{count} number",
|
"{count} number": "{count} number",
|
||||||
"{count} special character": "{count} special character",
|
"{count} special character": "{count} special character",
|
||||||
|
|||||||
@@ -563,7 +563,8 @@
|
|||||||
"{card} ending with {cardno}": "{card} päättyen {cardno}",
|
"{card} ending with {cardno}": "{card} päättyen {cardno}",
|
||||||
"{checkInDate} from {checkInTime}": "{checkInDate} alkaen {checkInTime}",
|
"{checkInDate} from {checkInTime}": "{checkInDate} alkaen {checkInTime}",
|
||||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}",
|
"{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}",
|
||||||
"{count} Locations": "{count} sijaintia",
|
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotelli} other {# hotellit}}",
|
||||||
|
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# sijainti} other {# sijainnit}}",
|
||||||
"{count} lowercase letter": "{count} pien kirjain",
|
"{count} lowercase letter": "{count} pien kirjain",
|
||||||
"{count} number": "{count} määrä",
|
"{count} number": "{count} määrä",
|
||||||
"{count} special character": "{count} erikoishahmo",
|
"{count} special character": "{count} erikoishahmo",
|
||||||
|
|||||||
@@ -564,7 +564,8 @@
|
|||||||
"{card} ending with {cardno}": "{card} slutter med {cardno}",
|
"{card} ending with {cardno}": "{card} slutter med {cardno}",
|
||||||
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
|
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
|
||||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
|
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
|
||||||
"{count} Locations": "{count} steder",
|
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotell} other {# hoteller}}",
|
||||||
|
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# sted} other {# steder}}",
|
||||||
"{count} lowercase letter": "{count} liten bokstav",
|
"{count} lowercase letter": "{count} liten bokstav",
|
||||||
"{count} number": "{count} antall",
|
"{count} number": "{count} antall",
|
||||||
"{count} special character": "{count} spesiell karakter",
|
"{count} special character": "{count} spesiell karakter",
|
||||||
|
|||||||
@@ -566,7 +566,8 @@
|
|||||||
"{card} ending with {cardno}": "{card} som slutar på {cardno}",
|
"{card} ending with {cardno}": "{card} som slutar på {cardno}",
|
||||||
"{checkInDate} from {checkInTime}": "{checkInDate} från {checkInTime}",
|
"{checkInDate} from {checkInTime}": "{checkInDate} från {checkInTime}",
|
||||||
"{checkOutDate} from {checkOutTime}": "{checkOutDate} från {checkOutTime}",
|
"{checkOutDate} from {checkOutTime}": "{checkOutDate} från {checkOutTime}",
|
||||||
"{count} Locations": "{count} platser",
|
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {# hotell} other {# hotell}}",
|
||||||
|
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {# plats} other {# platser}}",
|
||||||
"{count} lowercase letter": "{count} liten bokstav",
|
"{count} lowercase letter": "{count} liten bokstav",
|
||||||
"{count} number": "{count} nummer",
|
"{count} number": "{count} nummer",
|
||||||
"{count} special character": "{count} speciell karaktär",
|
"{count} special character": "{count} speciell karaktär",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from "@/lib/graphql/Query/DestinationCityPage/DestinationCityPage.graphql"
|
} from "@/lib/graphql/Query/DestinationCityPage/DestinationCityPage.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
|
||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
getDestinationCityPageRefsSuccessCounter,
|
getDestinationCityPageRefsSuccessCounter,
|
||||||
getDestinationCityPageSuccessCounter,
|
getDestinationCityPageSuccessCounter,
|
||||||
} from "./telemetry"
|
} from "./telemetry"
|
||||||
import { generatePageTags } from "./utils"
|
import { generatePageTags, getHotelListData } from "./utils"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GetDestinationCityPageData,
|
GetDestinationCityPageData,
|
||||||
@@ -28,8 +28,8 @@ import type {
|
|||||||
} from "@/types/trpc/routers/contentstack/destinationCityPage"
|
} from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||||
|
|
||||||
export const destinationCityPageQueryRouter = router({
|
export const destinationCityPageQueryRouter = router({
|
||||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
|
||||||
const { lang, uid } = ctx
|
const { lang, uid, serviceToken } = ctx
|
||||||
|
|
||||||
getDestinationCityPageRefsCounter.add(1, { lang, uid })
|
getDestinationCityPageRefsCounter.add(1, { lang, uid })
|
||||||
console.info(
|
console.info(
|
||||||
@@ -128,27 +128,31 @@ export const destinationCityPageQueryRouter = router({
|
|||||||
throw notFoundError
|
throw notFoundError
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedDestinationCityPage = destinationCityPageSchema.safeParse(
|
const validatedResponse = destinationCityPageSchema.safeParse(response.data)
|
||||||
response.data
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!validatedDestinationCityPage.success) {
|
if (!validatedResponse.success) {
|
||||||
getDestinationCityPageFailCounter.add(1, {
|
getDestinationCityPageFailCounter.add(1, {
|
||||||
lang,
|
lang,
|
||||||
uid: `${uid}`,
|
uid: `${uid}`,
|
||||||
error_type: "validation_error",
|
error_type: "validation_error",
|
||||||
error: JSON.stringify(validatedDestinationCityPage.error),
|
error: JSON.stringify(validatedResponse.error),
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"contentstack.destinationCityPage validation error",
|
"contentstack.destinationCityPage validation error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { lang, uid },
|
query: { lang, uid },
|
||||||
error: validatedDestinationCityPage.error,
|
error: validatedResponse.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hotels = await getHotelListData(
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
validatedResponse.data.destinationCityPage.destination_settings.city
|
||||||
|
)
|
||||||
|
|
||||||
getDestinationCityPageSuccessCounter.add(1, { lang, uid: `${uid}` })
|
getDestinationCityPageSuccessCounter.add(1, { lang, uid: `${uid}` })
|
||||||
console.info(
|
console.info(
|
||||||
"contentstack.destinationCityPage success",
|
"contentstack.destinationCityPage success",
|
||||||
@@ -157,6 +161,9 @@ export const destinationCityPageQueryRouter = router({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return validatedDestinationCityPage.data
|
return {
|
||||||
|
...validatedResponse.data,
|
||||||
|
hotels,
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||||
|
|
||||||
|
import { getHotel } from "../../hotels/query"
|
||||||
|
import { getHotelIdsByCityIdentifier } from "../../hotels/utils"
|
||||||
|
import { getHotelPageUrl } from "../hotelPage/utils"
|
||||||
|
|
||||||
|
import type { HotelData } from "@/types/hotel"
|
||||||
import type { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import type { GetDestinationCityPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
import type { GetDestinationCityPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
@@ -29,3 +34,29 @@ export function getConnections({
|
|||||||
|
|
||||||
return connections
|
return connections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getHotelListData(
|
||||||
|
lang: Lang,
|
||||||
|
serviceToken: string,
|
||||||
|
cityIdentifier: string
|
||||||
|
) {
|
||||||
|
const hotelIds = await getHotelIdsByCityIdentifier(
|
||||||
|
cityIdentifier,
|
||||||
|
serviceToken
|
||||||
|
)
|
||||||
|
|
||||||
|
const hotels = await Promise.all(
|
||||||
|
hotelIds.map(async (hotelId) => {
|
||||||
|
const [hotelData, url] = await Promise.all([
|
||||||
|
getHotel({ hotelId, language: lang }, serviceToken),
|
||||||
|
getHotelPageUrl(lang, hotelId),
|
||||||
|
])
|
||||||
|
|
||||||
|
return hotelData ? { ...hotelData, url } : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return hotels.filter(
|
||||||
|
(hotel): hotel is HotelData & { url: string | null } => !!hotel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { env } from "@/env/server"
|
|
||||||
import {
|
import {
|
||||||
GetDestinationCountryPage,
|
GetDestinationCountryPage,
|
||||||
GetDestinationCountryPageRefs,
|
GetDestinationCountryPageRefs,
|
||||||
@@ -10,7 +9,6 @@ import { toApiLang } from "@/server/utils"
|
|||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import { getCitiesByCountry } from "../../hotels/utils"
|
|
||||||
import {
|
import {
|
||||||
destinationCountryPageRefsSchema,
|
destinationCountryPageRefsSchema,
|
||||||
destinationCountryPageSchema,
|
destinationCountryPageSchema,
|
||||||
@@ -23,10 +21,9 @@ import {
|
|||||||
getDestinationCountryPageRefsSuccessCounter,
|
getDestinationCountryPageRefsSuccessCounter,
|
||||||
getDestinationCountryPageSuccessCounter,
|
getDestinationCountryPageSuccessCounter,
|
||||||
} from "./telemetry"
|
} from "./telemetry"
|
||||||
import { generatePageTags, getCityListDataByCityIdentifier } from "./utils"
|
import { generatePageTags, getCityPages } from "./utils"
|
||||||
|
|
||||||
import { ApiCountry } from "@/types/enums/country"
|
import { ApiCountry } from "@/types/enums/country"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
||||||
import type {
|
import type {
|
||||||
GetDestinationCountryPageData,
|
GetDestinationCountryPageData,
|
||||||
GetDestinationCountryPageRefsSchema,
|
GetDestinationCountryPageRefsSchema,
|
||||||
@@ -155,44 +152,10 @@ export const destinationCountryPageQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const cities = await getCityPages(
|
||||||
language: apiLang,
|
|
||||||
})
|
|
||||||
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 selectedCountry =
|
|
||||||
validatedResponse.data.destinationCountryPage.destination_settings.country
|
|
||||||
const apiCountry = ApiCountry[lang][selectedCountry]
|
|
||||||
const cities = await getCitiesByCountry(
|
|
||||||
[apiCountry],
|
|
||||||
options,
|
|
||||||
params,
|
|
||||||
lang,
|
lang,
|
||||||
true,
|
serviceToken,
|
||||||
"destinationCountryPage"
|
validatedResponse.data.destinationCountryPage.destination_settings.country
|
||||||
)
|
|
||||||
|
|
||||||
const cityPages = await Promise.all(
|
|
||||||
cities[apiCountry].map(async (city) => {
|
|
||||||
if (!city.cityIdentifier) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getCityListDataByCityIdentifier(
|
|
||||||
lang,
|
|
||||||
city.cityIdentifier
|
|
||||||
)
|
|
||||||
return data ? { ...data, cityName: city.name } : null
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
getDestinationCountryPageSuccessCounter.add(1, { lang, uid: `${uid}` })
|
getDestinationCountryPageSuccessCounter.add(1, { lang, uid: `${uid}` })
|
||||||
@@ -205,10 +168,12 @@ export const destinationCountryPageQueryRouter = router({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...validatedResponse.data,
|
...validatedResponse.data,
|
||||||
translatedCountry: apiCountry,
|
translatedCountry:
|
||||||
cities: cityPages
|
ApiCountry[lang][
|
||||||
.flat()
|
validatedResponse.data.destinationCountryPage.destination_settings
|
||||||
.filter((city): city is NonNullable<typeof city> => !!city),
|
.country
|
||||||
|
],
|
||||||
|
cities,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { env } from "@/env/server"
|
||||||
import { GetDestinationCityListData } from "@/lib/graphql/Query/DestinationCityPage/DestinationCityListData.graphql"
|
import { GetDestinationCityListData } from "@/lib/graphql/Query/DestinationCityPage/DestinationCityListData.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
|
import { toApiLang } from "@/server/utils"
|
||||||
|
|
||||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||||
|
|
||||||
|
import { getCitiesByCountry } from "../../hotels/utils"
|
||||||
import { destinationCityListDataSchema } from "../destinationCityPage/output"
|
import { destinationCityListDataSchema } from "../destinationCityPage/output"
|
||||||
import {
|
import {
|
||||||
getCityListDataCounter,
|
getCityListDataCounter,
|
||||||
@@ -10,6 +13,8 @@ import {
|
|||||||
getCityListDataSuccessCounter,
|
getCityListDataSuccessCounter,
|
||||||
} from "./telemetry"
|
} from "./telemetry"
|
||||||
|
|
||||||
|
import { ApiCountry, type Country } from "@/types/enums/country"
|
||||||
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||||
import type { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import type { GetDestinationCityListDataResponse } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
import type { GetDestinationCityListDataResponse } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||||
import type { GetDestinationCountryPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCountryPage"
|
import type { GetDestinationCountryPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCountryPage"
|
||||||
@@ -108,3 +113,47 @@ export async function getCityListDataByCityIdentifier(
|
|||||||
|
|
||||||
return validatedResponse.data
|
return validatedResponse.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCityPages(
|
||||||
|
lang: Lang,
|
||||||
|
serviceToken: string,
|
||||||
|
country: Country
|
||||||
|
) {
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
language: apiLang,
|
||||||
|
})
|
||||||
|
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 apiCountry = ApiCountry[lang][country]
|
||||||
|
const cities = await getCitiesByCountry([apiCountry], options, params, lang)
|
||||||
|
|
||||||
|
const publishedCities = cities[apiCountry].filter((city) => city.isPublished)
|
||||||
|
|
||||||
|
const cityPages = await Promise.all(
|
||||||
|
publishedCities.map(async (city) => {
|
||||||
|
if (!city.cityIdentifier) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getCityListDataByCityIdentifier(
|
||||||
|
lang,
|
||||||
|
city.cityIdentifier
|
||||||
|
)
|
||||||
|
return data ? { ...data, cityName: city.name } : null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return cityPages
|
||||||
|
.flat()
|
||||||
|
.filter((city): city is NonNullable<typeof city> => !!city)
|
||||||
|
}
|
||||||
|
|||||||
@@ -251,16 +251,23 @@ export const packagesSchema = z
|
|||||||
})
|
})
|
||||||
.transform(({ data }) => data?.attributes.packages)
|
.transform(({ data }) => data?.attributes.packages)
|
||||||
|
|
||||||
export const getHotelIdsByCityIdSchema = z
|
export const getHotelIdsSchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
// We only care about the hotel id
|
attributes: z.object({
|
||||||
|
isPublished: z.boolean(),
|
||||||
|
}),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.transform((data) => data.data.map((hotel) => hotel.id))
|
.transform(({ data }) => {
|
||||||
|
const filteredHotels = data.filter(
|
||||||
|
(hotel) => !!hotel.attributes.isPublished
|
||||||
|
)
|
||||||
|
return filteredHotels.map((hotel) => hotel.id)
|
||||||
|
})
|
||||||
|
|
||||||
export const getNearbyHotelIdsSchema = z
|
export const getNearbyHotelIdsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const getHotel = cache(
|
|||||||
})
|
})
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotels.hotelData start",
|
"api.hotels.hotelData start",
|
||||||
JSON.stringify({ query: { hotelId, params } })
|
JSON.stringify({ query: { hotelId, params: params.toString() } })
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
@@ -126,7 +126,7 @@ export const getHotel = cache(
|
|||||||
console.error(
|
console.error(
|
||||||
"api.hotels.hotelData error",
|
"api.hotels.hotelData error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { hotelId, params },
|
query: { hotelId, params: params.toString() },
|
||||||
error: {
|
error: {
|
||||||
status: apiResponse.status,
|
status: apiResponse.status,
|
||||||
statusText: apiResponse.statusText,
|
statusText: apiResponse.statusText,
|
||||||
@@ -151,7 +151,7 @@ export const getHotel = cache(
|
|||||||
console.error(
|
console.error(
|
||||||
"api.hotels.hotelData validation error",
|
"api.hotels.hotelData validation error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { hotelId, params },
|
query: { hotelId, params: params.toString() },
|
||||||
error: validateHotelData.error,
|
error: validateHotelData.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -165,7 +165,7 @@ export const getHotel = cache(
|
|||||||
console.info(
|
console.info(
|
||||||
"api.hotels.hotelData success",
|
"api.hotels.hotelData success",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { hotelId, params: params },
|
query: { hotelId, params: params.toString() },
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const hotelData = validateHotelData.data
|
const hotelData = validateHotelData.data
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const restaurantsOverviewPageSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const extraPageSchema = z.object({
|
export const extraPageSchema = z.object({
|
||||||
elevatorPitch: z.string().optional(),
|
elevatorPitch: z.string().default(""),
|
||||||
mainBody: z.string().optional(),
|
mainBody: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import deepmerge from "deepmerge"
|
import deepmerge from "deepmerge"
|
||||||
import { unstable_cache } from "next/cache"
|
import { unstable_cache } from "next/cache"
|
||||||
|
|
||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
import { env } from "@/env/server"
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
|
import { toApiLang } from "@/server/utils"
|
||||||
|
|
||||||
import { metrics } from "./metrics"
|
import { metrics } from "./metrics"
|
||||||
import {
|
import {
|
||||||
citiesByCountrySchema,
|
citiesByCountrySchema,
|
||||||
citiesSchema,
|
citiesSchema,
|
||||||
countriesSchema,
|
countriesSchema,
|
||||||
getHotelIdsByCityIdSchema,
|
getHotelIdsSchema,
|
||||||
locationsSchema,
|
locationsSchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
|
|
||||||
@@ -17,10 +20,9 @@ import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
|||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||||
import type {
|
import type {
|
||||||
CitiesGroupedByCountry,
|
CitiesGroupedByCountry,
|
||||||
Countries,
|
CityLocation,
|
||||||
HotelLocation,
|
HotelLocation,
|
||||||
} from "@/types/trpc/routers/hotel/locations"
|
} from "@/types/trpc/routers/hotel/locations"
|
||||||
import type { Lang } from "@/constants/languages"
|
|
||||||
import type { Endpoint } from "@/lib/api/endpoints"
|
import type { Endpoint } from "@/lib/api/endpoints"
|
||||||
|
|
||||||
export function getPoiGroupByCategoryName(category: string | undefined) {
|
export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||||
@@ -305,11 +307,11 @@ export async function getHotelIdsByCityId(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
|
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||||
if (!validatedHotelIds.success) {
|
if (!validatedHotelIds.success) {
|
||||||
metrics.hotelIds.fail.add(1, {
|
metrics.hotelIds.fail.add(1, {
|
||||||
params: params.toString(),
|
params: params.toString(),
|
||||||
@@ -323,19 +325,22 @@ export async function getHotelIdsByCityId(
|
|||||||
error: validatedHotelIds.error,
|
error: validatedHotelIds.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.hotelIds.success.add(1, { cityId })
|
metrics.hotelIds.success.add(1, { cityId })
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotel.hotel-ids success",
|
"api.hotel.hotel-ids success",
|
||||||
JSON.stringify({ params: params.toString() })
|
JSON.stringify({
|
||||||
|
params: params.toString(),
|
||||||
|
response: validatedHotelIds.data,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return validatedHotelIds.data
|
return validatedHotelIds.data
|
||||||
},
|
},
|
||||||
[`hotelsByCityId`, params.toString()],
|
[`hotelsByCityId`, params.toString()],
|
||||||
{ revalidate: TWENTYFOUR_HOURS }
|
{ revalidate: env.CACHE_TIME_HOTELS }
|
||||||
)(params)
|
)(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,11 +381,11 @@ export async function getHotelIdsByCountry(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
|
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||||
if (!validatedHotelIds.success) {
|
if (!validatedHotelIds.success) {
|
||||||
metrics.hotelIds.fail.add(1, {
|
metrics.hotelIds.fail.add(1, {
|
||||||
country,
|
country,
|
||||||
@@ -394,7 +399,7 @@ export async function getHotelIdsByCountry(
|
|||||||
error: validatedHotelIds.error,
|
error: validatedHotelIds.error,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
return null
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.hotelIds.success.add(1, { country })
|
metrics.hotelIds.success.add(1, { country })
|
||||||
@@ -406,6 +411,69 @@ export async function getHotelIdsByCountry(
|
|||||||
return validatedHotelIds.data
|
return validatedHotelIds.data
|
||||||
},
|
},
|
||||||
[`hotelsByCountry`, params.toString()],
|
[`hotelsByCountry`, params.toString()],
|
||||||
{ revalidate: TWENTYFOUR_HOURS }
|
{ revalidate: env.CACHE_TIME_HOTELS }
|
||||||
)(params)
|
)(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getHotelIdsByCityIdentifier(
|
||||||
|
cityIdentifier: string,
|
||||||
|
serviceToken: string
|
||||||
|
) {
|
||||||
|
const apiLang = toApiLang(Lang.en)
|
||||||
|
const cityId = await getCityIdByCityIdentifier(cityIdentifier, serviceToken)
|
||||||
|
|
||||||
|
if (!cityId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotelIdsParams = new URLSearchParams({
|
||||||
|
language: apiLang,
|
||||||
|
city: cityId,
|
||||||
|
onlyBasicInfo: "true",
|
||||||
|
})
|
||||||
|
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 hotelIds = await getHotelIdsByCityId(cityId, options, hotelIdsParams)
|
||||||
|
return hotelIds
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCityIdByCityIdentifier(
|
||||||
|
cityIdentifier: string,
|
||||||
|
serviceToken: string
|
||||||
|
) {
|
||||||
|
const lang = Lang.en
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
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 params = new URLSearchParams({
|
||||||
|
language: apiLang,
|
||||||
|
})
|
||||||
|
const locations = await getLocations(lang, options, params, null)
|
||||||
|
if (!locations || "error" in locations) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cityId = locations
|
||||||
|
.filter((loc): loc is CityLocation => loc.type === "cities")
|
||||||
|
.find((loc) => loc.cityIdentifier === cityIdentifier)?.id
|
||||||
|
|
||||||
|
return cityId ?? null
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user