Merge remote-tracking branch 'origin' into feature/tracking

This commit is contained in:
Linus Flood
2024-12-13 09:02:37 +01:00
329 changed files with 4494 additions and 1910 deletions

View File

@@ -61,3 +61,5 @@ ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false"
SHOW_SITE_WIDE_ALERT="false"
SHOW_SIGNUP_FLOW="true"
USE_NEW_REWARDS_ENDPOINT="true"
USE_NEW_REWARD_MODEL="true"

View File

@@ -7,6 +7,7 @@ CMS_API_KEY="test"
CMS_PREVIEW_TOKEN="test"
CMS_PREVIEW_URL="test"
CMS_URL="test"
CMS_BRANCH="development"
CURITY_CLIENT_ID_SERVICE="test"
CURITY_CLIENT_SECRET_SERVICE="test"
CURITY_CLIENT_ID_USER="test"
@@ -44,6 +45,7 @@ GOOGLE_DYNAMIC_MAP_ID="test"
HIDE_FOR_NEXT_RELEASE="true"
SALESFORCE_PREFERENCE_BASE_URL="test"
USE_NEW_REWARDS_ENDPOINT="true"
USE_NEW_REWARD_MODEL="true"
TZ=UTC
ENABLE_BOOKING_FLOW="false"

View File

