diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index 08fa09422..d96f10f38 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -2,9 +2,10 @@ import { notFound } from "next/navigation" import { env } from "@/env/server" -import ContentPage from "@/components/ContentType/ContentPage" import HotelPage from "@/components/ContentType/HotelPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage" +import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage" +import ContentPage from "@/components/ContentType/StaticPages/ContentPage" import { setLang } from "@/i18n/serverContext" import { @@ -22,6 +23,11 @@ export default function ContentTypePage({ setLang(params.lang) switch (params.contentType) { + case "collection-page": + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return case "content-page": if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index ef7e818c4..c4a682da9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -45,8 +45,8 @@ export default async function BookingConfirmationPage({ } ) - const fromDate = dt(booking.temp.fromDate).locale(params.lang) - const toDate = dt(booking.temp.toDate).locale(params.lang) + const fromDate = dt(booking.checkInDate).locale(params.lang) + const toDate = dt(booking.checkOutDate).locale(params.lang) const nights = intl.formatMessage( { id: "booking.nights" }, { @@ -77,7 +77,7 @@ export default async function BookingConfirmationPage({ textTransform="regular" type="h1" > - {booking.hotel.name} + {booking.hotel?.data.attributes.name} @@ -91,7 +91,7 @@ export default async function BookingConfirmationPage({ {intl.formatMessage( { id: "Reference #{bookingNr}" }, - { bookingNr: "A92320VV" } + { bookingNr: booking.confirmationNumber } )} @@ -183,11 +183,13 @@ export default async function BookingConfirmationPage({
- {booking.hotel.name} + {booking.hotel?.data.attributes.name} - {booking.hotel.email} - {booking.hotel.phoneNumber} + {booking.hotel?.data.attributes.contactInformation.email} + + + {booking.hotel?.data.attributes.contactInformation.phoneNumber}
@@ -219,7 +221,16 @@ export default async function BookingConfirmationPage({ {intl.formatMessage({ id: "Total cost" })} - {booking.temp.total} + + {" "} + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(booking.totalPrice), + currency: booking.currencyCode, + } + )} + {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/[...paths]/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx new file mode 100644 index 000000000..a73eb305e --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx @@ -0,0 +1,25 @@ +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import SidePeek from "@/components/HotelReservation/SidePeek" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelSidePeek({ + params, + searchParams, +}: PageArgs) { + const search = new URLSearchParams(searchParams) + const { hotel: hotelId } = getQueryParamsForEnterDetails(search) + + if (!hotelId) { + return + } + + const hotel = await getHotelData({ + hotelId: hotelId, + language: params.lang, + }) + + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx index 58a216006..75101475a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx @@ -14,7 +14,10 @@ export default async function HotelHeader({ if (!searchParams.hotel) { redirect(home) } - const hotel = await getHotelData(searchParams.hotel, params.lang) + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) if (!hotel?.data) { redirect(home) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx deleted file mode 100644 index 13b770699..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@sidePeek/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { redirect } from "next/navigation" - -import { getHotelData } from "@/lib/trpc/memoizedRequests" - -import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" - -import type { LangParams, PageArgs } from "@/types/params" - -export default async function HotelSidePeek({ - params, - searchParams, -}: PageArgs) { - if (!searchParams.hotel) { - redirect(`/${params.lang}`) - } - const hotel = await getHotelData(searchParams.hotel, params.lang) - if (!hotel?.data) { - redirect(`/${params.lang}`) - } - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx new file mode 100644 index 000000000..a40136c28 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -0,0 +1,75 @@ +import { + getProfileSafely, + getSelectedRoomAvailability, +} from "@/lib/trpc/memoizedRequests" + +import Summary from "@/components/HotelReservation/EnterDetails/Summary" +import { + generateChildrenString, + getQueryParamsForEnterDetails, + mapChildrenFromString, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" + +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { LangParams, PageArgs, SearchParams } from "@/types/params" + +export default async function SummaryPage({ + searchParams, +}: PageArgs>) { + const selectRoomParams = new URLSearchParams(searchParams) + const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = + getQueryParamsForEnterDetails(selectRoomParams) + + const availability = await getSelectedRoomAvailability({ + hotelId: hotel, + adults, + children: children ? generateChildrenString(children) : undefined, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + const user = await getProfileSafely() + + if (!availability) { + console.error("No hotel or availability data", availability) + // TODO: handle this case + return null + } + + const prices = user + ? { + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, + } + : { + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, + } + + return ( + + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts new file mode 100644 index 000000000..6013a49cc --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts @@ -0,0 +1,9 @@ +import { + getCreditCardsSafely, + getProfileSafely, +} from "@/lib/trpc/memoizedRequests" + +export function preload() { + void getProfileSafely() + void getCreditCardsSafely() +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css index 4f337ccb2..296eea04d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css @@ -1,5 +1,4 @@ .layout { - min-height: 100dvh; background-color: var(--Scandic-Brand-Warm-White); } @@ -9,7 +8,6 @@ grid-template-columns: 1fr 340px; grid-template-rows: auto 1fr; margin: var(--Spacing-x5) auto 0; - padding-top: var(--Spacing-x6); /* simulates padding on viewport smaller than --max-width-navigation */ width: min( calc(100dvw - (var(--Spacing-x2) * 2)), @@ -17,8 +15,81 @@ ); } -.summary { - align-self: flex-start; +.summaryContainer { grid-column: 2 / 3; grid-row: 1/-1; } + +.summary { + background-color: var(--Main-Grey-White); + + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-width: 1px; + border-radius: var(--Corner-radius-Large); + + z-index: 1; +} + +.hider { + display: none; +} + +.shadow { + display: none; +} + +@media screen and (min-width: 950px) { + .summaryContainer { + display: grid; + grid-template-rows: auto auto 1fr; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .summary { + position: sticky; + top: calc( + var(--booking-widget-desktop-height) + + var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half) + ); + margin-top: calc(0px - var(--Spacing-x9)); + border-bottom: none; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + } + + .hider { + display: block; + background-color: var(--Scandic-Brand-Warm-White); + position: sticky; + margin-top: var(--Spacing-x4); + top: calc( + var(--booking-widget-desktop-height) + + var(--booking-widget-desktop-height) - 6px + ); + height: 40px; + } + + .shadow { + display: block; + background-color: var(--Main-Grey-White); + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-top: none; + border-bottom: none; + } +} + +@media screen and (min-width: 1367px) { + .summary { + top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + + var(--Spacing-x-half) + ); + } + + .hider { + top: calc(var(--booking-widget-desktop-height) - 6px); + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 271d19e6d..3e277f4a0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,40 +1,46 @@ +import { + getCreditCardsSafely, + getProfileSafely, +} from "@/lib/trpc/memoizedRequests" + import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" -import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" -import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { setLang } from "@/i18n/serverContext" -import { preload } from "./page" +import { preload } from "./_preload" import styles from "./layout.module.css" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" export default async function StepLayout({ + summary, children, hotelHeader, params, - sidePeek, }: React.PropsWithChildren< LayoutArgs & { hotelHeader: React.ReactNode - sidePeek: React.ReactNode + summary: React.ReactNode } >) { setLang(params.lang) preload() + + const user = await getProfileSafely() + return ( - +
{hotelHeader}
- {children} -
- {sidePeek}
) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 264f5b04d..7b8d2da86 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,11 +1,11 @@ -import { notFound, redirect } from "next/navigation" +import { notFound } from "next/navigation" import { getBreakfastPackages, getCreditCardsSafely, getHotelData, getProfileSafely, - getRoomAvailability, + getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" import BedType from "@/components/HotelReservation/EnterDetails/BedType" @@ -14,16 +14,17 @@ import Details from "@/components/HotelReservation/EnterDetails/Details" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" +import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import { + generateChildrenString, + getQueryParamsForEnterDetails, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" -import { StepEnum } from "@/types/components/enterDetails/step" +import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { LangParams, PageArgs } from "@/types/params" -export function preload() { - void getProfileSafely() - void getCreditCardsSafely() -} - function isValidStep(step: string): step is StepEnum { return Object.values(StepEnum).includes(step as StepEnum) } @@ -31,34 +32,53 @@ function isValidStep(step: string): step is StepEnum { export default async function StepPage({ params, searchParams, -}: PageArgs) { - if (!searchParams.hotel) { - redirect(`/${params.lang}`) - } - void getBreakfastPackages(searchParams.hotel) - void getRoomAvailability({ - hotelId: searchParams.hotel, - adults: Number(searchParams.adults), - roomStayStartDate: searchParams.checkIn, - roomStayEndDate: searchParams.checkOut, - }) +}: PageArgs) { + const { lang } = params const intl = await getIntl() + const selectRoomParams = new URLSearchParams(searchParams) + const { + hotel: hotelId, + adults, + children, + roomTypeCode, + rateCode, + fromDate, + toDate, + } = getQueryParamsForEnterDetails(selectRoomParams) - const hotel = await getHotelData(searchParams.hotel, params.lang) - const user = await getProfileSafely() - const savedCreditCards = await getCreditCardsSafely() - const breakfastPackages = await getBreakfastPackages(searchParams.hotel) + const childrenAsString = children && generateChildrenString(children) - const roomAvailability = await getRoomAvailability({ - hotelId: searchParams.hotel, - adults: Number(searchParams.adults), - roomStayStartDate: searchParams.checkIn, - roomStayEndDate: searchParams.checkOut, - rateCode: searchParams.rateCode, + const breakfastInput = { adults, fromDate, hotelId, toDate } + void getBreakfastPackages(breakfastInput) + void getSelectedRoomAvailability({ + hotelId, + adults, + children: childrenAsString, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, }) - if (!isValidStep(params.step) || !hotel || !roomAvailability) { + const hotelData = await getHotelData({ + hotelId, + language: lang, + }) + const roomAvailability = await getSelectedRoomAvailability({ + hotelId, + adults, + children: childrenAsString, + roomStayStartDate: fromDate, + roomStayEndDate: toDate, + rateCode, + roomTypeCode, + }) + const breakfastPackages = await getBreakfastPackages(breakfastInput) + const user = await getProfileSafely() + const savedCreditCards = await getCreditCardsSafely() + + if (!isValidStep(params.step) || !hotelData || !roomAvailability) { return notFound() } @@ -80,13 +100,20 @@ export default async function StepPage({ return (
- - - + + + + {/* TODO: How to handle no beds found? */} + {roomAvailability.bedTypes ? ( + + + + ) : null} + >) { + sidePeek, +}: React.PropsWithChildren> & { + sidePeek: React.ReactNode +}) { if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() } - return
{children}
+ return ( +
+ {children} + {sidePeek} +
+ ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx new file mode 100644 index 000000000..667d43ad6 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -0,0 +1,75 @@ +import { notFound } from "next/navigation" + +import { env } from "@/env/server" +import { getLocations } from "@/lib/trpc/memoizedRequests" + +import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" +import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { MapModal } from "@/components/MapModal" +import { setLang } from "@/i18n/serverContext" + +import { + fetchAvailableHotels, + generateChildrenString, + getCentralCoordinates, + getPointOfInterests, +} from "../../utils" + +import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" +import type { LangParams, PageArgs } from "@/types/params" + +export default async function SelectHotelMapPage({ + params, + searchParams, +}: PageArgs) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + + 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 googleMapId = env.GOOGLE_DYNAMIC_MAP_ID + const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY + + const selectHotelParams = new URLSearchParams(searchParams) + const selectHotelParamsObject = + getHotelReservationQueryParams(selectHotelParams) + const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selectHotelParamsObject.room[0].child + ? generateChildrenString(selectHotelParamsObject.room[0].child) + : undefined // TODO: Handle multiple rooms + + const hotels = await fetchAvailableHotels({ + cityId: city.id, + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults, + children, + }) + + const pointOfInterests = getPointOfInterests(hotels) + + const centralCoordinates = getCentralCoordinates(pointOfInterests) + + return ( + + + + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx new file mode 100644 index 000000000..86b9e9a38 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css new file mode 100644 index 000000000..b86e58a72 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.module.css @@ -0,0 +1,5 @@ +.layout { + min-height: 100dvh; + background-color: var(--Base-Background-Primary-Normal); + position: relative; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx new file mode 100644 index 000000000..ab5f62674 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/layout.tsx @@ -0,0 +1,24 @@ +import { notFound } from "next/navigation" + +import { env } from "@/env/server" + +import styles from "./layout.module.css" + +import { LangParams, LayoutArgs } from "@/types/params" + +export default function HotelReservationLayout({ + children, + modal, +}: React.PropsWithChildren< + LayoutArgs & { modal: React.ReactNode } +>) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return ( +
+ {children} + {modal} +
+ ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css index ec9b9ce4f..d0a692d8b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css @@ -2,5 +2,5 @@ display: grid; background-color: var(--Scandic-Brand-Warm-White); min-height: 100dvh; - grid-template-columns: 420px 1fr; + position: relative; } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 8dca8f4c7..bfd164880 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -1,58 +1 @@ -import { env } from "@/env/server" - -import { - fetchAvailableHotels, - getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" -import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" -import { setLang } from "@/i18n/serverContext" - -import styles from "./page.module.css" - -import { - PointOfInterest, - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" -import { LangParams, PageArgs } from "@/types/params" - -export default async function SelectHotelMapPage({ - params, -}: PageArgs) { - const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID - const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY - setLang(params.lang) - - const hotels = await fetchAvailableHotels({ - cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, - }) - - const filters = getFiltersFromHotels(hotels) - - // TODO: this is just a quick transformation to get something there. May need rework - const pointOfInterests: PointOfInterest[] = hotels.map((hotel) => ({ - coordinates: { - lat: hotel.hotelData.location.latitude, - lng: hotel.hotelData.location.longitude, - }, - name: hotel.hotelData.name, - distance: hotel.hotelData.location.distanceToCentre, - categoryName: PointOfInterestCategoryNameEnum.HOTEL, - group: PointOfInterestGroupEnum.LOCATION, - })) - - return ( -
- -
- ) -} +export { default } from "../@modal/(.)map/page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index acc942e21..800ffe8f9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -1,6 +1,6 @@ .main { display: flex; - gap: var(--Spacing-x4); + gap: var(--Spacing-x3); padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4); background-color: var(--Scandic-Brand-Warm-White); min-height: 100dvh; @@ -19,8 +19,28 @@ padding: var(--Spacing-x2) var(--Spacing-x0); } +.mapContainer { + display: none; +} + +.buttonContainer { + display: flex; + gap: var(--Spacing-x2); + margin-bottom: var(--Spacing-x3); +} + +.button { + flex: 1; +} + @media (min-width: 768px) { + .mapContainer { + display: block; + } .main { flex-direction: row; } + .buttonContainer { + display: none; + } } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 79b1055d0..65a1dae45 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -7,12 +7,15 @@ import { getLocations } from "@/lib/trpc/memoizedRequests" import { fetchAvailableHotels, - generateChildrenString, getFiltersFromHotels, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer" +import { + generateChildrenString, + getHotelReservationQueryParams, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Link from "@/components/TempDesignSystem/Link" @@ -95,25 +98,28 @@ export default async function SelectHotelPage({ return (
- - - - - {intl.formatMessage({ id: "Show map" })} - - +
+ + + + + {intl.formatMessage({ id: "Show map" })} + + +
+
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index a6a48e12f..c3dc16c49 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -1,12 +1,18 @@ +import { getHotelData } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" import { BedTypeEnum } from "@/types/components/bookingWidget/enums" -import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" -import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" -import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -import { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import type { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import { + type PointOfInterest, + PointOfInterestCategoryNameEnum, + PointOfInterestGroupEnum, +} from "@/types/hotel" export async function fetchAvailableHotels( input: AvailabilityInput @@ -17,7 +23,7 @@ export async function fetchAvailableHotels( const language = getLang() const hotels = availableHotels.availability.map(async (hotel) => { - const hotelData = await serverClient().hotel.hotelData.get({ + const hotelData = await getHotelData({ hotelId: hotel.hotelId.toString(), language, }) @@ -59,3 +65,33 @@ export function generateChildrenString(children: Child[]): string { }) .join(",")}]` } + +export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] { + // TODO: this is just a quick transformation to get something there. May need rework + return hotels.map((hotel) => ({ + coordinates: { + lat: hotel.hotelData.location.latitude, + lng: hotel.hotelData.location.longitude, + }, + name: hotel.hotelData.name, + distance: hotel.hotelData.location.distanceToCentre, + categoryName: PointOfInterestCategoryNameEnum.HOTEL, + group: PointOfInterestGroupEnum.LOCATION, + })) +} + +export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) { + const centralCoordinates = pointOfInterests.reduce( + (acc, poi) => { + acc.lat += poi.coordinates.lat + acc.lng += poi.coordinates.lng + return acc + }, + { lat: 0, lng: 0 } + ) + + centralCoordinates.lat /= pointOfInterests.length + centralCoordinates.lng /= pointOfInterests.length + + return centralCoordinates +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 35a940f0b..816c937ae 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,15 +1,21 @@ import { notFound } from "next/navigation" -import { getProfileSafely } from "@/lib/trpc/memoizedRequests" +import { dt } from "@/lib/dt" +import { + getHotelData, + getLocations, + getProfileSafely, +} from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import Rooms from "@/components/HotelReservation/SelectRate/Rooms" -import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { + generateChildrenString, + getHotelReservationQueryParams, +} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" -import { generateChildrenString } from "../select-hotel/utils" - import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs } from "@/types/params" @@ -20,6 +26,17 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) + const locations = await getLocations() + if (!locations || "error" in locations) { + return null + } + const hotel = locations.data.find( + (location) => + "operaId" in location && location.operaId == searchParams.hotel + ) + if (!hotel) { + return notFound() + } const selectRoomParams = new URLSearchParams(searchParams) const selectRoomParamsObject = getHotelReservationQueryParams(selectRoomParams) @@ -28,22 +45,27 @@ export default async function SelectRatePage({ return notFound() } - const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const validFromDate = + searchParams.fromDate && + dt(searchParams.fromDate).isAfter(dt().subtract(1, "day")) + ? searchParams.fromDate + : dt().utc().format("YYYY-MM-DD") + const validToDate = + searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate) + ? searchParams.toDate + : dt().utc().add(1, "day").format("YYYY-MM-DD") + const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms const childrenCount = selectRoomParamsObject.room[0].child?.length const children = selectRoomParamsObject.room[0].child ? generateChildrenString(selectRoomParamsObject.room[0].child) : undefined // TODO: Handle multiple rooms const [hotelData, roomsAvailability, packages, user] = await Promise.all([ - serverClient().hotel.hotelData.get({ - hotelId: searchParams.hotel, - language: params.lang, - include: ["RoomCategories"], - }), + getHotelData({ hotelId: searchParams.hotel, language: params.lang }), serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), - roomStayStartDate: searchParams.fromDate, - roomStayEndDate: searchParams.toDate, + roomStayStartDate: validFromDate, + roomStayEndDate: validToDate, adults, children, }), diff --git a/app/[lang]/(preview)/layout.tsx b/app/[lang]/(preview)/layout.tsx index 25727e7d6..948bcceba 100644 --- a/app/[lang]/(preview)/layout.tsx +++ b/app/[lang]/(preview)/layout.tsx @@ -1,26 +1,29 @@ -import InitLivePreview from "@/components/Current/LivePreview" -import { setLang } from "@/i18n/serverContext" +import "@/app/globals.css" +import "@scandic-hotels/design-system/style.css" -import type { Metadata } from "next" +import TrpcProvider from "@/lib/trpc/Provider" + +import InitLivePreview from "@/components/LivePreview" +import { getIntl } from "@/i18n" +import ServerIntlProvider from "@/i18n/Provider" +import { setLang } from "@/i18n/serverContext" import type { LangParams, LayoutArgs } from "@/types/params" -export const metadata: Metadata = { - description: "New web", - title: "Scandic Hotels", -} - -export default function RootLayout({ +export default async function RootLayout({ children, params, }: React.PropsWithChildren>) { setLang(params.lang) + const { defaultLocale, locale, messages } = await getIntl() return ( - {children} + + {children} + ) diff --git a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx index fbdfa4893..ff954a31d 100644 --- a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx @@ -1,6 +1,14 @@ +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" +import { notFound } from "next/navigation" + +import HotelPage from "@/components/ContentType/HotelPage" +import LoyaltyPage from "@/components/ContentType/LoyaltyPage" +import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage" +import ContentPage from "@/components/ContentType/StaticPages/ContentPage" +import LoadingSpinner from "@/components/LoadingSpinner" import { setLang } from "@/i18n/serverContext" -import { +import type { ContentTypeParams, LangParams, PageArgs, @@ -13,12 +21,32 @@ export default async function PreviewPage({ }: PageArgs) { setLang(params.lang) - return ( -
-

- Preview for {params.contentType}:{params.uid} in {params.lang} with - params

{JSON.stringify(searchParams, null, 2)}
goes here -

-
- ) + try { + ContentstackLivePreview.setConfigFromParams(searchParams) + + if (!searchParams.live_preview) { + return + } + + switch (params.contentType) { + case "content-page": + return + case "loyalty-page": + return + case "collection-page": + return + case "hotel-page": + return + default: + console.log({ PREVIEW: params }) + const type = params.contentType + console.error(`Unsupported content type given: ${type}`) + notFound() + } + } catch (error) { + // TODO: throw 500 + console.error("Error in preview page") + console.error(error) + throw new Error("Something went wrong") + } } diff --git a/app/[lang]/(preview-current)/layout.tsx b/app/[lang]/(preview-current)/layout.tsx index 3e98666c8..e5ff698bb 100644 --- a/app/[lang]/(preview-current)/layout.tsx +++ b/app/[lang]/(preview-current)/layout.tsx @@ -1,6 +1,6 @@ import Footer from "@/components/Current/Footer" import LangPopup from "@/components/Current/LangPopup" -import InitLivePreview from "@/components/Current/LivePreview" +import InitLivePreview from "@/components/LivePreview" import SkipToMainContent from "@/components/SkipToMainContent" import { setLang } from "@/i18n/serverContext" diff --git a/app/[lang]/(preview-current)/preview-current/page.tsx b/app/[lang]/(preview-current)/preview-current/page.tsx index f8ca81201..82c1e9c45 100644 --- a/app/[lang]/(preview-current)/preview-current/page.tsx +++ b/app/[lang]/(preview-current)/preview-current/page.tsx @@ -1,4 +1,4 @@ -import ContentstackLivePreview from "@contentstack/live-preview-utils" +import { ContentstackLivePreview } from "@contentstack/live-preview-utils" import { previewRequest } from "@/lib/graphql/previewRequest" import { GetCurrentBlockPage } from "@/lib/graphql/Query/Current/CurrentBlockPage.graphql" diff --git a/app/[lang]/webview/loading.tsx b/app/[lang]/webview/loading.tsx deleted file mode 100644 index c739b6635..000000000 --- a/app/[lang]/webview/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function Loading() { - return -} diff --git a/app/globals.css b/app/globals.css index 539f9827a..4a8b655c1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,5 @@ @font-face { - font-display: swap; + font-display: fallback; font-family: "biro script plus"; font-style: normal; font-weight: 400; @@ -7,7 +7,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "brandon text"; font-weight: 700; src: @@ -16,7 +16,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "brandon text"; font-weight: 900; src: @@ -25,7 +25,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 400; @@ -33,7 +33,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 500; @@ -41,7 +41,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira mono"; font-style: normal; font-weight: 700; @@ -49,7 +49,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 300; @@ -57,7 +57,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 400; @@ -65,7 +65,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 500; @@ -73,7 +73,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 600; @@ -81,7 +81,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 700; @@ -89,7 +89,7 @@ } @font-face { - font-display: swap; + font-display: fallback; font-family: "fira sans"; font-style: normal; font-weight: 900; diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 1c07b73ec..81e0b0eff 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -42,13 +42,17 @@ export default function CardsGrid({ case CardsGridEnum.cards.Card: return ( ) case CardsGridEnum.cards.TeaserCard: diff --git a/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx index e64ddb4ef..fa21a8c83 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx @@ -4,6 +4,7 @@ import { dt } from "@/lib/dt" import Body from "@/components/TempDesignSystem/Text/Body" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { formatNumber } from "@/utils/format" import { getMembership } from "@/utils/user" import type { UserProps } from "@/types/components/myPages/user" @@ -16,9 +17,6 @@ export default async function ExpiringPoints({ user }: UserProps) { // TODO: handle this case? return null } - - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) const d = dt(membership.pointsExpiryDate) const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD" @@ -29,7 +27,7 @@ export default async function ExpiringPoints({ user }: UserProps) { {intl.formatMessage( { id: "spendable points expiring by" }, { - points: formatter.format(membership.pointsToExpire), + points: formatNumber(membership.pointsToExpire), date: d.format(dateFormat), } )} diff --git a/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx b/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx index f19b42444..aa9d320cd 100644 --- a/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/LargeTable/index.tsx @@ -70,7 +70,7 @@ function RewardTableHeader({ name, description }: RewardTableHeaderProps) {
- + <Title as="h4" level="h2" textTransform={"regular"}> {name} diff --git a/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx index b54d9722d..4a49abd04 100644 --- a/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx +++ b/components/Blocks/DynamicContent/OverviewTable/RewardList/Card/index.tsx @@ -19,7 +19,7 @@ export default function RewardCard({
- + <Title as="h4" level="h2" textTransform={"regular"}> {title} diff --git a/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx b/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx index 119324806..e32c3c5ca 100644 --- a/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx +++ b/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx @@ -1,8 +1,7 @@ import { useIntl } from "react-intl" -import { Lang } from "@/constants/languages" - import Body from "@/components/TempDesignSystem/Text/Body" +import { formatNumber } from "@/utils/format" import { awardPointsVariants } from "./awardPointsVariants" @@ -32,12 +31,10 @@ export default function AwardPoints({ variant, }) - // sv hardcoded to force space on thousands - const formatter = new Intl.NumberFormat(Lang.sv) return ( {isCalculated - ? formatter.format(awardPoints) + ? formatNumber(awardPoints) : intl.formatMessage({ id: "Points being calculated" })} ) diff --git a/components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx b/components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx index 7517ed955..cc311048d 100644 --- a/components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx +++ b/components/Blocks/DynamicContent/Rewards/NextLevel/index.tsx @@ -56,7 +56,7 @@ export default async function NextLevelRewardsBlock({ { level: nextLevelRewards.level?.name } )} - + <Title level="h4" as="h4" color="pale" textAlign="center"> {reward.label} diff --git a/components/Blocks/DynamicContent/SignUpVerification/index.tsx b/components/Blocks/DynamicContent/SignUpVerification/index.tsx index 3755b59b4..2b182d4bf 100644 --- a/components/Blocks/DynamicContent/SignUpVerification/index.tsx +++ b/components/Blocks/DynamicContent/SignUpVerification/index.tsx @@ -3,7 +3,7 @@ import { redirect } from "next/navigation" import { overview } from "@/constants/routes/myPages" import { auth } from "@/auth" -import LoginButton from "@/components/Current/Header/LoginButton" +import LoginButton from "@/components/LoginButton" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" diff --git a/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx b/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx index e80cab97c..e4da92974 100644 --- a/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Previous/EmptyPreviousStays/index.tsx @@ -7,7 +7,7 @@ export default async function EmptyPreviousStaysBlock() { const { formatMessage } = await getIntl() return (
- + <Title as="h4" level="h3" color="red" textAlign="center"> {formatMessage({ id: "You have no previous stays.", })} diff --git a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx index 1cd297438..0db81b6ce 100644 --- a/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Soonest/EmptyUpcomingStays/index.tsx @@ -14,7 +14,7 @@ export default async function EmptyUpcomingStaysBlock() { return ( <section className={styles.container}> <div className={styles.titleContainer}> - <Title as="h5" level="h3" color="red" className={styles.title}> + <Title as="h4" level="h3" color="red" className={styles.title}> {formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {formatMessage({ id: "Where should you go next?" })} diff --git a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index 9e63c0f77..59c768156 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -47,7 +47,7 @@ export default function StayCard({ stay }: StayCardProps) { height={240} /> <footer className={styles.footer}> - <Title as="h5" className={styles.hotel} level="h3"> + <Title as="h4" className={styles.hotel} level="h3"> {hotelInformation.hotelName}
diff --git a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx index 1cd297438..0db81b6ce 100644 --- a/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx +++ b/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx @@ -14,7 +14,7 @@ export default async function EmptyUpcomingStaysBlock() { return (
- + <Title as="h4" level="h3" color="red" className={styles.title}> {formatMessage({ id: "You have no upcoming stays." })} <span className={styles.burgundyTitle}> {formatMessage({ id: "Where should you go next?" })} diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 6a594199e..af4bf4c48 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -1,17 +1,18 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { dt } from "@/lib/dt" +import { StickyElementNameEnum } from "@/stores/sticky-position" import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" import { CloseLargeIcon } from "@/components/Icons" +import useStickyPosition from "@/hooks/useStickyPosition" import { debounce } from "@/utils/debounce" import { getFormattedUrlQueryParams } from "@/utils/url" -import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils" import MobileToggleButton from "./MobileToggleButton" import styles from "./bookingWidget.module.css" @@ -29,6 +30,11 @@ export default function BookingWidgetClient({ searchParams, }: BookingWidgetClientProps) { const [isOpen, setIsOpen] = useState(false) + const bookingWidgetRef = useRef(null) + useStickyPosition({ + ref: bookingWidgetRef, + name: StickyElementNameEnum.BOOKING_WIDGET, + }) const sessionStorageSearchData = typeof window !== "undefined" @@ -61,12 +67,14 @@ export default function BookingWidgetClient({ return undefined } + const reqFromDate = bookingWidgetSearchData?.fromDate?.toString() + const reqToDate = bookingWidgetSearchData?.toDate?.toString() + const isDateParamValid = - bookingWidgetSearchData?.fromDate && - bookingWidgetSearchData?.toDate && - dt(bookingWidgetSearchData?.toDate.toString()).isAfter( - dt(bookingWidgetSearchData?.fromDate.toString()) - ) + reqFromDate && + reqToDate && + dt(reqFromDate).isAfter(dt().subtract(1, "day")) && + dt(reqToDate).isAfter(dt(reqFromDate)) const selectedLocation = bookingWidgetSearchData ? getLocationObj( @@ -140,7 +148,10 @@ export default function BookingWidgetClient({ return ( <FormProvider {...methods}> - <section className={styles.container} data-open={isOpen}> + <section ref={bookingWidgetRef} className={styles.containerDesktop}> + <Form locations={locations} type={type} /> + </section> + <section className={styles.containerMobile} data-open={isOpen}> <button className={styles.close} onClick={closeMobileSearch} diff --git a/components/BookingWidget/MobileToggleButton/index.tsx b/components/BookingWidget/MobileToggleButton/index.tsx index a58bdd1b2..80026ff93 100644 --- a/components/BookingWidget/MobileToggleButton/index.tsx +++ b/components/BookingWidget/MobileToggleButton/index.tsx @@ -1,15 +1,17 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import { useWatch } from "react-hook-form" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" +import { StickyElementNameEnum } from "@/stores/sticky-position" import { EditIcon, SearchIcon } from "@/components/Icons" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import useLang from "@/hooks/useLang" +import useStickyPosition from "@/hooks/useStickyPosition" import styles from "./button.module.css" @@ -29,6 +31,12 @@ export default function MobileToggleButton({ const location = useWatch({ name: "location" }) const rooms: BookingWidgetSchema["rooms"] = useWatch({ name: "rooms" }) + const bookingWidgetMobileRef = useRef(null) + useStickyPosition({ + ref: bookingWidgetMobileRef, + name: StickyElementNameEnum.BOOKING_WIDGET_MOBILE, + }) + const parsedLocation: Location | null = location ? JSON.parse(decodeURIComponent(location)) : null @@ -46,70 +54,82 @@ export default function MobileToggleButton({ return null } - if (parsedLocation && d) { - const totalRooms = rooms.length - const totalAdults = rooms.reduce((acc, room) => { - if (room.adults) { - acc = acc + room.adults - } - return acc - }, 0) - const totalChildren = rooms.reduce((acc, room) => { - if (room.child) { - acc = acc + room.child.length - } - return acc - }, 0) - return ( - <div className={styles.complete} onClick={openMobileSearch} role="button"> - <div> - <Caption color="red">{parsedLocation.name}</Caption> - <Caption> - {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( - { id: "booking.nights" }, - { totalNights: nights } - )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${ - totalChildren > 0 - ? intl.formatMessage( - { id: "booking.children" }, - { totalChildren } - ) + ", " - : "" - }${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} - </Caption> - </div> - <div className={styles.icon}> - <EditIcon color="white" /> - </div> - </div> - ) - } + const locationAndDateIsSet = parsedLocation && d + + const totalRooms = rooms.length + const totalAdults = rooms.reduce((acc, room) => { + if (room.adults) { + acc = acc + room.adults + } + return acc + }, 0) + const totalChildren = rooms.reduce((acc, room) => { + if (room.child) { + acc = acc + room.child.length + } + return acc + }, 0) return ( - <div className={styles.partial} onClick={openMobileSearch} role="button"> - <div> - <Caption color="red">{intl.formatMessage({ id: "Where to" })}</Caption> - <Body color="uiTextPlaceholder"> - {parsedLocation - ? parsedLocation.name - : intl.formatMessage({ id: "Destination" })} - </Body> - </div> - <Divider color="baseSurfaceSubtleNormal" variant="vertical" /> - <div> - <Caption color="red"> - {intl.formatMessage( - { id: "booking.nights" }, - { totalNights: nights } - )} - </Caption> - <Body> - {selectedFromDate} - {selectedToDate} - </Body> - </div> - <div className={styles.icon}> - <SearchIcon color="white" /> - </div> + <div + className={locationAndDateIsSet ? styles.complete : styles.partial} + onClick={openMobileSearch} + role="button" + ref={bookingWidgetMobileRef} + > + {!locationAndDateIsSet && ( + <> + <div> + <Caption type="bold" color="red"> + {intl.formatMessage({ id: "Where to" })} + </Caption> + <Body color="uiTextPlaceholder"> + {parsedLocation + ? parsedLocation.name + : intl.formatMessage({ id: "Destination" })} + </Body> + </div> + <Divider color="baseSurfaceSubtleNormal" variant="vertical" /> + <div> + <Caption type="bold" color="red"> + {intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )} + </Caption> + <Body> + {selectedFromDate} - {selectedToDate} + </Body> + </div> + <div className={styles.icon}> + <SearchIcon color="white" /> + </div> + </> + )} + + {locationAndDateIsSet && ( + <> + <div> + <Caption color="red">{parsedLocation?.name}</Caption> + <Caption> + {`${selectedFromDate} - ${selectedToDate} (${intl.formatMessage( + { id: "booking.nights" }, + { totalNights: nights } + )}) ${intl.formatMessage({ id: "booking.adults" }, { totalAdults })}, ${ + totalChildren > 0 + ? intl.formatMessage( + { id: "booking.children" }, + { totalChildren } + ) + ", " + : "" + }${intl.formatMessage({ id: "booking.rooms" }, { totalRooms })}`} + </Caption> + </div> + <div className={styles.icon}> + <EditIcon color="white" /> + </div> + </> + )} </div> ) } diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index d8235bae8..f0dec979d 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -1,5 +1,11 @@ +.containerDesktop, +.containerMobile, +.close { + display: none; +} + @media screen and (max-width: 767px) { - .container { + .containerMobile { background-color: var(--UI-Input-Controls-Surface-Normal); bottom: -100%; display: grid; @@ -14,7 +20,7 @@ border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; } - .container[data-open="true"] { + .containerMobile[data-open="true"] { bottom: 0; } @@ -25,7 +31,7 @@ justify-self: flex-end; } - .container[data-open="true"] + .backdrop { + .containerMobile[data-open="true"] + .backdrop { background-color: rgba(0, 0, 0, 0.4); height: 100%; left: 0; @@ -37,7 +43,7 @@ } @media screen and (min-width: 768px) { - .container { + .containerDesktop { display: block; box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05); position: sticky; @@ -45,10 +51,6 @@ z-index: 10; background-color: var(--Base-Surface-Primary-light-Normal); } - - .close { - display: none; - } } @media screen and (min-width: 1367px) { diff --git a/components/Breadcrumbs/breadcrumbs.module.css b/components/Breadcrumbs/breadcrumbs.module.css index 9523fd4c9..93bedd5a6 100644 --- a/components/Breadcrumbs/breadcrumbs.module.css +++ b/components/Breadcrumbs/breadcrumbs.module.css @@ -19,6 +19,7 @@ .listItem { align-items: center; display: flex; + gap: var(--Spacing-x-quarter); } .homeLink { diff --git a/components/Breadcrumbs/index.tsx b/components/Breadcrumbs/index.tsx index 1939e47fa..adf892011 100644 --- a/components/Breadcrumbs/index.tsx +++ b/components/Breadcrumbs/index.tsx @@ -1,6 +1,6 @@ import { serverClient } from "@/lib/trpc/server" -import { ChevronRightIcon, HouseIcon } from "@/components/Icons" +import { ChevronRightSmallIcon, HouseIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import Footnote from "@/components/TempDesignSystem/Text/Footnote" @@ -24,9 +24,9 @@ export default async function Breadcrumbs() { href={homeBreadcrumb.href!} variant="breadcrumb" > - <HouseIcon color="peach80" /> + <HouseIcon width={16} height={16} color="peach80" /> </Link> - <ChevronRightIcon aria-hidden="true" color="peach80" /> + <ChevronRightSmallIcon aria-hidden="true" color="peach80" /> </li> ) : null} @@ -41,7 +41,7 @@ export default async function Breadcrumbs() { > {breadcrumb.title} </Link> - <ChevronRightIcon aria-hidden="true" color="peach80" /> + <ChevronRightSmallIcon aria-hidden="true" color="peach80" /> </li> ) } diff --git a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css index e35dc13a6..d3e5d9d91 100644 --- a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css +++ b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css @@ -18,6 +18,7 @@ .amenityItem { display: inline-flex; gap: var(--Spacing-x1); + align-items: center; } .icon { diff --git a/components/ContentType/HotelPage/AmenitiesList/index.tsx b/components/ContentType/HotelPage/AmenitiesList/index.tsx index 8c90f19c2..03244ea7a 100644 --- a/components/ContentType/HotelPage/AmenitiesList/index.tsx +++ b/components/ContentType/HotelPage/AmenitiesList/index.tsx @@ -1,7 +1,7 @@ import { amenities } from "@/constants/routes/hotelPageParams" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" -import { ChevronRightIcon } from "@/components/Icons" +import { ChevronRightSmallIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -29,7 +29,12 @@ export default async function AmenitiesList({ return ( <div className={styles.amenityItem} key={facility.id}> {IconComponent && ( - <IconComponent className={styles.icon} color="grey80" /> + <IconComponent + className={styles.icon} + color="grey80" + width={20} + height={20} + /> )} <Body color="textMediumContrast">{facility.name}</Body> </div> @@ -44,7 +49,7 @@ export default async function AmenitiesList({ className={styles.showAllAmenities} > {intl.formatMessage({ id: "Show all amenities" })} - <ChevronRightIcon color="burgundy" /> + <ChevronRightSmallIcon color="burgundy" /> </Link> </section> ) diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index 360d0f8df..010530f74 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -1,7 +1,6 @@ import { about } from "@/constants/routes/hotelPageParams" -import { ChevronRightIcon } from "@/components/Icons" -import ArrowRight from "@/components/Icons/ArrowRight" +import { ChevronRightSmallIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" import Link from "@/components/TempDesignSystem/Link" import BiroScript from "@/components/TempDesignSystem/Text/BiroScript" @@ -48,7 +47,7 @@ export default async function IntroSection({ <div className={styles.mainContent}> <div className={styles.titleContainer}> <BiroScript tilted="medium" color="red"> - {intl.formatMessage({ id: "Welcome to" })}: + {intl.formatMessage({ id: "Welcome to" })} </BiroScript> <Title level="h2">{hotelName}
@@ -77,7 +76,7 @@ export default async function IntroSection({ scroll={false} > {intl.formatMessage({ id: "Read more about the hotel" })} - +
diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx index 5ed761081..db979bf47 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -37,21 +37,26 @@ export default function Sidebar({ function moveToPoi(poiCoordinates: Coordinates) { if (map) { + const hotelLatLng = new google.maps.LatLng( + coordinates.lat, + coordinates.lng + ) + const poiLatLng = new google.maps.LatLng( + poiCoordinates.lat, + poiCoordinates.lng + ) + const bounds = new google.maps.LatLngBounds() - const boundPadding = 0.02 + bounds.extend(hotelLatLng) + bounds.extend(poiLatLng) - const minLat = Math.min(coordinates.lat, poiCoordinates.lat) - const maxLat = Math.max(coordinates.lat, poiCoordinates.lat) - const minLng = Math.min(coordinates.lng, poiCoordinates.lng) - const maxLng = Math.max(coordinates.lng, poiCoordinates.lng) - - bounds.extend( - new google.maps.LatLng(minLat - boundPadding, minLng - boundPadding) - ) - bounds.extend( - new google.maps.LatLng(maxLat + boundPadding, maxLng + boundPadding) - ) map.fitBounds(bounds) + + const currentZoomLevel = map.getZoom() + + if (currentZoomLevel) { + map.setZoom(currentZoomLevel - 1) + } } } @@ -61,12 +66,6 @@ export default function Sidebar({ } } - function handleMouseLeave() { - if (!isClicking) { - onActivePoiChange(null) - } - } - function handlePoiClick(poiName: string, poiCoordinates: Coordinates) { setIsClicking(true) toggleFullScreenSidebar() @@ -78,66 +77,71 @@ export default function Sidebar({ } return ( - + {poisInGroups.map(({ group, pois }) => + pois.length ? ( +
+ +

+ + {intl.formatMessage({ id: group })} +

+ +
    + {pois.map((poi) => ( +
  • + +
  • + ))} +
+
+ ) : null + )} + + +
+ ) } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css index 11f26b822..4bab124e7 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css @@ -1,50 +1,12 @@ .sidebar { - --sidebar-max-width: 26.25rem; - --sidebar-mobile-toggle-height: 91px; - --sidebar-mobile-fullscreen-height: calc( - 100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height) - ); - - position: absolute; - top: var(--sidebar-mobile-fullscreen-height); - height: 100%; - right: 0; - left: 0; background-color: var(--Base-Surface-Primary-light-Normal); - z-index: 1; - transition: top 0.3s; -} - -.sidebar:not(.fullscreen) { - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; -} - -.sidebar.fullscreen { - top: 0; -} - -.sidebarToggle { - position: relative; - margin: var(--Spacing-x4) 0 var(--Spacing-x2); - width: 100%; -} - -.sidebarToggle::before { - content: ""; - position: absolute; - display: block; - top: -0.5rem; - width: 100px; - height: 3px; - background-color: var(--UI-Text-High-contrast); + z-index: 2; } .sidebarContent { display: grid; gap: var(--Spacing-x5); align-content: start; - padding: var(--Spacing-x3) var(--Spacing-x2); - height: var(--sidebar-mobile-fullscreen-height); overflow-y: auto; } @@ -90,12 +52,65 @@ background-color: var(--Base-Surface-Primary-light-Hover); } +@media screen and (max-width: 767px) { + .sidebar { + --sidebar-mobile-toggle-height: 84px; + --sidebar-mobile-top-space: 40px; + --sidebar-mobile-content-height: calc( + var(--hotel-map-height) - var(--sidebar-mobile-toggle-height) - + var(--sidebar-mobile-top-space) + ); + + position: absolute; + bottom: calc(-1 * var(--sidebar-mobile-content-height)); + width: 100%; + transition: + bottom 0.3s, + top 0.3s; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + } + + .sidebar.fullscreen + .backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + z-index: 1; + } + + .sidebar.fullscreen { + bottom: 0; + } + + .sidebarToggle { + position: relative; + margin-top: var(--Spacing-x4); + } + + .sidebarToggle::before { + content: ""; + position: absolute; + display: block; + top: -0.5rem; + width: 100px; + height: 3px; + background-color: var(--UI-Text-High-contrast); + } + + .sidebarContent { + padding: var(--Spacing-x3) var(--Spacing-x2); + height: var(--sidebar-mobile-content-height); + } +} + @media screen and (min-width: 768px) { .sidebar { position: static; width: 40vw; min-width: 10rem; - max-width: var(--sidebar-max-width); + max-width: 26.25rem; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css index 32df5b502..7cdd02371 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css @@ -1,18 +1,19 @@ .dynamicMap { - position: fixed; - top: var(--main-menu-mobile-height); - right: 0; - bottom: 0; + --hotel-map-height: 100dvh; + + position: absolute; + top: 0; left: 0; - z-index: var(--dialog-z-index); + height: var(--hotel-map-height); + width: 100dvw; + z-index: var(--hotel-dynamic-map-z-index); display: flex; background-color: var(--Base-Surface-Primary-light-Normal); } - -@media screen and (min-width: 768px) { - .dynamicMap { - top: var(--main-menu-desktop-height); - } +.wrapper { + position: absolute; + top: 0; + left: 0; } .closeButton { diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index f539d7614..969b24588 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -1,6 +1,6 @@ "use client" import { APIProvider } from "@vis.gl/react-google-maps" -import { useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" @@ -10,6 +10,7 @@ import CloseLargeIcon from "@/components/Icons/CloseLarge" import InteractiveMap from "@/components/Maps/InteractiveMap" import Button from "@/components/TempDesignSystem/Button" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" +import { debounce } from "@/utils/debounce" import Sidebar from "./Sidebar" @@ -25,9 +26,10 @@ export default function DynamicMap({ mapId, }: DynamicMapProps) { const intl = useIntl() + const rootDiv = useRef(null) + const [mapHeight, setMapHeight] = useState("0px") const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) - const hasMounted = useRef(false) const [activePoi, setActivePoi] = useState(null) useHandleKeyUp((event: KeyboardEvent) => { @@ -36,23 +38,47 @@ export default function DynamicMap({ } }) - // Making sure the map is always opened at the top of the page, just below the header. + // Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget) + const handleMapHeight = useCallback(() => { + const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0 + const scrollY = window.scrollY + setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`) + }, []) + + // Making sure the map is always opened at the top of the page, + // just below the header and booking widget as these should stay visible. // When closing, the page should scroll back to the position it was before opening the map. useEffect(() => { // Skip the first render - if (!hasMounted.current) { - hasMounted.current = true + if (!rootDiv.current) { return } if (isDynamicMapOpen && scrollHeightWhenOpened === 0) { - setScrollHeightWhenOpened(window.scrollY) + const scrollY = window.scrollY + setScrollHeightWhenOpened(scrollY) window.scrollTo({ top: 0, behavior: "instant" }) } else if (!isDynamicMapOpen && scrollHeightWhenOpened !== 0) { window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" }) setScrollHeightWhenOpened(0) } - }, [isDynamicMapOpen, scrollHeightWhenOpened]) + }, [isDynamicMapOpen, scrollHeightWhenOpened, rootDiv]) + + useEffect(() => { + const debouncedResizeHandler = debounce(function () { + handleMapHeight() + }) + + const observer = new ResizeObserver(debouncedResizeHandler) + + observer.observe(document.documentElement) + + return () => { + if (observer) { + observer.unobserve(document.documentElement) + } + } + }, [rootDiv, isDynamicMapOpen, handleMapHeight]) const closeButton = (
) } diff --git a/components/ContentType/HotelPage/Map/MapWithCard/index.tsx b/components/ContentType/HotelPage/Map/MapWithCard/index.tsx new file mode 100644 index 000000000..b544e39d3 --- /dev/null +++ b/components/ContentType/HotelPage/Map/MapWithCard/index.tsx @@ -0,0 +1,24 @@ +"use client" + +import { PropsWithChildren, useRef } from "react" + +import { StickyElementNameEnum } from "@/stores/sticky-position" + +import useStickyPosition from "@/hooks/useStickyPosition" + +import styles from "./mapWithCard.module.css" + +export default function MapWithCard({ children }: PropsWithChildren) { + const mapWithCardRef = useRef(null) + useStickyPosition({ + ref: mapWithCardRef, + name: StickyElementNameEnum.HOTEL_STATIC_MAP, + group: "hotelPage", + }) + + return ( +
+ {children} +
+ ) +} diff --git a/components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css b/components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css new file mode 100644 index 000000000..528c5fa42 --- /dev/null +++ b/components/ContentType/HotelPage/Map/MapWithCard/mapWithCard.module.css @@ -0,0 +1,12 @@ +.mapWithCard { + position: sticky; + top: var(--booking-widget-desktop-height); + min-height: 500px; /* Fixed min to not cover the marker with the card */ + height: calc( + 100vh - var(--main-menu-desktop-height) - + var(--booking-widget-desktop-height) + ); /* Full height without the header + booking widget */ + max-height: 935px; /* Fixed max according to figma */ + overflow: hidden; + width: 100%; +} diff --git a/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css b/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css index e5bb3b75b..b2edd97a0 100644 --- a/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css +++ b/components/ContentType/HotelPage/Map/MobileMapToggle/mobileToggle.module.css @@ -1,7 +1,7 @@ .mobileToggle { position: sticky; bottom: var(--Spacing-x5); - z-index: 1; + z-index: var(--hotel-mobile-map-toggle-button-z-index); margin: 0 auto; display: grid; grid-template-columns: repeat(2, 1fr); diff --git a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css index 7a1818557..064d99dce 100644 --- a/components/ContentType/HotelPage/PreviewImages/previewImages.module.css +++ b/components/ContentType/HotelPage/PreviewImages/previewImages.module.css @@ -4,6 +4,7 @@ position: relative; width: 100%; padding: var(--Spacing-x2) var(--Spacing-x2) 0; + z-index: 0; } .image { diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index a6c4748f9..2b0620885 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -2,36 +2,39 @@ import { useIntl } from "react-intl" -import { ChevronRightIcon, ImageIcon } from "@/components/Icons" +import { GalleryIcon } from "@/components/Icons" import Image from "@/components/Image" -import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import RoomDetailsButton from "../RoomDetailsButton" + import styles from "./roomCard.module.css" import type { RoomCardProps } from "@/types/components/hotelPage/room" -export function RoomCard({ - badgeTextTransKey, - id, - images, - subtitle, - title, -}: RoomCardProps) { +export function RoomCard({ hotelId, room }: RoomCardProps) { + const { images, name, roomSize, occupancy, id } = room const intl = useIntl() const mainImage = images[0] + const size = + roomSize?.min === roomSize?.max + ? `${roomSize.min} m²` + : `${roomSize.min} - ${roomSize.max} m²` + + const personLabel = intl.formatMessage( + { id: "hotelPages.rooms.roomCard.persons" }, + { totalOccupancy: occupancy.total } + ) + + const subtitle = `${size} (${personLabel})` + function handleImageClick() { // TODO: Implement opening of a model with carousel console.log("Image clicked: ", id) } - function handleRoomCtaClick() { - // TODO: Implement opening side-peek component with room details - console.log("Room CTA clicked: ", id) - } - return (
+
) diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css index fcec738e4..5270e9882 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css +++ b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css @@ -26,10 +26,11 @@ display: flex; gap: var(--Spacing-x-half); align-items: center; - background-color: var(--Main-Grey-90); + background-color: var(--UI-Grey-90); + opacity: 90%; color: var(--UI-Input-Controls-Fill-Normal); padding: var(--Spacing-x-half) var(--Spacing-x1); - border-radius: var(--Corner-radius-Medium); + border-radius: var(--Corner-radius-Small); } .content { diff --git a/components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx b/components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx new file mode 100644 index 000000000..d68ba8b4a --- /dev/null +++ b/components/ContentType/HotelPage/Rooms/RoomDetailsButton/index.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useIntl } from "react-intl" + +import useSidePeekStore from "@/stores/sidepeek" + +import { ChevronRightSmallIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" +import { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps" + +export default function RoomDetailsButton({ + hotelId, + roomTypeCode, +}: ToggleSidePeekProps) { + const intl = useIntl() + const openSidePeek = useSidePeekStore((state) => state.openSidePeek) + + return ( + + ) +} diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index b7ac43a90..325f60eb6 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -15,34 +15,13 @@ import styles from "./rooms.module.css" import type { RoomsProps } from "@/types/components/hotelPage/room" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" -export function Rooms({ rooms }: RoomsProps) { +export function Rooms({ hotelId, rooms }: RoomsProps) { const intl = useIntl() const showToggleButton = rooms.length > 3 const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton) const scrollRef = useRef(null) - const mappedRooms = rooms - .map((room) => { - const size = `${room.roomSize.min} - ${room.roomSize.max} m²` - const personLabel = - room.occupancy.total === 1 - ? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" }) - : intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) - - const subtitle = `${size} (${room.occupancy.total} ${personLabel})` - - return { - id: room.id, - images: room.images, - title: room.name, - subtitle: subtitle, - sortOrder: room.sortOrder, - popularChoice: null, - } - }) - .sort((a, b) => a.sortOrder - b.sortOrder) - function handleShowMore() { if (scrollRef.current && allRoomsVisible) { scrollRef.current.scrollIntoView({ behavior: "smooth" }) @@ -64,15 +43,9 @@ export function Rooms({ rooms }: RoomsProps) { - {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( -
- + {rooms.map((room) => ( +
+
))} diff --git a/components/ContentType/HotelPage/TabNavigation/index.tsx b/components/ContentType/HotelPage/TabNavigation/index.tsx index 6f8b29130..b2699f461 100644 --- a/components/ContentType/HotelPage/TabNavigation/index.tsx +++ b/components/ContentType/HotelPage/TabNavigation/index.tsx @@ -1,12 +1,15 @@ "use client" import { useRouter } from "next/navigation" -import { useEffect } from "react" +import { useEffect, useRef } from "react" import { useIntl } from "react-intl" +import { StickyElementNameEnum } from "@/stores/sticky-position" + import Link from "@/components/TempDesignSystem/Link" import useHash from "@/hooks/useHash" import useScrollSpy from "@/hooks/useScrollSpy" +import useStickyPosition from "@/hooks/useStickyPosition" import styles from "./tabNavigation.module.css" @@ -23,6 +26,12 @@ export default function TabNavigation({ const hash = useHash() const intl = useIntl() const router = useRouter() + const tabNavigationRef = useRef(null) + useStickyPosition({ + ref: tabNavigationRef, + name: StickyElementNameEnum.HOTEL_TAB_NAVIGATION, + group: "hotelPage", + }) const tabLinks: { hash: HotelHashValues; text: string }[] = [ { @@ -71,7 +80,7 @@ export default function TabNavigation({ }, [activeSectionId, router]) return ( -
+
) : null}
- + {faq.accordions.length > 0 && ( @@ -107,10 +108,10 @@ export default async function HotelPage() { {googleMapsApiKey ? ( <> {/* eslint-enable import/no-named-as-default-member */} +
) } diff --git a/components/ContentType/StaticPages/CollectionPage/index.tsx b/components/ContentType/StaticPages/CollectionPage/index.tsx new file mode 100644 index 000000000..41a06391c --- /dev/null +++ b/components/ContentType/StaticPages/CollectionPage/index.tsx @@ -0,0 +1,22 @@ +import { serverClient } from "@/lib/trpc/server" + +import StaticPage from ".." + +export default async function CollectionPage() { + const collectionPageRes = + await serverClient().contentstack.collectionPage.get() + + if (!collectionPageRes) { + return null + } + + const { tracking, collectionPage } = collectionPageRes + + return ( + + ) +} diff --git a/components/ContentType/StaticPages/ContentPage/index.tsx b/components/ContentType/StaticPages/ContentPage/index.tsx new file mode 100644 index 000000000..037f011b8 --- /dev/null +++ b/components/ContentType/StaticPages/ContentPage/index.tsx @@ -0,0 +1,17 @@ +import { serverClient } from "@/lib/trpc/server" + +import StaticPage from ".." + +export default async function ContentPage() { + const contentPageRes = await serverClient().contentstack.contentPage.get() + + if (!contentPageRes) { + return null + } + + const { tracking, contentPage } = contentPageRes + + return ( + + ) +} diff --git a/components/ContentType/ContentPage/index.tsx b/components/ContentType/StaticPages/index.tsx similarity index 75% rename from components/ContentType/ContentPage/index.tsx rename to components/ContentType/StaticPages/index.tsx index b6a556dc5..4eed6b89b 100644 --- a/components/ContentType/ContentPage/index.tsx +++ b/components/ContentType/StaticPages/index.tsx @@ -1,5 +1,3 @@ -import { serverClient } from "@/lib/trpc/server" - import Blocks from "@/components/Blocks" import Hero from "@/components/Hero" import Sidebar from "@/components/Sidebar" @@ -8,21 +6,22 @@ import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Title from "@/components/TempDesignSystem/Text/Title" import TrackingSDK from "@/components/TrackingSDK" -import styles from "./contentPage.module.css" +import { staticPageVariants } from "./variants" -export default async function ContentPage() { - const contentPageRes = await serverClient().contentstack.contentPage.get() +import styles from "./staticPage.module.css" - if (!contentPageRes) { - return null - } +import type { StaticPageProps } from "./staticPage" - const { tracking, contentPage } = contentPageRes - const { blocks, hero_image, header, sidebar } = contentPage +export default function StaticPage({ + content, + tracking, + pageType, +}: StaticPageProps) { + const { blocks, hero_image, header } = content return ( <> -
+
{header ? ( @@ -54,7 +53,9 @@ export default async function ContentPage() { {blocks ? : null}
- {sidebar?.length ? : null} + {"sidebar" in content && content.sidebar?.length ? ( + + ) : null}
diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/StaticPages/staticPage.module.css similarity index 87% rename from components/ContentType/ContentPage/contentPage.module.css rename to components/ContentType/StaticPages/staticPage.module.css index 825a2445d..13d2f440e 100644 --- a/components/ContentType/ContentPage/contentPage.module.css +++ b/components/ContentType/StaticPages/staticPage.module.css @@ -1,4 +1,4 @@ -.contentPage { +.page { padding-bottom: var(--Spacing-x9); } @@ -32,20 +32,27 @@ } .contentContainer { + padding: var(--Spacing-x4) var(--Spacing-x2) 0; +} + +.content .contentContainer { display: grid; grid-template-areas: "main" "sidebar"; gap: var(--Spacing-x4); align-items: start; - padding: var(--Spacing-x4) var(--Spacing-x2) 0; } .mainContent { - grid-area: main; display: grid; gap: var(--Spacing-x4); width: 100%; + gap: var(--Spacing-x6); +} + +.content .mainContent { + grid-area: main; } @media (min-width: 768px) { @@ -58,12 +65,20 @@ .heroContainer { padding: var(--Spacing-x4) 0; } + .contentContainer { + max-width: var(--max-width-content); + padding: var(--Spacing-x4) 0 0; + margin: 0 auto; + } + + .content .contentContainer { grid-template-areas: "main sidebar"; grid-template-columns: var(--max-width-text-block) 1fr; gap: var(--Spacing-x9); - max-width: var(--max-width-content); - margin: 0 auto; - padding: var(--Spacing-x4) 0 0; + } + + .mainContent { + gap: var(--Spacing-x9); } } diff --git a/components/ContentType/StaticPages/staticPage.ts b/components/ContentType/StaticPages/staticPage.ts new file mode 100644 index 000000000..5a3bf84b4 --- /dev/null +++ b/components/ContentType/StaticPages/staticPage.ts @@ -0,0 +1,15 @@ +import { staticPageVariants } from "./variants" + +import type { VariantProps } from "class-variance-authority" + +import type { TrackingSDKPageData } from "@/types/components/tracking" +import type { CollectionPage } from "@/types/trpc/routers/contentstack/collectionPage" +import type { ContentPage } from "@/types/trpc/routers/contentstack/contentPage" + +export interface StaticPageProps + extends Omit, "content">, + VariantProps { + pageType?: "collection" | "content" + content: CollectionPage["collection_page"] | ContentPage["content_page"] + tracking: TrackingSDKPageData +} diff --git a/components/ContentType/StaticPages/variants.ts b/components/ContentType/StaticPages/variants.ts new file mode 100644 index 000000000..4d59bc641 --- /dev/null +++ b/components/ContentType/StaticPages/variants.ts @@ -0,0 +1,15 @@ +import { cva } from "class-variance-authority" + +import styles from "./staticPage.module.css" + +export const staticPageVariants = cva(styles.page, { + variants: { + pageType: { + collection: styles.collection, + content: styles.content, + }, + }, + defaultVariants: { + pageType: "content", + }, +}) diff --git a/components/Current/Footer/index.tsx b/components/Current/Footer/index.tsx index 83860cae7..683c26011 100644 --- a/components/Current/Footer/index.tsx +++ b/components/Current/Footer/index.tsx @@ -1,4 +1,4 @@ -import { serverClient } from "@/lib/trpc/server" +import { getCurrentFooter } from "@/lib/trpc/memoizedRequests" import Image from "@/components/Image" import { getLang } from "@/i18n/serverContext" @@ -8,9 +8,7 @@ import Navigation from "./Navigation" import styles from "./footer.module.css" export default async function Footer() { - const footerData = await serverClient().contentstack.base.currentFooter({ - lang: getLang(), - }) + const footerData = await getCurrentFooter(getLang()) if (!footerData) { return null } diff --git a/components/Current/Header/MainMenu/index.tsx b/components/Current/Header/MainMenu/index.tsx index e8f41a12e..0303a9448 100644 --- a/components/Current/Header/MainMenu/index.tsx +++ b/components/Current/Header/MainMenu/index.tsx @@ -7,13 +7,13 @@ import { myPages } from "@/constants/routes/myPages" import useDropdownStore from "@/stores/main-menu" import Image from "@/components/Image" +import LoginButton from "@/components/LoginButton" import Avatar from "@/components/MyPages/Avatar" import Link from "@/components/TempDesignSystem/Link" import useLang from "@/hooks/useLang" import { trackClick } from "@/utils/tracking" import BookingButton from "../BookingButton" -import LoginButton from "../LoginButton" import styles from "./mainMenu.module.css" diff --git a/components/Current/Header/TopMenu/index.tsx b/components/Current/Header/TopMenu/index.tsx index 1472461d8..cb7c103f7 100644 --- a/components/Current/Header/TopMenu/index.tsx +++ b/components/Current/Header/TopMenu/index.tsx @@ -2,12 +2,11 @@ import { logout } from "@/constants/routes/handleAuth" import { overview } from "@/constants/routes/myPages" import { getName } from "@/lib/trpc/memoizedRequests" +import LoginButton from "@/components/LoginButton" import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import LoginButton from "../LoginButton" - import styles from "./topMenu.module.css" import type { TopMenuProps } from "@/types/components/current/header/topMenu" @@ -67,7 +66,7 @@ export default async function TopMenu({ ) : ( {formatMessage({ id: "Log in" })} diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css index b2e23cd41..ee9034648 100644 --- a/components/DatePicker/date-picker.module.css +++ b/components/DatePicker/date-picker.module.css @@ -14,6 +14,7 @@ outline: none; padding: 0; width: 100%; + text-align: left; } .body { diff --git a/components/Footer/Details/index.tsx b/components/Footer/Details/index.tsx index d006f778c..59b9c2488 100644 --- a/components/Footer/Details/index.tsx +++ b/components/Footer/Details/index.tsx @@ -32,10 +32,6 @@ export default async function FooterDetails() { Scandic Hotels logo link.href && ( {appDownloads && (