Merge remote-tracking branch 'origin' into feature/tracking
This commit is contained in:
@@ -61,3 +61,5 @@ ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false"
|
|||||||
SHOW_SITE_WIDE_ALERT="false"
|
SHOW_SITE_WIDE_ALERT="false"
|
||||||
SHOW_SIGNUP_FLOW="true"
|
SHOW_SIGNUP_FLOW="true"
|
||||||
USE_NEW_REWARDS_ENDPOINT="true"
|
USE_NEW_REWARDS_ENDPOINT="true"
|
||||||
|
|
||||||
|
USE_NEW_REWARD_MODEL="true"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ CMS_API_KEY="test"
|
|||||||
CMS_PREVIEW_TOKEN="test"
|
CMS_PREVIEW_TOKEN="test"
|
||||||
CMS_PREVIEW_URL="test"
|
CMS_PREVIEW_URL="test"
|
||||||
CMS_URL="test"
|
CMS_URL="test"
|
||||||
|
CMS_BRANCH="development"
|
||||||
CURITY_CLIENT_ID_SERVICE="test"
|
CURITY_CLIENT_ID_SERVICE="test"
|
||||||
CURITY_CLIENT_SECRET_SERVICE="test"
|
CURITY_CLIENT_SECRET_SERVICE="test"
|
||||||
CURITY_CLIENT_ID_USER="test"
|
CURITY_CLIENT_ID_USER="test"
|
||||||
@@ -44,6 +45,7 @@ GOOGLE_DYNAMIC_MAP_ID="test"
|
|||||||
HIDE_FOR_NEXT_RELEASE="true"
|
HIDE_FOR_NEXT_RELEASE="true"
|
||||||
SALESFORCE_PREFERENCE_BASE_URL="test"
|
SALESFORCE_PREFERENCE_BASE_URL="test"
|
||||||
USE_NEW_REWARDS_ENDPOINT="true"
|
USE_NEW_REWARDS_ENDPOINT="true"
|
||||||
|
USE_NEW_REWARD_MODEL="true"
|
||||||
|
|
||||||
TZ=UTC
|
TZ=UTC
|
||||||
ENABLE_BOOKING_FLOW="false"
|
ENABLE_BOOKING_FLOW="false"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals", "plugin:import/recommended"],
|
"extends": ["next/core-web-vitals", "plugin:import/recommended"],
|
||||||
"plugins": ["simple-import-sort"],
|
"plugins": ["simple-import-sort", "@typescript-eslint"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/function-component-definition": "error",
|
"react/function-component-definition": "error",
|
||||||
"simple-import-sort/imports": [
|
"simple-import-sort/imports": [
|
||||||
@@ -52,6 +53,12 @@
|
|||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
"import/first": "error",
|
"import/first": "error",
|
||||||
"import/newline-after-import": "error",
|
"import/newline-after-import": "error",
|
||||||
"import/no-duplicates": "error"
|
"import/no-duplicates": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"prefer-inline": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/consistent-type-imports": "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
import { AuthError } from "next-auth"
|
import { AuthError } from "next-auth"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Breadcrumbs from "@/components/Breadcrumbs"
|
|||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
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>) {
|
export default function AllBreadcrumbs({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.content {
|
.content {
|
||||||
gap: var(--Spacing-x5);
|
gap: var(--Spacing-x5);
|
||||||
grid-template-columns: max(360px) 1fr;
|
grid-template-columns: max(340px) 1fr;
|
||||||
padding-left: var(--Spacing-x5);
|
padding-left: var(--Spacing-x5);
|
||||||
padding-right: var(--Spacing-x5);
|
padding-right: var(--Spacing-x5);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function CommunicationSlot({
|
export default async function CommunicationSlot({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
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>) {
|
export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function MembershipCardSlot({
|
export default async function MembershipCardSlot({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getProfile } from "@/lib/trpc/memoizedRequests"
|
|||||||
import Form from "@/components/Forms/Edit/Profile"
|
import Form from "@/components/Forms/Edit/Profile"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function EditProfileSlot({
|
export default async function EditProfileSlot({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
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>) {
|
export default async function Profile({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export { generateMetadata } from "@/utils/generateMetadata"
|
export { generateMetadata } from "@/utils/generateMetadata"
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Breadcrumbs from "@/components/Breadcrumbs"
|
|||||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
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>) {
|
export default function PageBreadcrumbs({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { setPreviewData } from "@/lib/previewContext"
|
|||||||
|
|
||||||
import InitLivePreview from "@/components/LivePreview"
|
import InitLivePreview from "@/components/LivePreview"
|
||||||
|
|
||||||
import { PageArgs, UIDParams } from "@/types/params"
|
import type { PageArgs, UIDParams } from "@/types/params"
|
||||||
|
|
||||||
export default function PreviewPage({
|
export default function PreviewPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
ContentTypeParams,
|
ContentTypeParams,
|
||||||
LangParams,
|
LangParams,
|
||||||
LayoutArgs,
|
LayoutArgs,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
|
|||||||
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
|
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
ContentTypeParams,
|
ContentTypeParams,
|
||||||
LangParams,
|
LangParams,
|
||||||
PageArgs,
|
PageArgs,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
export default function PaymentCallbackLayout({
|
export default function PaymentCallbackLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
|
|
||||||
import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback"
|
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({
|
export default async function PaymentCallbackPage({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
export default function HotelReservationLayout({
|
export default function HotelReservationLayout({
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
|
|
||||||
export default function LoadingModal() {
|
|
||||||
return <LoadingSpinner />
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function Default() {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -3,3 +3,9 @@
|
|||||||
background-color: var(--Base-Background-Primary-Normal);
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.layout {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
export default function HotelReservationLayout({
|
export default function HotelReservationLayout({
|
||||||
children,
|
children,
|
||||||
modal,
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
}: React.PropsWithChildren<
|
return <div className={styles.layout}>{children}</div>
|
||||||
LayoutArgs<LangParams> & { modal: React.ReactNode }
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<div className={styles.layout}>
|
|
||||||
{children}
|
|
||||||
{modal}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,51 +1,18 @@
|
|||||||
import { differenceInCalendarDays, format, isWeekend } from "date-fns"
|
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import {
|
|
||||||
selectHotel,
|
|
||||||
selectHotelMap,
|
|
||||||
} from "@/constants/routes/hotelReservation"
|
|
||||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import {
|
import SelectHotel from "@/components/HotelReservation/SelectHotel"
|
||||||
fetchAvailableHotels,
|
import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton"
|
||||||
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 {
|
import {
|
||||||
generateChildrenString,
|
generateChildrenString,
|
||||||
getHotelReservationQueryParams,
|
getHotelReservationQueryParams,
|
||||||
} from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
} 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 { 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 type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams"
|
||||||
import {
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
TrackingChannelEnum,
|
|
||||||
TrackingSDKHotelInfo,
|
|
||||||
TrackingSDKPageData,
|
|
||||||
} from "@/types/components/tracking"
|
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export default async function SelectHotelPage({
|
export default async function SelectHotelPage({
|
||||||
params,
|
params,
|
||||||
@@ -64,10 +31,6 @@ export default async function SelectHotelPage({
|
|||||||
|
|
||||||
if (!city) return notFound()
|
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 selectHotelParams = new URLSearchParams(searchParams)
|
||||||
const selectHotelParamsObject =
|
const selectHotelParamsObject =
|
||||||
getHotelReservationQueryParams(selectHotelParams)
|
getHotelReservationQueryParams(selectHotelParams)
|
||||||
@@ -79,157 +42,30 @@ export default async function SelectHotelPage({
|
|||||||
return notFound()
|
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 child = selectHotelParamsObject.room[0].child // TODO: Handle multiple rooms
|
||||||
const children = child ? generateChildrenString(child) : undefined
|
|
||||||
|
|
||||||
const hotels = await fetchAvailableHotels({
|
const reservationParams = {
|
||||||
cityId: city.id,
|
selectHotelParams,
|
||||||
roomStayStartDate: searchParams.fromDate,
|
searchParams,
|
||||||
roomStayEndDate: searchParams.toDate,
|
adultsParams,
|
||||||
adults,
|
childrenParams,
|
||||||
children,
|
child,
|
||||||
})
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense
|
||||||
<header className={styles.header}>
|
key={`${city.name}-${searchParams.fromDate}-${searchParams.toDate}-${adultsParams}-${childrenParams}`}
|
||||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
fallback={<SelectHotelSkeleton />}
|
||||||
<Breadcrumbs breadcrumbs={breadcrumbs} />
|
>
|
||||||
</Suspense>
|
<SelectHotel
|
||||||
<div className={styles.title}>
|
city={city}
|
||||||
<div className={styles.cityInformation}>
|
params={params}
|
||||||
<Subtitle>{city.name}</Subtitle>
|
reservationParams={reservationParams}
|
||||||
<HotelCount />
|
/>
|
||||||
</div>
|
</Suspense>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dayjs } from "dayjs"
|
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
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
|
* 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")
|
* @example const { fromDate, toDate} = getValidDates("2021-01-01", "2021-01-02")
|
||||||
|
|||||||
@@ -17,12 +17,10 @@
|
|||||||
|
|
||||||
@media screen and (min-width: 1367px) {
|
@media screen and (min-width: 1367px) {
|
||||||
.container {
|
.container {
|
||||||
width: var(--max-width-page);
|
|
||||||
|
|
||||||
grid-template-columns: 1fr 340px;
|
grid-template-columns: 1fr 340px;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
|
width: var(--max-width-page);
|
||||||
margin: var(--Spacing-x5) auto 0;
|
margin: var(--Spacing-x5) auto 0;
|
||||||
/* simulates padding on viewport smaller than --max-width-navigation */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -35,12 +35,11 @@ import EnterDetailsTracking from "./enterDetailsTracking"
|
|||||||
import styles from "./page.module.css"
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import {
|
import type {
|
||||||
TrackingChannelEnum,
|
TrackingChannelEnum,
|
||||||
TrackingSDKHotelInfo,
|
TrackingSDKHotelInfo,
|
||||||
TrackingSDKPageData,
|
TrackingSDKPageData} from "@/types/components/tracking";
|
||||||
} from "@/types/components/tracking"
|
|
||||||
import { StepEnum } from "@/types/enums/step"
|
import { StepEnum } from "@/types/enums/step"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
import { AuthError } from "next-auth"
|
import { AuthError } from "next-auth"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
@@ -117,7 +117,7 @@ export async function GET(
|
|||||||
/** Record<string, any> is next-auth typings */
|
/** Record<string, any> is next-auth typings */
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
ui_locales: context.params.lang,
|
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
|
* The `acr_values` param is used to make Curity display the proper login
|
||||||
* page for Scandic. Without the parameter Curity presents some choices
|
* page for Scandic. Without the parameter Curity presents some choices
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
import { AuthError } from "next-auth"
|
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 { badRequest, internalServerError } from "@/server/errors/next"
|
||||||
import { getPublicURL } from "@/server/utils"
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
import { signIn } from "@/auth"
|
import { signIn } from "@/auth"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
context: { params: { lang: Lang } }
|
context: { params: { lang: Lang } }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { env } from "@/env/server"
|
|||||||
|
|
||||||
import BookingWidget, { preload } from "@/components/BookingWidget"
|
import BookingWidget, { preload } from "@/components/BookingWidget"
|
||||||
|
|
||||||
import { PageArgs } from "@/types/params"
|
import type { PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function BookingWidgetPage({
|
export default async function BookingWidgetPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
|
|
||||||
import BookingWidget, { preload } from "@/components/BookingWidget"
|
import BookingWidget, { preload } from "@/components/BookingWidget"
|
||||||
|
|
||||||
import { PageArgs } from "@/types/params"
|
import type { PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function BookingWidgetPage({
|
export default async function BookingWidgetPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CurrentFooter from "@/components/Current/Footer"
|
|||||||
import Footer, { preload } from "@/components/Footer"
|
import Footer, { preload } from "@/components/Footer"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
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>) {
|
export default function FooterSlot({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import HeaderFallback from "@/components/Current/Header/HeaderFallback"
|
|||||||
import Header from "@/components/Header"
|
import Header from "@/components/Header"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
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>) {
|
export default function HeaderPage({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { findLang } from "@/utils/languages"
|
|||||||
|
|
||||||
import styles from "./error.module.css"
|
import styles from "./error.module.css"
|
||||||
|
|
||||||
import { LangParams } from "@/types/params"
|
import type { LangParams } from "@/types/params"
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import "@scandic-hotels/design-system/style.css"
|
|||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
import TrpcProvider from "@/lib/trpc/Provider"
|
import TrpcProvider from "@/lib/trpc/Provider"
|
||||||
|
|
||||||
import TokenRefresher from "@/components/Auth/TokenRefresher"
|
import TokenRefresher from "@/components/Auth/TokenRefresher"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import NotFound from "@/components/Current/NotFound"
|
import NotFound from "@/components/Current/NotFound"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
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>) {
|
export default function NotFoundPage({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
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({
|
export default function MiddlewareError({
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Header from "@/components/Current/Header"
|
import Header from "@/components/Current/Header"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
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>) {
|
export default async function HeaderPage({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import AccountPage from "@/components/Webviews/AccountPage"
|
|||||||
import LoyaltyPage from "@/components/Webviews/LoyaltyPage"
|
import LoyaltyPage from "@/components/Webviews/LoyaltyPage"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
ContentTypeWebviewParams,
|
ContentTypeWebviewParams,
|
||||||
LangParams,
|
LangParams,
|
||||||
PageArgs,
|
PageArgs,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { setLang } from "@/i18n/serverContext"
|
|||||||
|
|
||||||
import styles from "./page.module.css"
|
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>) {
|
export default function Refresh({ params }: PageArgs<LangParams>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { NextRequest } from "next/server"
|
|
||||||
import { env } from "process"
|
import { env } from "process"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
@@ -6,6 +5,8 @@ import { profile } from "@/constants/routes/myPages"
|
|||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
import { getPublicURL } from "@/server/utils"
|
import { getPublicURL } from "@/server/utils"
|
||||||
|
|
||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: { lang: string } }
|
{ params }: { params: { lang: string } }
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
import type { NextRequest } from "next/server"
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
return NextResponse.json(Object.fromEntries(request.headers.entries()))
|
return NextResponse.json(Object.fromEntries(request.headers.entries()))
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { revalidateTag } from "next/cache"
|
import { revalidateTag } from "next/cache"
|
||||||
import { headers } from "next/headers"
|
import { headers } from "next/headers"
|
||||||
import { NextRequest } from "next/server"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
@@ -9,6 +8,8 @@ import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
|||||||
|
|
||||||
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
enum LoyaltyConfigContentTypes {
|
enum LoyaltyConfigContentTypes {
|
||||||
loyalty_level = "loyalty_level",
|
loyalty_level = "loyalty_level",
|
||||||
reward = "reward",
|
reward = "reward",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { revalidateTag } from "next/cache"
|
import { revalidateTag } from "next/cache"
|
||||||
import { headers } from "next/headers"
|
import { headers } from "next/headers"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
// This file is primarily to be used locally to test
|
// This file is primarily to be used locally to test
|
||||||
// purging your cache for new (and old) requests
|
// purging your cache for new (and old) requests
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { revalidateTag } from "next/cache"
|
import { revalidateTag } from "next/cache"
|
||||||
import { headers } from "next/headers"
|
import { headers } from "next/headers"
|
||||||
import { NextRequest } from "next/server"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
@@ -17,6 +16,8 @@ import {
|
|||||||
generateTag,
|
generateTag,
|
||||||
} from "@/utils/generateTag"
|
} from "@/utils/generateTag"
|
||||||
|
|
||||||
|
import type { NextRequest } from "next/server"
|
||||||
|
|
||||||
const validateJsonBody = z.object({
|
const validateJsonBody = z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
content_type: z.object({
|
content_type: z.object({
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function CardsGrid({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
theme={
|
theme={
|
||||||
cards_grid.theme ?? (card.backgroundImage ? "image" : "one")
|
card.backgroundImage ? "image" : cards_grid.theme ?? "one"
|
||||||
}
|
}
|
||||||
key={card.system.uid}
|
key={card.system.uid}
|
||||||
scriptedTopTitle={card.scripted_top_title}
|
scriptedTopTitle={card.scripted_top_title}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import SectionWrapper from "../SectionWrapper"
|
|||||||
|
|
||||||
import styles from "./loyaltyLevels.module.css"
|
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"
|
import type { LevelCardProps } from "@/types/components/overviewTable"
|
||||||
|
|
||||||
export default async function LoyaltyLevels({
|
export default async function LoyaltyLevels({
|
||||||
|
|||||||
@@ -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
|
export interface HeroProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HeroProps } from "./hero"
|
|
||||||
import { heroVariants } from "./heroVariants"
|
import { heroVariants } from "./heroVariants"
|
||||||
|
|
||||||
|
import type { HeroProps } from "./hero"
|
||||||
|
|
||||||
export default function Hero({ className, color, children }: HeroProps) {
|
export default function Hero({ className, color, children }: HeroProps) {
|
||||||
const classNames = heroVariants({ className, color })
|
const classNames = heroVariants({ className, color })
|
||||||
return <section className={classNames}>{children}</section>
|
return <section className={classNames}>{children}</section>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getMembership } from "@/utils/user"
|
|||||||
import PointsContainer from "./Container"
|
import PointsContainer from "./Container"
|
||||||
import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn"
|
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) {
|
export default async function Points({ user }: UserProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useReducer } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MembershipLevel,
|
type MembershipLevel,
|
||||||
MembershipLevelEnum,
|
MembershipLevelEnum,
|
||||||
} from "@/constants/membershipLevels"
|
} from "@/constants/membershipLevels"
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ import styles from "./overviewTable.module.css"
|
|||||||
import type { Key } from "react-aria-components"
|
import type { Key } from "react-aria-components"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ComparisonLevel,
|
type ComparisonLevel,
|
||||||
DesktopSelectColumns,
|
type DesktopSelectColumns,
|
||||||
type MobileColumnHeaderProps,
|
type MobileColumnHeaderProps,
|
||||||
OverviewTableActionsEnum,
|
OverviewTableActionsEnum,
|
||||||
type OverviewTableClientProps,
|
type OverviewTableClientProps,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { serverClient } from "@/lib/trpc/server"
|
|||||||
import SectionWrapper from "../SectionWrapper"
|
import SectionWrapper from "../SectionWrapper"
|
||||||
import OverviewTableClient from "./Client"
|
import OverviewTableClient from "./Client"
|
||||||
|
|
||||||
import { OverviewTableProps } from "@/types/components/blocks/dynamicContent"
|
import type { OverviewTableProps } from "@/types/components/blocks/dynamicContent"
|
||||||
|
|
||||||
export default async function OverviewTable({
|
export default async function OverviewTable({
|
||||||
dynamic_content,
|
dynamic_content,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type LevelWithRewards,
|
type LevelWithRewards,
|
||||||
OverviewTableActionsEnum,
|
OverviewTableActionsEnum,
|
||||||
type OverviewTableClientProps,
|
type OverviewTableClientProps,
|
||||||
OverviewTableReducerAction,
|
type OverviewTableReducerAction,
|
||||||
} from "@/types/components/overviewTable"
|
} from "@/types/components/overviewTable"
|
||||||
|
|
||||||
export function getLevel(
|
export function getLevel(
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import { useState } from "react"
|
|||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
import Pagination from "@/components/MyPages/Pagination"
|
||||||
|
|
||||||
import ClientTable from "./ClientTable"
|
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({
|
export default function TransactionTable({
|
||||||
initialJourneyTransactions,
|
initialJourneyTransactions,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import SectionHeader from "@/components/Section/Header"
|
|||||||
|
|
||||||
import ExpiringPointsTable from "./ExpiringPointsTable"
|
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({
|
export default async function ExpiringPoints({
|
||||||
link,
|
link,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
YourPointsColumn,
|
YourPointsColumn,
|
||||||
} from "../../../Overview/Stats/Points/PointsColumn"
|
} from "../../../Overview/Stats/Points/PointsColumn"
|
||||||
|
|
||||||
import { UserProps } from "@/types/components/myPages/user"
|
import type { UserProps } from "@/types/components/myPages/user"
|
||||||
import { LangParams } from "@/types/params"
|
import type { LangParams } from "@/types/params"
|
||||||
|
|
||||||
/* TODO */
|
/* TODO */
|
||||||
export default async function Points({ user, lang }: UserProps & LangParams) {
|
export default async function Points({ user, lang }: UserProps & LangParams) {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { env } from "@/env/server"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import SectionContainer from "@/components/Section/Container"
|
import SectionContainer from "@/components/Section/Container"
|
||||||
@@ -13,19 +14,20 @@ export default async function CurrentRewardsBlock({
|
|||||||
subtitle,
|
subtitle,
|
||||||
link,
|
link,
|
||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const initialCurrentRewards =
|
const rewardsResponse = await serverClient().contentstack.rewards.current()
|
||||||
await serverClient().contentstack.rewards.current({
|
|
||||||
limit: 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!initialCurrentRewards) {
|
if (!rewardsResponse?.rewards.length) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<SectionHeader title={title} link={link} preamble={subtitle} />
|
<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" />
|
<SectionLink link={link} variant="mobile" />
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
)
|
||||||
68
components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts
Normal file
68
components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts
Normal 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)
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
|||||||
import SignupForm from "@/components/Forms/Signup"
|
import SignupForm from "@/components/Forms/Signup"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
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({
|
export default async function SignupFormWrapper({
|
||||||
dynamic_content,
|
dynamic_content,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Grids from "@/components/TempDesignSystem/Grids"
|
|||||||
import StayCard from "../StayCard"
|
import StayCard from "../StayCard"
|
||||||
import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays"
|
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({
|
export default async function SoonestStays({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable"
|
|||||||
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
||||||
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
|
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
|
||||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
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 NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
||||||
import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper"
|
import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper"
|
||||||
import SignUpVerification from "@/components/Blocks/DynamicContent/SignUpVerification"
|
import SignUpVerification from "@/components/Blocks/DynamicContent/SignUpVerification"
|
||||||
|
|||||||
@@ -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"
|
import { IconName } from "@/types/components/icon"
|
||||||
|
|
||||||
export function getUspIconName(icon?: UspIcon | null) {
|
export function getUspIconName(icon?: UspIcon | null) {
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ export default function BookingWidgetClient({
|
|||||||
rooms: defaultRoomsData,
|
rooms: defaultRoomsData,
|
||||||
},
|
},
|
||||||
shouldFocusError: false,
|
shouldFocusError: false,
|
||||||
mode: "all",
|
mode: "onSubmit",
|
||||||
resolver: zodResolver(bookingWidgetSchema),
|
resolver: zodResolver(bookingWidgetSchema),
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onSubmit",
|
||||||
})
|
})
|
||||||
|
|
||||||
function closeMobileSearch() {
|
function closeMobileSearch() {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default async function AmenitiesList({
|
|||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Body color="textMediumContrast">{facility.name}</Body>
|
<Body color="uiTextMediumContrast">{facility.name}</Body>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
|
|||||||
href: `?s=${activities[lang]}`,
|
href: `?s=${activities[lang]}`,
|
||||||
title: activitiesCard.ctaText,
|
title: activitiesCard.ctaText,
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
|
scrollOnClick: false,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
secondaryButton: hasImage
|
secondaryButton: hasImage
|
||||||
@@ -31,6 +32,7 @@ export default function ActivitiesCardGrid(activitiesCard: ActivityCard) {
|
|||||||
href: `?s=${activities[lang]}`,
|
href: `?s=${activities[lang]}`,
|
||||||
title: activitiesCard.ctaText,
|
title: activitiesCard.ctaText,
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
|
scrollOnClick: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default async function IntroSection({
|
|||||||
</BiroScript>
|
</BiroScript>
|
||||||
<Title level="h2">{hotelName}</Title>
|
<Title level="h2">{hotelName}</Title>
|
||||||
</div>
|
</div>
|
||||||
<Body color="textMediumContrast">{formattedLocationText}</Body>
|
<Body color="uiTextMediumContrast">{formattedLocationText}</Body>
|
||||||
{hasTripAdvisorData && (
|
{hasTripAdvisorData && (
|
||||||
<Link
|
<Link
|
||||||
className={styles.introLink}
|
className={styles.introLink}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import type {
|
||||||
HotelAddress,
|
HotelAddress,
|
||||||
HotelData,
|
HotelData,
|
||||||
HotelLocation,
|
HotelLocation,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { PropsWithChildren, useRef } from "react"
|
import { type PropsWithChildren, useRef } from "react"
|
||||||
|
|
||||||
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
import { StickyElementNameEnum } from "@/stores/sticky-position"
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default function PreviewImages({
|
|||||||
title={image.metaData.title}
|
title={image.metaData.title}
|
||||||
width={index === 0 ? 752 : 292}
|
width={index === 0 ? 752 : 292}
|
||||||
height={index === 0 ? 540 : 266}
|
height={index === 0 ? 540 : 266}
|
||||||
|
onClick={() => setLightboxIsOpen(true)}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 30vh;
|
max-height: 30vh;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageWrapper > :nth-child(2),
|
.imageWrapper > :nth-child(2),
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useRef, useState } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import SectionContainer from "@/components/Section/Container"
|
import SectionContainer from "@/components/Section/Container"
|
||||||
import SectionHeader from "@/components/Section/Header"
|
|
||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
import Grids from "@/components/TempDesignSystem/Grids"
|
||||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import { RoomCard } from "./RoomCard"
|
import { RoomCard } from "./RoomCard"
|
||||||
|
|
||||||
@@ -35,19 +35,19 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
className={styles.roomsContainer}
|
className={styles.roomsContainer}
|
||||||
>
|
>
|
||||||
<div ref={scrollRef} className={styles.scrollRef}></div>
|
<div ref={scrollRef} className={styles.scrollRef}></div>
|
||||||
<SectionHeader
|
<Title as="h3" level="h2">
|
||||||
textTransform="capitalize"
|
{intl.formatMessage({ id: "Rooms" })}
|
||||||
title={intl.formatMessage({ id: "Rooms" })}
|
</Title>
|
||||||
preamble={null}
|
|
||||||
/>
|
|
||||||
<Grids.Stackable
|
<Grids.Stackable
|
||||||
className={`${styles.grid} ${allRoomsVisible ? styles.allVisible : ""}`}
|
className={`${styles.grid} ${allRoomsVisible ? styles.allVisible : ""}`}
|
||||||
>
|
>
|
||||||
{rooms.map((room) => (
|
{rooms
|
||||||
<div key={room.id}>
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
<RoomCard room={room} />
|
.map((room) => (
|
||||||
</div>
|
<div key={room.id}>
|
||||||
))}
|
<RoomCard room={room} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</Grids.Stackable>
|
</Grids.Stackable>
|
||||||
|
|
||||||
{showToggleButton ? (
|
{showToggleButton ? (
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default async function AboutTheHotelSidePeek({
|
|||||||
<Divider color="baseSurfaceSubtleHover" />
|
<Divider color="baseSurfaceSubtleHover" />
|
||||||
<Preamble>{descriptions.descriptions.medium}</Preamble>
|
<Preamble>{descriptions.descriptions.medium}</Preamble>
|
||||||
<Body>{descriptions.facilityInformation}</Body>
|
<Body>{descriptions.facilityInformation}</Body>
|
||||||
|
<Body>{descriptions.surroundingInformation}</Body>
|
||||||
</section>
|
</section>
|
||||||
</SidePeek>
|
</SidePeek>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default async function AccessibilityAmenity({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Accessibility" })}
|
title={intl.formatMessage({ id: "Accessibility" })}
|
||||||
icon={IconName.Accessibility}
|
icon={IconName.Accessibility}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{accessibility?.description && (
|
{accessibility?.description && (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default async function BreakfastAmenity() {
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Breakfast" })}
|
title={intl.formatMessage({ id: "Breakfast" })}
|
||||||
icon={IconName.CoffeeAlt}
|
icon={IconName.CoffeeAlt}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
{/* TODO: breakfast to be implemented */}
|
{/* TODO: breakfast to be implemented */}
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default async function CheckInAmenity({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={`${intl.formatMessage({ id: "Check-in" })}/${intl.formatMessage({ id: "Check-out" })}`}
|
title={`${intl.formatMessage({ id: "Check-in" })}/${intl.formatMessage({ id: "Check-out" })}`}
|
||||||
icon={IconName.Business}
|
icon={IconName.Business}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<Body textTransform="bold">{intl.formatMessage({ id: "Times" })}</Body>
|
<Body textTransform="bold">{intl.formatMessage({ id: "Times" })}</Body>
|
||||||
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check in from" })}: ${checkInTime}`}</Body>
|
<Body color="uiTextHighContrast">{`${intl.formatMessage({ id: "Check in from" })}: ${checkInTime}`}</Body>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default async function ParkingAmenity({
|
|||||||
<AccordionItem
|
<AccordionItem
|
||||||
title={intl.formatMessage({ id: "Parking" })}
|
title={intl.formatMessage({ id: "Parking" })}
|
||||||
icon={IconName.Parking}
|
icon={IconName.Parking}
|
||||||
|
variant="sidepeek"
|
||||||
>
|
>
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{parking.map((data) => (
|
{parking.map((data) => (
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
padding: var(--Spacing-x1);
|
padding: var(--Spacing-x1) var(--Spacing-x0);
|
||||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.amenity {
|
.amenity {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HeartIcon } from "@/components/Icons"
|
import { HeartIcon } from "@/components/Icons"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
import { mapFacilityToIcon } from "../../../data"
|
import { mapFacilityToIcon } from "../../../data"
|
||||||
|
|
||||||
@@ -15,16 +15,18 @@ export default function FilteredAmenities({
|
|||||||
{filteredAmenities?.map((amenity) => {
|
{filteredAmenities?.map((amenity) => {
|
||||||
const Icon = mapFacilityToIcon(amenity.id)
|
const Icon = mapFacilityToIcon(amenity.id)
|
||||||
return (
|
return (
|
||||||
<div key={amenity.name} className={styles.wrapper}>
|
<li key={amenity.name} className={styles.wrapper}>
|
||||||
<div className={styles.amenity}>
|
<div className={styles.amenity}>
|
||||||
{Icon ? (
|
{Icon ? (
|
||||||
<Icon color="burgundy" width={24} height={24} />
|
<Icon color="burgundy" width={24} height={24} />
|
||||||
) : (
|
) : (
|
||||||
<HeartIcon 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>
|
||||||
</div>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { getIntl } from "@/i18n"
|
|||||||
|
|
||||||
import styles from "./facility.module.css"
|
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) {
|
export default async function Facility({ data }: FacilityProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 0 var(--Spacing-x2);
|
padding: 0 var(--Spacing-x2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
@@ -26,5 +27,6 @@
|
|||||||
.tabsContainer {
|
.tabsContainer {
|
||||||
padding: 0 var(--Spacing-x5);
|
padding: 0 var(--Spacing-x5);
|
||||||
max-width: calc(100% - var(--hotel-page-map-desktop-width));
|
max-width: calc(100% - var(--hotel-page-map-desktop-width));
|
||||||
|
overflow-x: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { FC } from "react"
|
|
||||||
|
|
||||||
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
|
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"
|
import { FacilityEnum } from "@/types/enums/facilities"
|
||||||
|
|
||||||
const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
const facilityToIconMap: Record<FacilityEnum, IconName> = {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
|
|||||||
const roomCategories =
|
const roomCategories =
|
||||||
hotelData.included?.filter((item) => item.type === "roomcategories") || []
|
hotelData.included?.filter((item) => item.type === "roomcategories") || []
|
||||||
const images = gallery?.smallerImages
|
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 activitiesCard = content?.[0]?.upcoming_activities_card || null
|
||||||
|
|
||||||
const facilities: Facility[] = [
|
const facilities: Facility[] = [
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { staticPageVariants } from "./variants"
|
|
||||||
|
|
||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import type { TrackingSDKPageData } from "@/types/components/tracking"
|
import type { TrackingSDKPageData } from "@/types/components/tracking"
|
||||||
import type { CollectionPage } from "@/types/trpc/routers/contentstack/collectionPage"
|
import type { CollectionPage } from "@/types/trpc/routers/contentstack/collectionPage"
|
||||||
import type { ContentPage } from "@/types/trpc/routers/contentstack/contentPage"
|
import type { ContentPage } from "@/types/trpc/routers/contentstack/contentPage"
|
||||||
|
import type { staticPageVariants } from "./variants"
|
||||||
|
|
||||||
export interface StaticPageProps
|
export interface StaticPageProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "content">,
|
extends Omit<React.HTMLAttributes<HTMLDivElement>, "content">,
|
||||||
|
|||||||
34
components/Countdown/index.tsx
Normal file
34
components/Countdown/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@ import { cva } from "class-variance-authority"
|
|||||||
|
|
||||||
import styles from "./list.module.css"
|
import styles from "./list.module.css"
|
||||||
|
|
||||||
import type { ListItem } from "@/types/requests/blocks/list"
|
import { BlockListItemsEnum, type ListItem } from "@/types/requests/blocks/list"
|
||||||
import { BlockListItemsEnum } from "@/types/requests/blocks/list"
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
variants: {
|
variants: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useCallback, useEffect, useRef, useState } from "react"
|
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 Link from "@/components/TempDesignSystem/Link"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
import { Lang, languages } from "@/constants/languages"
|
import { type Lang, languages } from "@/constants/languages"
|
||||||
|
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Desktop from "./Desktop"
|
import Desktop from "./Desktop"
|
||||||
import Mobile from "./Mobile"
|
import Mobile from "./Mobile"
|
||||||
|
|
||||||
import { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
|
import type { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
|
||||||
|
|
||||||
type LanguageSwitcherProps = { urls: LanguageSwitcherData }
|
type LanguageSwitcherProps = { urls: LanguageSwitcherData }
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Fragment } from "react"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { logout } from "@/constants/routes/handleAuth"
|
import { logout } from "@/constants/routes/handleAuth"
|
||||||
import { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
|
|
||||||
import useDropdownStore from "@/stores/main-menu"
|
import useDropdownStore from "@/stores/main-menu"
|
||||||
|
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
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 styles from "./my-pages-mobile-dropdown.module.css"
|
||||||
|
|
||||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||||
|
import type { navigationQueryRouter } from "@/server/routers/contentstack/myPages/navigation/query"
|
||||||
|
|
||||||
type Navigation = Awaited<ReturnType<(typeof navigationQueryRouter)["get"]>>
|
type Navigation = Awaited<ReturnType<(typeof navigationQueryRouter)["get"]>>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
type Texts = {
|
type Texts = {
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { usePathname, useSearchParams } from "next/navigation"
|
import { usePathname, useSearchParams } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
SiteSectionObject,
|
SiteSectionObject,
|
||||||
TrackingData,
|
TrackingData,
|
||||||
TrackingProps,
|
TrackingProps,
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
|
|||||||
import { EmbedEnum } from "@/types/requests/utils/embeds"
|
import { EmbedEnum } from "@/types/requests/utils/embeds"
|
||||||
import type { Attributes } from "@/types/rte/attrs"
|
import type { Attributes } from "@/types/rte/attrs"
|
||||||
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/rte/enums"
|
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/rte/enums"
|
||||||
import type {
|
import {
|
||||||
RTEDefaultNode,
|
type RTEDefaultNode,
|
||||||
RTENext,
|
RTEMarkType,
|
||||||
RTENode,
|
type RTENext,
|
||||||
RTERegularNode,
|
type RTENode,
|
||||||
|
type RTERegularNode,
|
||||||
} from "@/types/rte/node"
|
} from "@/types/rte/node"
|
||||||
import { RTEMarkType } from "@/types/rte/node"
|
|
||||||
import type { RenderOptions } from "@/types/rte/option"
|
import type { RenderOptions } from "@/types/rte/option"
|
||||||
|
|
||||||
function extractPossibleAttributes(attrs: Attributes | undefined) {
|
function extractPossibleAttributes(attrs: Attributes | undefined) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { hasAvailableParagraphFormat, hasAvailableULFormat } from "./utils"
|
|||||||
import styles from "./jsontohtml.module.css"
|
import styles from "./jsontohtml.module.css"
|
||||||
|
|
||||||
import type { EmbedByUid } from "@/types/components/deprecatedjsontohtml"
|
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 { EmbedEnum } from "@/types/requests/utils/embeds"
|
||||||
import type { Attributes, RTEImageVaultAttrs } from "@/types/rte/attrs"
|
import type { Attributes, RTEImageVaultAttrs } from "@/types/rte/attrs"
|
||||||
import {
|
import {
|
||||||
@@ -25,15 +25,15 @@ import {
|
|||||||
RTEItemTypeEnum,
|
RTEItemTypeEnum,
|
||||||
RTETypeEnum,
|
RTETypeEnum,
|
||||||
} from "@/types/rte/enums"
|
} from "@/types/rte/enums"
|
||||||
import type {
|
import {
|
||||||
RTEDefaultNode,
|
type RTEDefaultNode,
|
||||||
RTEImageNode,
|
type RTEImageNode,
|
||||||
RTENext,
|
RTEMarkType,
|
||||||
RTENode,
|
type RTENext,
|
||||||
RTERegularNode,
|
type RTENode,
|
||||||
RTETextNode,
|
type RTERegularNode,
|
||||||
|
type RTETextNode,
|
||||||
} from "@/types/rte/node"
|
} from "@/types/rte/node"
|
||||||
import { RTEMarkType } from "@/types/rte/node"
|
|
||||||
import type { RenderOptions } from "@/types/rte/option"
|
import type { RenderOptions } from "@/types/rte/option"
|
||||||
|
|
||||||
function extractPossibleAttributes(attrs: Attributes | undefined) {
|
function extractPossibleAttributes(attrs: Attributes | undefined) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user