@@ -1,6 +1,7 @@
{
"extends": ["next/core-web-vitals", "plugin:import/recommended"],
"plugins": ["simple-import-sort"],
"plugins": ["simple-import-sort", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"rules": {
"react/function-component-definition": "error",
"simple-import-sort/imports": [
@@ -52,6 +53,12 @@
"simple-import-sort/exports": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error"
"import/no-duplicates": [
"error",
{
"prefer-inline": true
}
],
"@typescript-eslint/consistent-type-imports": "error"
}
}

View File

@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
import { AuthError } from "next-auth"
import { Lang } from "@/constants/languages"

View File

@@ -4,7 +4,7 @@ import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default function AllBreadcrumbs({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -23,7 +23,7 @@
@media screen and (min-width: 1367px) {
.content {
gap: var(--Spacing-x5);
grid-template-columns: max(360px) 1fr;
grid-template-columns: max(340px) 1fr;
padding-left: var(--Spacing-x5);
padding-right: var(--Spacing-x5);
}

View File

@@ -6,7 +6,7 @@ import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function CommunicationSlot({
params,

View File

@@ -9,7 +9,7 @@ import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -9,7 +9,7 @@ import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function MembershipCardSlot({
params,

View File

@@ -3,7 +3,7 @@ import { getProfile } from "@/lib/trpc/memoizedRequests"
import Form from "@/components/Forms/Edit/Profile"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function EditProfileSlot({
params,

View File

@@ -20,7 +20,7 @@ import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function Profile({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -5,7 +5,7 @@ import { serverClient } from "@/lib/trpc/server"
import TrackingSDK from "@/components/TrackingSDK"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/generateMetadata"

View File

@@ -4,7 +4,7 @@ import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default function PageBreadcrumbs({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -2,7 +2,7 @@ import { setPreviewData } from "@/lib/previewContext"
import InitLivePreview from "@/components/LivePreview"
import { PageArgs, UIDParams } from "@/types/params"
import type { PageArgs, UIDParams } from "@/types/params"
export default function PreviewPage({
searchParams,

View File

@@ -1,6 +1,6 @@
import styles from "./layout.module.css"
import {
import type {
ContentTypeParams,
LangParams,
LayoutArgs,

View File

@@ -11,7 +11,7 @@ import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { setLang } from "@/i18n/serverContext"
import {
import type {
ContentTypeParams,
LangParams,
PageArgs,

View File

@@ -1,6 +1,6 @@
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
import type { LangParams, LayoutArgs } from "@/types/params"
export default function PaymentCallbackLayout({
children,

View File

@@ -12,7 +12,7 @@ import { serverClient } from "@/lib/trpc/server"
import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function PaymentCallbackPage({
params,

View File

@@ -1,6 +1,6 @@
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
import type { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,

View File

@@ -1,5 +0,0 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function LoadingModal() {
return <LoadingSpinner />
}

View File

@@ -1,3 +0,0 @@
export default function Default() {
return null
}

View File

@@ -3,3 +3,9 @@
background-color: var(--Base-Background-Primary-Normal);
position: relative;
}
@media screen and (min-width: 768px) {
.layout {
z-index: 0;
}
}

View File

@@ -1,17 +1,9 @@
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
import type { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
modal,
}: React.PropsWithChildren<
LayoutArgs<LangParams> & { modal: React.ReactNode }
>) {
return (
<div className={styles.layout}>
{children}
{modal}
</div>
)
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return <div className={styles.layout}>{children}</div>
}

View File

@@ -1 +1,63 @@
export { default } from "../@modal/(.)map/page"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { getLocations } from "@/lib/trpc/memoizedRequests"
import { SelectHotelMapContainer } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer"
import { SelectHotelMapContainerSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainerSkeleton"
import {
generateChildrenString,
getHotelReservationQueryParams,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { MapContainer } from "@/components/MapContainer"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage({
params,
searchParams,
}: PageArgs<LangParams, SelectHotelSearchParams>) {
setLang(params.lang)
const locations = await getLocations()
if (!locations || "error" in locations) {
return null
}
const city = locations.data.find(
(location) =>
location.name.toLowerCase() === searchParams.city.toLowerCase()
)
if (!city) return notFound()
const selectHotelParams = new URLSearchParams(searchParams)
const selectHotelParamsObject =
getHotelReservationQueryParams(selectHotelParams)
const adultsInRoom = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
const childrenInRoom = selectHotelParamsObject.room[0].child
? generateChildrenString(selectHotelParamsObject.room[0].child)
: undefined // TODO: Handle multiple rooms
const child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms
return (
<div className={styles.main}>
<MapContainer>
<Suspense
key={city.name}
fallback={<SelectHotelMapContainerSkeleton />}
>
<SelectHotelMapContainer
city={city}
searchParams={searchParams}
adultsInRoom={adultsInRoom}
childrenInRoom={childrenInRoom}
child={child}
/>
</Suspense>
</MapContainer>
</div>
)
}

View File

@@ -1,51 +1,18 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { Lang } from "@/constants/languages"
import {
selectHotel,
selectHotelMap,
} from "@/constants/routes/hotelReservation"
import { getLocations } from "@/lib/trpc/memoizedRequests"
import {
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter"
import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer"
import SelectHotel from "@/components/HotelReservation/SelectHotel"
import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton"
import {
generateChildrenString,
getHotelReservationQueryParams,
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Alert from "@/components/TempDesignSystem/Alert"
import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
import {
TrackingChannelEnum,
TrackingSDKHotelInfo,
TrackingSDKPageData,
} from "@/types/components/tracking"
import { AlertTypeEnum } from "@/types/enums/alert"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelPage({
params,
@@ -64,10 +31,6 @@ export default async function SelectHotelPage({
if (!city) return notFound()
const isCityWithCountry = (city: any): city is { country: string } =>
"country" in city
const intl = await getIntl()
const selectHotelParams = new URLSearchParams(searchParams)
const selectHotelParamsObject =
getHotelReservationQueryParams(selectHotelParams)
@@ -79,157 +42,30 @@ export default async function SelectHotelPage({
return notFound()
}
const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
const adultsParams = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms
const childrenParams = selectHotelParamsObject.room[0].child
? generateChildrenString(selectHotelParamsObject.room[0].child)
: undefined // TODO: Handle multiple rooms
const child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms
const children = child ? generateChildrenString(child) : undefined
const hotels = await fetchAvailableHotels({
cityId: city.id,
roomStayStartDate: searchParams.fromDate,
roomStayEndDate: searchParams.toDate,
adults,
children,
})
const arrivalDate = new Date(searchParams.fromDate)
const departureDate = new Date(searchParams.toDate)
const validHotels = hotels.filter(
(hotel): hotel is HotelData => hotel !== null
)
const filterList = getFiltersFromHotels(validHotels)
const breadcrumbs = [
{
title: intl.formatMessage({ id: "Home" }),
href: `/${params.lang}`,
uid: "home-page",
},
{
title: intl.formatMessage({ id: "Hotel reservation" }),
href: `/${params.lang}/hotelreservation`,
uid: "hotel-reservation",
},
{
title: intl.formatMessage({ id: "Select hotel" }),
href: `${selectHotel(params.lang)}/?${selectHotelParams}`,
uid: "select-hotel",
},
{
title: city.name,
uid: city.id,
},
]
const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined)
const pageTrackingData: TrackingSDKPageData = {
pageId: "select-hotel",
domainLanguage: params.lang as Lang,
channel: TrackingChannelEnum["hotelreservation"],
pageName: "hotelreservation|select-hotel",
siteSections: "hotelreservation|select-hotel",
pageType: "bookinghotelspage",
siteVersion: "new-web",
}
const hotelsTrackingData: TrackingSDKHotelInfo = {
availableResults: hotels.length,
searchTerm: searchParams.city,
arrivalDate: format(arrivalDate, "yyyy-MM-dd"),
departureDate: format(departureDate, "yyyy-MM-dd"),
noOfAdults: adults,
noOfChildren: child?.length,
ageOfChildren: child?.map((c) => c.age).join(","),
childBedPreference: child?.map((c) => ChildBedMapEnum[c.bed]).join("|"),
noOfRooms: 1, // // TODO: Handle multiple rooms
duration: differenceInCalendarDays(departureDate, arrivalDate),
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
searchType: "destination",
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
country: validHotels?.[0].hotelData.address.country,
region: validHotels?.[0].hotelData.address.city,
const reservationParams = {
selectHotelParams,
searchParams,
adultsParams,
childrenParams,
child,
}
return (
<>
<header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs breadcrumbs={breadcrumbs} />
</Suspense>
<div className={styles.title}>
<div className={styles.cityInformation}>
<Subtitle>{city.name}</Subtitle>
<HotelCount />
</div>
<div className={styles.sorter}>
<HotelSorter discreet />
</div>
</div>
<MobileMapButtonContainer filters={filterList} />
</header>
<main className={styles.main}>
<div className={styles.sideBar}>
{hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap(params.lang)}
keepSearchParams
>
<div className={styles.mapContainer}>
<StaticMap
city={searchParams.city}
country={isCityWithCountry(city) ? city.country : undefined}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${searchParams.city} city center`}
/>
<Button wrapping size="medium" intent="text" theme="base">
{intl.formatMessage({ id: "See map" })}
<ChevronRightIcon
color="baseButtonTextOnFillNormal"
width={20}
height={20}
/>
</Button>
</div>
</Link>
) : (
<div className={styles.mapContainer}>
<StaticMap
city={searchParams.city}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${searchParams.city} city center`}
/>
</div>
)}
<HotelFilter filters={filterList} className={styles.filter} />
</div>
<div className={styles.hotelList}>
{isAllUnavailable && (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No availability" })}
text={intl.formatMessage({
id: "There are no rooms available that match your request.",
})}
/>
)}
<HotelCardListing hotelData={validHotels} />
</div>
<Suspense fallback={null}>
<TrackingSDK
pageData={pageTrackingData}
hotelInfo={hotelsTrackingData}
/>
</Suspense>
</main>
</>
<Suspense
key={`${city.name}-${searchParams.fromDate}-${searchParams.toDate}-${adultsParams}-${childrenParams}`}
fallback={<SelectHotelSkeleton />}
>
<SelectHotel
city={city}
params={params}
reservationParams={reservationParams}
/>
</Suspense>
)
}

View File

@@ -1,7 +1,7 @@
import { Dayjs } from "dayjs"
import { dt } from "@/lib/dt"
import type { Dayjs } from "dayjs"
/**
* Get valid dates from stringFromDate and stringToDate making sure that they are not in the past and chronologically correct
* @example const { fromDate, toDate} = getValidDates("2021-01-01", "2021-01-02")

View File

@@ -17,12 +17,10 @@
@media screen and (min-width: 1367px) {
.container {
width: var(--max-width-page);
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
width: var(--max-width-page);
margin: var(--Spacing-x5) auto 0;
/* simulates padding on viewport smaller than --max-width-navigation */
}
.content {

View File

@@ -35,12 +35,11 @@ import EnterDetailsTracking from "./enterDetailsTracking"
import styles from "./page.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import {
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type {
TrackingChannelEnum,
TrackingSDKHotelInfo,
TrackingSDKPageData,
} from "@/types/components/tracking"
TrackingSDKPageData} from "@/types/components/tracking";
import { StepEnum } from "@/types/enums/step"
import type { LangParams, PageArgs } from "@/types/params"

View File

@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
import { AuthError } from "next-auth"
import { Lang } from "@/constants/languages"
@@ -117,7 +117,7 @@ export async function GET(
/** Record<string, any> is next-auth typings */
const params: Record<string, any> = {
ui_locales: context.params.lang,
scope: ["openid", "profile"],
scope: ["openid", "profile", "booking"],
/**
* The `acr_values` param is used to make Curity display the proper login
* page for Scandic. Without the parameter Curity presents some choices

View File

@@ -1,14 +1,13 @@
import { NextRequest, NextResponse } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
import { AuthError } from "next-auth"
import { Lang } from "@/constants/languages"
import { login } from "@/constants/routes/handleAuth"
import { env } from "@/env/server"
import { badRequest, internalServerError } from "@/server/errors/next"
import { getPublicURL } from "@/server/utils"
import { signIn } from "@/auth"
import type { Lang } from "@/constants/languages"
export async function GET(
request: NextRequest,
context: { params: { lang: Lang } }

View File

@@ -2,7 +2,7 @@ import { env } from "@/env/server"
import BookingWidget, { preload } from "@/components/BookingWidget"
import { PageArgs } from "@/types/params"
import type { PageArgs } from "@/types/params"
export default async function BookingWidgetPage({
searchParams,

View File

@@ -3,7 +3,7 @@ import { serverClient } from "@/lib/trpc/server"
import BookingWidget, { preload } from "@/components/BookingWidget"
import { PageArgs } from "@/types/params"
import type { PageArgs } from "@/types/params"
export default async function BookingWidgetPage({
searchParams,

View File

@@ -4,7 +4,7 @@ import CurrentFooter from "@/components/Current/Footer"
import Footer, { preload } from "@/components/Footer"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default function FooterSlot({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -7,7 +7,7 @@ import HeaderFallback from "@/components/Current/Header/HeaderFallback"
import Header from "@/components/Header"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default function HeaderPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -16,7 +16,7 @@ import { findLang } from "@/utils/languages"
import styles from "./error.module.css"
import { LangParams } from "@/types/params"
import type { LangParams } from "@/types/params"
export default function Error({
error,

View File

@@ -3,7 +3,6 @@ import "@scandic-hotels/design-system/style.css"
import Script from "next/script"
import { env } from "@/env/server"
import TrpcProvider from "@/lib/trpc/Provider"
import TokenRefresher from "@/components/Auth/TokenRefresher"

View File

@@ -1,7 +1,7 @@
import NotFound from "@/components/Current/NotFound"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default function NotFoundPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -2,7 +2,7 @@ import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, LayoutArgs, StatusParams } from "@/types/params"
import type { LangParams, LayoutArgs, StatusParams } from "@/types/params"
export default function MiddlewareError({
params,

View File

@@ -1,7 +1,7 @@
import Header from "@/components/Current/Header"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HeaderPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -7,7 +7,7 @@ import AccountPage from "@/components/Webviews/AccountPage"
import LoyaltyPage from "@/components/Webviews/LoyaltyPage"
import { setLang } from "@/i18n/serverContext"
import {
import type {
ContentTypeWebviewParams,
LangParams,
PageArgs,

View File

@@ -3,7 +3,7 @@ import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
import type { LangParams, PageArgs } from "@/types/params"
export default function Refresh({ params }: PageArgs<LangParams>) {
setLang(params.lang)

View File

@@ -1,4 +1,3 @@
import { NextRequest } from "next/server"
import { env } from "process"
import { Lang } from "@/constants/languages"
@@ -6,6 +5,8 @@ import { profile } from "@/constants/routes/myPages"
import { serverClient } from "@/lib/trpc/server"
import { getPublicURL } from "@/server/utils"
import type { NextRequest } from "next/server"
export async function GET(
request: NextRequest,
{ params }: { params: { lang: string } }

View File

@@ -1,6 +1,4 @@
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { type NextRequest, NextResponse } from "next/server"
export async function GET(request: NextRequest) {
return NextResponse.json(Object.fromEntries(request.headers.entries()))

View File

@@ -1,6 +1,5 @@
import { revalidateTag } from "next/cache"
import { headers } from "next/headers"
import { NextRequest } from "next/server"
import { z } from "zod"
import { Lang } from "@/constants/languages"
@@ -9,6 +8,8 @@ import { badRequest, internalServerError, notFound } from "@/server/errors/next"
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
import type { NextRequest } from "next/server"
enum LoyaltyConfigContentTypes {
loyalty_level = "loyalty_level",
reward = "reward",

View File

@@ -1,12 +1,13 @@
import { revalidateTag } from "next/cache"
import { headers } from "next/headers"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import { badRequest, internalServerError } from "@/server/errors/next"
import { generateTag } from "@/utils/generateTag"
import type { Lang } from "@/constants/languages"
// This file is primarily to be used locally to test
// purging your cache for new (and old) requests
export async function POST() {

View File

@@ -1,6 +1,5 @@
import { revalidateTag } from "next/cache"
import { headers } from "next/headers"
import { NextRequest } from "next/server"
import { z } from "zod"
import { Lang } from "@/constants/languages"
@@ -17,6 +16,8 @@ import {
generateTag,
} from "@/utils/generateTag"
import type { NextRequest } from "next/server"
const validateJsonBody = z.object({
data: z.object({
content_type: z.object({

View File

@@ -43,7 +43,7 @@ export default function CardsGrid({
return (
<Card
theme={
cards_grid.theme ?? (card.backgroundImage ? "image" : "one")
card.backgroundImage ? "image" : cards_grid.theme ?? "one"
}
key={card.system.uid}
scriptedTopTitle={card.scripted_top_title}

View File

@@ -12,7 +12,7 @@ import SectionWrapper from "../SectionWrapper"
import styles from "./loyaltyLevels.module.css"
import { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent"
import type { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent"
import type { LevelCardProps } from "@/types/components/overviewTable"
export default async function LoyaltyLevels({

View File

@@ -1,6 +1,6 @@
import { VariantProps } from "class-variance-authority"
import type { VariantProps } from "class-variance-authority"
import { heroVariants } from "./heroVariants"
import type { heroVariants } from "./heroVariants"
export interface HeroProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,

View File

@@ -1,6 +1,7 @@
import { HeroProps } from "./hero"
import { heroVariants } from "./heroVariants"
import type { HeroProps } from "./hero"
export default function Hero({ className, color, children }: HeroProps) {
const classNames = heroVariants({ className, color })
return <section className={classNames}>{children}</section>

View File

@@ -7,7 +7,7 @@ import { getMembership } from "@/utils/user"
import PointsContainer from "./Container"
import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn"
import { UserProps } from "@/types/components/myPages/user"
import type { UserProps } from "@/types/components/myPages/user"
export default async function Points({ user }: UserProps) {
const intl = await getIntl()

View File

@@ -4,7 +4,7 @@ import { useReducer } from "react"
import { useIntl } from "react-intl"
import {
MembershipLevel,
type MembershipLevel,
MembershipLevelEnum,
} from "@/constants/membershipLevels"
@@ -22,8 +22,8 @@ import styles from "./overviewTable.module.css"
import type { Key } from "react-aria-components"
import {
ComparisonLevel,
DesktopSelectColumns,
type ComparisonLevel,
type DesktopSelectColumns,
type MobileColumnHeaderProps,
OverviewTableActionsEnum,
type OverviewTableClientProps,

View File

@@ -4,7 +4,7 @@ import { serverClient } from "@/lib/trpc/server"
import SectionWrapper from "../SectionWrapper"
import OverviewTableClient from "./Client"
import { OverviewTableProps } from "@/types/components/blocks/dynamicContent"
import type { OverviewTableProps } from "@/types/components/blocks/dynamicContent"
export default async function OverviewTable({
dynamic_content,

View File

@@ -9,7 +9,7 @@ import {
type LevelWithRewards,
OverviewTableActionsEnum,
type OverviewTableClientProps,
OverviewTableReducerAction,
type OverviewTableReducerAction,
} from "@/types/components/overviewTable"
export function getLevel(

View File

@@ -6,11 +6,11 @@ import { useState } from "react"
import { trpc } from "@/lib/trpc/client"
import LoadingSpinner from "@/components/LoadingSpinner"
import Pagination from "@/components/MyPages/Pagination"
import ClientTable from "./ClientTable"
import Pagination from "./Pagination"
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
export default function TransactionTable({
initialJourneyTransactions,

View File

@@ -5,7 +5,7 @@ import SectionHeader from "@/components/Section/Header"
import ExpiringPointsTable from "./ExpiringPointsTable"
import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
export default async function ExpiringPoints({
link,

View File

@@ -12,8 +12,8 @@ import {
YourPointsColumn,
} from "../../../Overview/Stats/Points/PointsColumn"
import { UserProps } from "@/types/components/myPages/user"
import { LangParams } from "@/types/params"
import type { UserProps } from "@/types/components/myPages/user"
import type { LangParams } from "@/types/params"
/* TODO */
export default async function Points({ user, lang }: UserProps & LangParams) {

View File

@@ -1,76 +0,0 @@
"use client"
import { trpc } from "@/lib/trpc/client"
import { Reward } from "@/server/routers/contentstack/reward/output"
import LoadingSpinner from "@/components/LoadingSpinner"
import Grids from "@/components/TempDesignSystem/Grids"
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
import Title from "@/components/TempDesignSystem/Text/Title"
import useLang from "@/hooks/useLang"
import styles from "./current.module.css"
type CurrentRewardsClientProps = {
initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined }
}
export default function ClientCurrentRewards({
initialCurrentRewards,
}: CurrentRewardsClientProps) {
const lang = useLang()
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
trpc.contentstack.rewards.current.useInfiniteQuery(
{
limit: 3,
lang,
},
{
getNextPageParam: (lastPage) => lastPage?.nextCursor,
initialData: {
pageParams: [undefined, 1],
pages: [initialCurrentRewards],
},
}
)
function loadMoreData() {
if (hasNextPage) {
fetchNextPage()
}
}
const filteredRewards =
data?.pages.filter((page) => page && page.rewards) ?? []
const rewards = filteredRewards.flatMap((page) => page?.rewards) as Reward[]
if (isLoading) {
return <LoadingSpinner />
}
if (!rewards.length) {
return null
}
return (
<div>
<Grids.Stackable>
{rewards.map((reward, idx) => (
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
<Title
as="h4"
level="h3"
textAlign="center"
textTransform="regular"
>
{reward.label}
</Title>
</article>
))}
</Grids.Stackable>
{hasNextPage &&
(isFetching ? (
<LoadingSpinner />
) : (
<ShowMoreButton loadMoreData={loadMoreData} />
))}
</div>
)
}

View File

@@ -1,12 +0,0 @@
.card {
align-items: center;
background-color: var(--UI-Opacity-White-100);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
justify-content: center;
min-height: 280px;
padding: var(--Spacing-x7) var(--Spacing-x3);
}

View File

@@ -0,0 +1,73 @@
"use client"
import { useRef, useState } from "react"
import { RewardIcon } from "@/components/Blocks/DynamicContent/Rewards/RewardIcon"
import Pagination from "@/components/MyPages/Pagination"
import Grids from "@/components/TempDesignSystem/Grids"
import Title from "@/components/TempDesignSystem/Text/Title"
import Redeem from "./Redeem"
import styles from "./current.module.css"
import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage"
export default function ClientCurrentRewards({
rewards,
pageSize,
showRedeem,
}: CurrentRewardsClientProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [currentPage, setCurrentPage] = useState(1)
const totalPages = Math.ceil(rewards.length / pageSize)
const startIndex = (currentPage - 1) * pageSize
const endIndex = startIndex + pageSize
const currentRewards = rewards.slice(startIndex, endIndex)
function handlePageChange(page: number) {
requestAnimationFrame(() => {
setCurrentPage(page)
containerRef.current?.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "nearest",
})
})
}
return (
<div ref={containerRef} className={styles.container}>
<Grids.Stackable>
{currentRewards.map((reward, idx) => (
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
<div className={styles.content}>
<RewardIcon rewardId={reward.reward_id} />
<Title
as="h4"
level="h3"
textAlign="center"
textTransform="regular"
>
{reward.label}
</Title>
</div>
{showRedeem && "redeem_description" in reward && (
<div className={styles.btnContainer}>
<Redeem reward={reward} />
</div>
)}
</article>
))}
</Grids.Stackable>
{totalPages > 1 && (
<Pagination
pageCount={totalPages}
currentPage={currentPage}
handlePageChange={handlePageChange}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,191 @@
"use client"
import { motion } from "framer-motion"
import { useState } from "react"
import {
Dialog,
DialogTrigger,
Modal,
ModalOverlay,
} from "react-aria-components"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import Countdown from "@/components/Countdown"
import { CheckCircleIcon, CloseLargeIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import { RewardIcon } from "../RewardIcon"
import styles from "./current.module.css"
import type {
RedeemModalState,
RedeemProps,
RedeemStep,
} from "@/types/components/myPages/myPage/accountPage"
const MotionOverlay = motion(ModalOverlay)
const MotionModal = motion(Modal)
export default function Redeem({ reward }: RedeemProps) {
const [animation, setAnimation] = useState<RedeemModalState>("unmounted")
const intl = useIntl()
const update = trpc.contentstack.rewards.redeem.useMutation()
const [redeemStep, setRedeemStep] = useState<RedeemStep>("initial")
function onProceed() {
if (reward.id) {
update.mutate(
{ rewardId: reward.id },
{
onSuccess() {
setRedeemStep("redeemed")
},
onError(error) {
console.error("Failed to redeem", error)
},
}
)
}
}
function modalStateHandler(newAnimationState: RedeemModalState) {
setAnimation((currentAnimationState) =>
newAnimationState === "hidden" && currentAnimationState === "hidden"
? "unmounted"
: currentAnimationState
)
if (newAnimationState === "unmounted") {
setRedeemStep("initial")
}
}
return (
<DialogTrigger
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
>
<Button intent="primary" fullWidth>
{intl.formatMessage({ id: "Open" })}
</Button>
<MotionOverlay
className={styles.overlay}
isExiting={animation === "hidden"}
onAnimationComplete={modalStateHandler}
variants={variants.fade}
initial="hidden"
animate={animation}
>
<MotionModal
className={styles.modal}
variants={variants.slideInOut}
initial="hidden"
animate={animation}
>
<Dialog className={styles.dialog} aria-label={reward.label}>
{({ close }) => (
<>
<header className={styles.modalHeader}>
<button
onClick={close}
type="button"
className={styles.modalClose}
>
<CloseLargeIcon />
</button>
</header>
<div className={styles.modalContent}>
{redeemStep === "redeemed" && (
<div className={styles.badge}>
<div className={styles.redeemed}>
<CheckCircleIcon color="uiSemanticSuccess" />
<Caption>
{intl.formatMessage({
id: "Redeemed & valid through:",
})}
</Caption>
</div>
<Countdown />
</div>
)}
<RewardIcon rewardId={reward.reward_id} />
<Title level="h3" textAlign="center" textTransform="regular">
{reward.label}
</Title>
{redeemStep === "initial" && (
<Body textAlign="center">{reward.description}</Body>
)}
{redeemStep === "confirmation" &&
"redeem_description" in reward && (
<Body textAlign="center">
{reward.redeem_description}
</Body>
)}
</div>
{redeemStep === "initial" && (
<footer className={styles.modalFooter}>
<Button
onClick={() => setRedeemStep("confirmation")}
intent="primary"
theme="base"
>
{intl.formatMessage({ id: "Redeem benefit" })}
</Button>
</footer>
)}
{redeemStep === "confirmation" && (
<footer className={styles.modalFooter}>
<Button
onClick={onProceed}
disabled={update.isPending}
intent="primary"
theme="base"
>
{intl.formatMessage({ id: "Yes, redeem" })}
</Button>
<Button onClick={close} intent="secondary" theme="base">
{intl.formatMessage({ id: "Go back" })}
</Button>
</footer>
)}
</>
)}
</Dialog>
</MotionModal>
</MotionOverlay>
</DialogTrigger>
)
}
const variants = {
fade: {
hidden: {
opacity: 0,
transition: { duration: 0.4, ease: "easeInOut" },
},
visible: {
opacity: 1,
transition: { duration: 0.4, ease: "easeInOut" },
},
},
slideInOut: {
hidden: {
opacity: 0,
y: 32,
transition: { duration: 0.4, ease: "easeInOut" },
},
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeInOut" },
},
},
}

View File

@@ -0,0 +1,132 @@
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x4);
position: relative;
scroll-margin-top: calc(var(--current-mobile-site-header-height) * 2);
}
.card {
background-color: var(--UI-Opacity-White-100);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.content {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
align-items: center;
justify-content: center;
padding: var(--Spacing-x3);
}
.btnContainer {
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
}
.badge {
border-radius: var(--Small, 4px);
border: 1px solid var(--Base-Border-Subtle);
display: flex;
padding: var(--Spacing-x1) var(--Spacing-x2);
flex-direction: column;
justify-content: center;
align-items: center;
}
.redeemed {
display: flex;
justify-content: center;
align-items: center;
gap: var(--Spacing-x-half);
align-self: stretch;
}
.overlay {
background: rgba(0, 0, 0, 0.5);
height: var(--visual-viewport-height);
position: fixed;
top: 0;
left: 0;
width: 100vw;
z-index: 100;
}
@media screen and (min-width: 768px) {
.overlay {
display: flex;
justify-content: center;
align-items: center;
}
}
.modal {
background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Medium);
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
width: 100%;
position: absolute;
left: 0;
bottom: 0;
z-index: 101;
}
@media screen and (min-width: 768px) {
.modal {
left: auto;
bottom: auto;
width: 400px;
}
}
.dialog {
display: flex;
flex-direction: column;
padding-bottom: var(--Spacing-x3);
}
.modalHeader {
--button-height: 32px;
box-sizing: content-box;
display: flex;
align-items: center;
height: var(--button-height);
position: relative;
justify-content: center;
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
}
.modalContent {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--Spacing-x2);
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
}
.modalFooter {
display: flex;
flex-direction: column;
padding: 0 var(--Spacing-x3) var(--Spacing-x1);
gap: var(--Spacing-x-one-and-half);
}
.modalFooter > button {
flex: 1 0 100%;
}
.modalClose {
background: none;
border: none;
cursor: pointer;
position: absolute;
right: var(--Spacing-x2);
width: 32px;
height: var(--button-height);
display: flex;
align-items: center;
}

View File

@@ -1,3 +1,4 @@
import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import SectionContainer from "@/components/Section/Container"
@@ -13,19 +14,20 @@ export default async function CurrentRewardsBlock({
subtitle,
link,
}: AccountPageComponentProps) {
const initialCurrentRewards =
await serverClient().contentstack.rewards.current({
limit: 3,
})
const rewardsResponse = await serverClient().contentstack.rewards.current()
if (!initialCurrentRewards) {
if (!rewardsResponse?.rewards.length) {
return null
}
return (
<SectionContainer>
<SectionHeader title={title} link={link} preamble={subtitle} />
<ClientCurrentRewards initialCurrentRewards={initialCurrentRewards} />
<ClientCurrentRewards
rewards={rewardsResponse.rewards}
pageSize={6}
showRedeem={env.USE_NEW_REWARDS_ENDPOINT && env.USE_NEW_REWARD_MODEL}
/>
<SectionLink link={link} variant="mobile" />
</SectionContainer>
)

View File

@@ -0,0 +1,68 @@
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
import { isValidRewardId } from "@/utils/rewards"
import type { FC } from "react"
import { IconName, type IconProps } from "@/types/components/icon"
import { RewardId } from "@/types/enums/rewards"
function getIconForRewardId(rewardId: RewardId): IconName {
switch (rewardId) {
// Food & beverage
case RewardId.TenPercentFood:
case RewardId.FifteenPercentFood:
return IconName.CroissantCoffeeEgg
case RewardId.TwoForOneBreakfast:
return IconName.CutleryTwo
case RewardId.FreeBreakfast:
return IconName.CutleryOne
case RewardId.FreeKidsDrink:
return IconName.KidsMocktail
// Monetary vouchers
case RewardId.Bonus50SEK:
case RewardId.Bonus75SEK:
case RewardId.Bonus100SEK:
case RewardId.Bonus150SEK:
case RewardId.Bonus200SEK:
return IconName.Voucher
// Hotel perks
case RewardId.EarlyCheckin:
return IconName.HandKey
case RewardId.LateCheckout:
return IconName.HotelNight
case RewardId.FreeUpgrade:
return IconName.MagicWand
case RewardId.RoomGuarantee48H:
return IconName.Bed
// Earnings
case RewardId.EarnRate25Percent:
case RewardId.EarnRate50Percent:
return IconName.MoneyHand
case RewardId.StayBoostForKids:
return IconName.Kids
case RewardId.MemberRate:
return IconName.Coin
// Special
case RewardId.YearlyExclusiveGift:
return IconName.GiftOpen
default: {
const unhandledRewardId: never = rewardId
return IconName.GiftOpen
}
}
}
export function mapRewardToIcon(rewardId: string): FC<IconProps> | null {
if (!isValidRewardId(rewardId)) {
// TODO: Update once UX has decided on fallback icon.
return getIconByIconName(IconName.GiftOpen)
}
const iconName = getIconForRewardId(rewardId)
return getIconByIconName(iconName)
}

View File

@@ -0,0 +1,27 @@
import { mapRewardToIcon } from "./data"
import type { RewardIconProps } from "@/types/components/myPages/rewards"
// Original SVG aspect ratio is 358:202 (≈1.77:1)
const sizeMap = {
small: { width: 120, height: 68 }, // 40% of card width
medium: { width: 180, height: 102 }, // 60% of card width
large: { width: 240, height: 135 }, // 80% of card width
} as const
export function RewardIcon({
rewardId,
size = "medium",
...props
}: RewardIconProps) {
const IconComponent = mapRewardToIcon(rewardId)
if (!IconComponent) return null
return (
<IconComponent
{...props}
width={sizeMap[size].width}
height={sizeMap[size].height}
/>
)
}

View File

@@ -6,7 +6,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import SignupForm from "@/components/Forms/Signup"
import { getLang } from "@/i18n/serverContext"
import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
export default async function SignupFormWrapper({
dynamic_content,

View File

@@ -8,7 +8,7 @@ import Grids from "@/components/TempDesignSystem/Grids"
import StayCard from "../StayCard"
import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays"
import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
export default async function SoonestStays({
title,

View File

@@ -9,7 +9,7 @@ import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable"
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel"
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper"
import SignUpVerification from "@/components/Blocks/DynamicContent/SignUpVerification"

View File

@@ -1,4 +1,4 @@
import { UspIcon } from "@/types/components/blocks/uspGrid"
import type { UspIcon } from "@/types/components/blocks/uspGrid"
import { IconName } from "@/types/components/icon"
export function getUspIconName(icon?: UspIcon | null) {

View File

@@ -114,9 +114,9 @@ export default function BookingWidgetClient({
rooms: defaultRoomsData,
},
shouldFocusError: false,
mode: "all",
mode: "onSubmit",
resolver: zodResolver(bookingWidgetSchema),
reValidateMode: "onChange",
reValidateMode: "onSubmit",
})
function closeMobileSearch() {

View File

@@ -36,7 +36,7 @@ export default async function AmenitiesList({
height={20}
/>
)}
<Body color="textMediumContrast">{facility.name}</Body>
<Body color="uiTextMediumContrast">{facility.name}</Body>
</div>
)
})}

View File

@@ -23,6 +23,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
href: `?s=${activities[lang]}`,
title: activitiesCard.ctaText,
isExternal: false,
scrollOnClick: false,
}
: undefined,
secondaryButton: hasImage
@@ -31,6 +32,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
href: `?s=${activities[lang]}`,
title: activitiesCard.ctaText,
isExternal: false,
scrollOnClick: false,
},
}
return (

View File

@@ -51,7 +51,7 @@ export default async function IntroSection({
</BiroScript>
<Title level="h2">{hotelName}</Title>
</div>
<Body color="textMediumContrast">{formattedLocationText}</Body>
<Body color="uiTextMediumContrast">{formattedLocationText}</Body>
{hasTripAdvisorData && (
<Link
className={styles.introLink}

View File

@@ -1,4 +1,4 @@
import {
import type {
HotelAddress,
HotelData,
HotelLocation,

View File

@@ -1,6 +1,6 @@
"use client"
import { PropsWithChildren, useRef } from "react"
import { type PropsWithChildren, useRef } from "react"
import { StickyElementNameEnum } from "@/stores/sticky-position"

View File

@@ -28,6 +28,7 @@ export default function PreviewImages({
title={image.metaData.title}
width={index === 0 ? 752 : 292}
height={index === 0 ? 540 : 266}
onClick={() => setLightboxIsOpen(true)}
className={styles.image}
/>
))}

View File

@@ -13,6 +13,7 @@
width: 100%;
height: 100%;
max-height: 30vh;
cursor: pointer;
}
.imageWrapper > :nth-child(2),

View File

@@ -4,9 +4,9 @@ import { useRef, useState } from "react"
import { useIntl } from "react-intl"
import SectionContainer from "@/components/Section/Container"
import SectionHeader from "@/components/Section/Header"
import Grids from "@/components/TempDesignSystem/Grids"
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
import Title from "@/components/TempDesignSystem/Text/Title"
import { RoomCard } from "./RoomCard"
@@ -35,19 +35,19 @@ export function Rooms({ rooms }: RoomsProps) {
className={styles.roomsContainer}
>
<div ref={scrollRef} className={styles.scrollRef}></div>
<SectionHeader
textTransform="capitalize"
title={intl.formatMessage({ id: "Rooms" })}
preamble={null}
/>
<Title as="h3" level="h2">
{intl.formatMessage({ id: "Rooms" })}
</Title>
<Grids.Stackable
className={`${styles.grid} ${allRoomsVisible ? styles.allVisible : ""}`}
>
{rooms.map((room) => (
<div key={room.id}>
<RoomCard room={room} />
</div>
))}
{rooms
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((room) => (
<div key={room.id}>
<RoomCard room={room} />
</div>
))}
</Grids.Stackable>
{showToggleButton ? (

View File

@@ -40,6 +40,7 @@ export default async function AboutTheHotelSidePeek({
<Divider color="baseSurfaceSubtleHover" />
<Preamble>{descriptions.descriptions.medium}</Preamble>
<Body>{descriptions.facilityInformation}</Body>
<Body>{descriptions.surroundingInformation}</Body>
</section>
</SidePeek>
)

View File

@@ -17,6 +17,7 @@ export default async function AccessibilityAmenity({
<AccordionItem
title={intl.formatMessage({ id: "Accessibility" })}
icon={IconName.Accessibility}
variant="sidepeek"
>
<div className={styles.wrapper}>
{accessibility?.description && (

View File

@@ -9,6 +9,7 @@ export default async function BreakfastAmenity() {
<AccordionItem
title={intl.formatMessage({ id: "Breakfast" })}
icon={IconName.CoffeeAlt}
variant="sidepeek"
>
{/* TODO: breakfast to be implemented */}
</AccordionItem>

View File

@@ -14,6 +14,7 @@ export default async function CheckInAmenity({
<AccordionItem
title={`${intl.formatMessage({ id: "Check-in" })}/${intl.formatMessage({ id: "Check-out" })}`}
icon={IconName.Business}
variant="sidepeek"
>
<Body textTransform="bold">{intl.formatMessage({ id: "Times" })}</Body>
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check in from" })}: ${checkInTime}`}</Body>

View File

@@ -25,6 +25,7 @@ export default async function ParkingAmenity({
<AccordionItem
title={intl.formatMessage({ id: "Parking" })}
icon={IconName.Parking}
variant="sidepeek"
>
<div className={styles.wrapper}>
{parking.map((data) => (

View File

@@ -1,10 +1,10 @@
.wrapper {
padding: var(--Spacing-x1);
padding: var(--Spacing-x1) var(--Spacing-x0);
border-bottom: 1px solid var(--Base-Border-Subtle);
}
.amenity {
display: flex;
gap: var(--Spacing-x1);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
}

View File

@@ -1,5 +1,5 @@
import { HeartIcon } from "@/components/Icons"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { mapFacilityToIcon } from "../../../data"
@@ -15,16 +15,18 @@ export default function FilteredAmenities({
{filteredAmenities?.map((amenity) => {
const Icon = mapFacilityToIcon(amenity.id)
return (
<div key={amenity.name} className={styles.wrapper}>
<li key={amenity.name} className={styles.wrapper}>
<div className={styles.amenity}>
{Icon ? (
<Icon color="burgundy" width={24} height={24} />
) : (
<HeartIcon color="burgundy" width={24} height={24} />
)}
<Body color="burgundy">{amenity.name}</Body>
<Subtitle color="burgundy" type="two">
{amenity.name}
</Subtitle>
</div>
</div>
</li>
)
})}
</>

View File

@@ -6,7 +6,7 @@ import { getIntl } from "@/i18n"
import styles from "./facility.module.css"
import { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility"
import type { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility"
export default async function Facility({ data }: FacilityProps) {
const intl = await getIntl()

View File

@@ -14,6 +14,7 @@
justify-content: flex-start;
padding: 0 var(--Spacing-x2);
width: 100%;
overflow-x: auto;
}
@media screen and (min-width: 768px) {
@@ -26,5 +27,6 @@
.tabsContainer {
padding: 0 var(--Spacing-x5);
max-width: calc(100% - var(--hotel-page-map-desktop-width));
overflow-x: visible;
}
}

View File

@@ -1,8 +1,8 @@
import { FC } from "react"
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
import { IconName, IconProps } from "@/types/components/icon"
import type { FC } from "react"
import { IconName, type IconProps } from "@/types/components/icon"
import { FacilityEnum } from "@/types/enums/facilities"
const facilityToIconMap: Record<FacilityEnum, IconName> = {

View File

@@ -78,7 +78,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
const roomCategories =
hotelData.included?.filter((item) => item.type === "roomcategories") || []
const images = gallery?.smallerImages
const description = hotelContent.texts.descriptions.short
const description = hotelContent.texts.descriptions.medium
const activitiesCard = content?.[0]?.upcoming_activities_card || null
const facilities: Facility[] = [

View File

@@ -1,10 +1,9 @@
import { staticPageVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
import type { TrackingSDKPageData } from "@/types/components/tracking"
import type { CollectionPage } from "@/types/trpc/routers/contentstack/collectionPage"
import type { ContentPage } from "@/types/trpc/routers/contentstack/contentPage"
import type { staticPageVariants } from "./variants"
export interface StaticPageProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "content">,

View File

@@ -0,0 +1,34 @@
"use client"
import { useState } from "react"
import { useInterval } from "usehooks-ts"
import { dt } from "@/lib/dt"
import Title from "@/components/TempDesignSystem/Text/Title"
import type { CountdownProps } from "@/types/components/countdown"
export default function Countdown({
minutes = 30,
seconds = 0,
}: CountdownProps) {
const [time, setTime] = useState(dt.duration({ minutes, seconds }))
const timeSeconds = time.asSeconds()
useInterval(
() => {
setTime((currentTime) => {
const newTime = currentTime.asMilliseconds() - 1000
return dt.duration(newTime)
})
},
timeSeconds > 0 ? 1000 : null
)
return (
<Title as="h1">
<time dateTime={time.toISOString()}>{time.format("m:ss")}</time>
</Title>
)
}

View File

@@ -2,8 +2,7 @@ import { cva } from "class-variance-authority"
import styles from "./list.module.css"
import type { ListItem } from "@/types/requests/blocks/list"
import { BlockListItemsEnum } from "@/types/requests/blocks/list"
import { BlockListItemsEnum, type ListItem } from "@/types/requests/blocks/list"
const config = {
variants: {

View File

@@ -1,7 +1,7 @@
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import { Lang, languages } from "@/constants/languages"
import { type Lang, languages } from "@/constants/languages"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"

View File

@@ -1,7 +1,7 @@
"use client"
import { useState } from "react"
import { Lang, languages } from "@/constants/languages"
import { type Lang, languages } from "@/constants/languages"
import useLang from "@/hooks/useLang"

View File

@@ -1,7 +1,7 @@
import Desktop from "./Desktop"
import Mobile from "./Mobile"
import { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
type LanguageSwitcherProps = { urls: LanguageSwitcherData }

View File

@@ -3,7 +3,6 @@ import { Fragment } from "react"
import { useIntl } from "react-intl"
import { logout } from "@/constants/routes/handleAuth"
import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
import useDropdownStore from "@/stores/main-menu"
import Divider from "@/components/TempDesignSystem/Divider"
@@ -14,6 +13,7 @@ import useLang from "@/hooks/useLang"
import styles from "./my-pages-mobile-dropdown.module.css"
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
import type { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
type Navigation = Awaited<ReturnType<(typeof navigationQueryRouter)["get"]>>

View File

@@ -1,4 +1,4 @@
import { Lang } from "@/constants/languages"
import type { Lang } from "@/constants/languages"
type Texts = {
title: string

View File

@@ -3,7 +3,7 @@
import { usePathname, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import {
import type {
SiteSectionObject,
TrackingData,
TrackingProps,

View File

@@ -7,13 +7,13 @@ import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
import { EmbedEnum } from "@/types/requests/utils/embeds"
import type { Attributes } from "@/types/rte/attrs"
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/rte/enums"
import type {
RTEDefaultNode,
RTENext,
RTENode,
RTERegularNode,
import {
type RTEDefaultNode,
RTEMarkType,
type RTENext,
type RTENode,
type RTERegularNode,
} from "@/types/rte/node"
import { RTEMarkType } from "@/types/rte/node"
import type { RenderOptions } from "@/types/rte/option"
function extractPossibleAttributes(attrs: Attributes | undefined) {

View File

@@ -17,7 +17,7 @@ import { hasAvailableParagraphFormat, hasAvailableULFormat } from "./utils"
import styles from "./jsontohtml.module.css"
import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
import { ImageVaultAsset } from "@/types/components/imageVault"
import type { ImageVaultAsset } from "@/types/components/imageVault"
import { EmbedEnum } from "@/types/requests/utils/embeds"
import type { Attributes, RTEImageVaultAttrs } from "@/types/rte/attrs"
import {
@@ -25,15 +25,15 @@ import {
RTEItemTypeEnum,
RTETypeEnum,
} from "@/types/rte/enums"
import type {
RTEDefaultNode,
RTEImageNode,
RTENext,
RTENode,
RTERegularNode,
RTETextNode,
import {
type RTEDefaultNode,
type RTEImageNode,
RTEMarkType,
type RTENext,
type RTENode,
type RTERegularNode,
type RTETextNode,
} from "@/types/rte/node"
import { RTEMarkType } from "@/types/rte/node"
import type { RenderOptions } from "@/types/rte/option"
function extractPossibleAttributes(attrs: Attributes | undefined) {

Some files were not shown because too many files have changed in this diff Show More