diff --git a/.env.local.example b/.env.local.example index ab0b7ee72..c6f85bec7 100644 --- a/.env.local.example +++ b/.env.local.example @@ -51,4 +51,6 @@ GOOGLE_STATIC_MAP_SIGNATURE_SECRET="" GOOGLE_STATIC_MAP_ID="" GOOGLE_DYNAMIC_MAP_ID="" -HIDE_FOR_NEXT_RELEASE="true" +HIDE_FOR_NEXT_RELEASE="false" +SHOW_SIGNUP_FLOW="true" +USE_NEW_REWARDS_ENDPOINT="true" diff --git a/.env.test b/.env.test index ce2b20278..f651cbe63 100644 --- a/.env.test +++ b/.env.test @@ -43,3 +43,4 @@ GOOGLE_STATIC_MAP_ID="test" GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" +USE_NEW_REWARDS_ENDPOINT="true" diff --git a/app/[lang]/(live)/(protected)/my-pages/loading.tsx b/app/[lang]/(live)/(protected)/my-pages/loading.tsx index c739b6635..92ff5739e 100644 --- a/app/[lang]/(live)/(protected)/my-pages/loading.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/loading.tsx @@ -1,5 +1,5 @@ import LoadingSpinner from "@/components/LoadingSpinner" export default function Loading() { - return + return } diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx index d96f10f38..215e3aec8 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx @@ -1,5 +1,7 @@ +import { headers } from "next/headers" import { notFound } from "next/navigation" +import { isSignupPage } from "@/constants/routes/signup" import { env } from "@/env/server" import HotelPage from "@/components/ContentType/HotelPage" @@ -22,17 +24,33 @@ export default function ContentTypePage({ }: PageArgs) { setLang(params.lang) + const pathname = headers().get("x-pathname") || "" + switch (params.contentType) { case "collection-page": if (env.HIDE_FOR_NEXT_RELEASE) { return notFound() } return - case "content-page": + case "content-page": { + const isSignupRoute = isSignupPage(pathname) + if (env.HIDE_FOR_NEXT_RELEASE) { - return notFound() + // Hide content pages for next release for non-signup routes. + if (!isSignupRoute) { + return notFound() + } } + + if (!env.SHOW_SIGNUP_FLOW) { + // Hide content pages for signup routes when signup flow is disabled. + if (isSignupRoute) { + return notFound() + } + } + return + } case "loyalty-page": return case "hotel-page": diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css new file mode 100644 index 000000000..f680a23a1 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css @@ -0,0 +1,68 @@ +.mobileSummary { + display: block; +} + +.desktopSummary { + display: none; +} + +.summary { + background-color: var(--Main-Grey-White); + + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-width: 1px; + border-bottom: none; + z-index: 10; +} + +.hider { + display: none; +} + +.shadow { + display: none; +} + +@media screen and (min-width: 1367px) { + .mobileSummary { + display: none; + } + + .desktopSummary { + 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(--Spacing-x2) + + var(--Spacing-x-half) + ); + z-index: 10; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .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; + } + + .hider { + display: block; + background-color: var(--Scandic-Brand-Warm-White); + position: sticky; + top: calc(var(--booking-widget-desktop-height) - 6px); + margin-top: var(--Spacing-x4); + height: 40px; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx index b39c2622b..f8c5f20ac 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -1,25 +1,39 @@ +import { redirect } from "next/navigation" + +import { selectRate } from "@/constants/routes/hotelReservation" import { + getPackages, getProfileSafely, getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" import Summary from "@/components/HotelReservation/EnterDetails/Summary" +import { SummaryBottomSheet } from "@/components/HotelReservation/EnterDetails/Summary/BottomSheet" import { generateChildrenString, getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import styles from "./page.module.css" + import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { LangParams, PageArgs, SearchParams } from "@/types/params" export default async function SummaryPage({ + params, searchParams, }: PageArgs>) { const selectRoomParams = new URLSearchParams(searchParams) const { hotel, rooms, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) - const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const { + adults, + children, + roomTypeCode, + rateCode, + packages: packageCodes, + } = rooms[0] // TODO: Handle multiple rooms const availability = await getSelectedRoomAvailability({ hotelId: hotel, @@ -29,49 +43,88 @@ export default async function SummaryPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, + packageCodes, }) const user = await getProfileSafely() - if (!availability) { + const packages = packageCodes + ? await getPackages({ + hotelId: hotel, + startDate: fromDate, + endDate: toDate, + adults, + children: children?.length, + packageCodes, + }) + : null + + if (!availability || !availability.selectedRoom) { console.error("No hotel or availability data", availability) // TODO: handle this case - return null + redirect(selectRate[params.lang]) } const prices = user && availability.memberRate ? { local: { - price: availability.memberRate?.localPrice.pricePerStay, - currency: availability.memberRate?.localPrice.currency, + price: availability.memberRate.localPrice.pricePerStay, + currency: availability.memberRate.localPrice.currency, }, euro: { - price: availability.memberRate?.requestedPrice?.pricePerStay, - currency: availability.memberRate?.requestedPrice?.currency, + price: availability.memberRate.requestedPrice.pricePerStay, + currency: availability.memberRate.requestedPrice.currency, }, } : { local: { - price: availability.publicRate?.localPrice.pricePerStay, - currency: availability.publicRate?.localPrice.currency, + price: availability.publicRate.localPrice.pricePerStay, + currency: availability.publicRate.localPrice.currency, }, euro: { - price: availability.publicRate?.requestedPrice?.pricePerStay, - currency: availability.publicRate?.requestedPrice?.currency, + price: availability.publicRate.requestedPrice.pricePerStay, + currency: availability.publicRate.requestedPrice.currency, }, } return ( - + <> +
+ +
+ +
+
+
+
+
+
+ +
+
+
+ ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css index 13c3ded9f..0322e44a7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css @@ -8,94 +8,38 @@ background-color: var(--Scandic-Brand-Warm-White); } -.enter-details-layout__content { +.enter-details-layout__container { display: grid; gap: var(--Spacing-x3) var(--Spacing-x9); - grid-template-columns: 1fr 340px; - grid-template-rows: auto 1fr; - margin: var(--Spacing-x5) auto 0; /* simulates padding on viewport smaller than --max-width-navigation */ - width: min( - calc(100dvw - (var(--Spacing-x2) * 2)), - var(--max-width-navigation) - ); +} + +.enter-details-layout__content { + margin: var(--Spacing-x3) var(--Spacing-x2) 0; } .enter-details-layout__summaryContainer { - grid-column: 2 / 3; - grid-row: 1/-1; -} - -.enter-details-layout__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; -} - -.enter-details-layout__hider { - display: none; -} - -.enter-details-layout__shadow { - display: none; -} - -@media screen and (min-width: 950px) { - .enter-details-layout__summaryContainer { - display: grid; - grid-template-rows: auto auto 1fr; - margin-top: calc(0px - var(--Spacing-x9)); - } - - .enter-details-layout__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; - } - - .enter-details-layout__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; - } - - .enter-details-layout__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; - } + position: sticky; + bottom: 0; + left: 0; + right: 0; } @media screen and (min-width: 1367px) { - .enter-details-layout__summary { - top: calc( - var(--booking-widget-desktop-height) + var(--Spacing-x2) + - var(--Spacing-x-half) + .enter-details-layout__container { + grid-template-columns: 1fr 340px; + grid-template-rows: auto 1fr; + margin: var(--Spacing-x5) auto 0; + width: min( + calc(100dvw - (var(--Spacing-x2) * 2)), + var(--max-width-navigation) ); } - .enter-details-layout__hider { - top: calc(var(--booking-widget-desktop-height) - 6px); + .enter-details-layout__summaryContainer { + position: static; + display: grid; + grid-column: 2/3; + grid-row: 1/-1; } } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 3a4af62ba..fbd462544 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -28,12 +28,10 @@ export default async function StepLayout({
{hotelHeader} -
- {children} -
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 37f1412ff..70aef0ada 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -46,7 +46,13 @@ export default async function StepPage({ toDate, } = getQueryParamsForEnterDetails(selectRoomParams) - const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const { + adults, + children, + roomTypeCode, + rateCode, + packages: packageCodes, + } = rooms[0] // TODO: Handle multiple rooms const childrenAsString = children && generateChildrenString(children) @@ -60,12 +66,9 @@ export default async function StepPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, + packageCodes, }) - const hotelData = await getHotelData({ - hotelId, - language: lang, - }) const roomAvailability = await getSelectedRoomAvailability({ hotelId, adults, @@ -74,6 +77,12 @@ export default async function StepPage({ roomStayEndDate: toDate, rateCode, roomTypeCode, + packageCodes, + }) + const hotelData = await getHotelData({ + hotelId, + language: lang, + isCardOnlyPayment: roomAvailability?.mustBeGuaranteed, }) const breakfastPackages = await getBreakfastPackages(breakfastInput) const user = await getProfileSafely() 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 index 0f636136f..fd9cc33c5 100644 --- 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 @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { env } from "@/env/server" import { getLocations } from "@/lib/trpc/memoizedRequests" +import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" import { generateChildrenString, @@ -11,11 +12,7 @@ import { import { MapModal } from "@/components/MapModal" import { setLang } from "@/i18n/serverContext" -import { - fetchAvailableHotels, - getCentralCoordinates, - getHotelPins, -} from "../../utils" +import { fetchAvailableHotels } from "../../utils" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" @@ -61,16 +58,12 @@ export default async function SelectHotelMapPage({ const hotelPins = getHotelPins(hotels) - const centralCoordinates = getCentralCoordinates(hotelPins) - return ( 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 d8e3db57e..8bf36ee38 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,21 +1,23 @@ .main { display: flex; - gap: var(--Spacing-x3); - padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4); + padding: 0 var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x2); background-color: var(--Scandic-Brand-Warm-White); min-height: 100dvh; flex-direction: column; max-width: var(--max-width); margin: 0 auto; } - .header { display: flex; - margin: 0 auto; - padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3) - var(--Spacing-x5); - justify-content: space-between; - max-width: var(--max-width); + flex-direction: column; + gap: var(--Spacing-x2); + padding: var(--Spacing-x3) var(--Spacing-x2) 0 var(--Spacing-x2); +} + +.cityInformation { + display: flex; + flex-wrap: wrap; + gap: var(--Spacing-x1); } .sideBar { @@ -38,7 +40,31 @@ flex: 1; } +.hotelList { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--Spacing-x3); +} + @media (min-width: 768px) { + .main { + padding: var(--Spacing-x5); + } + .header { + display: block; + background-color: var(--Base-Surface-Subtle-Normal); + padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3) + var(--Spacing-x5); + } + + .title { + margin: 0 auto; + display: flex; + max-width: var(--max-width-navigation); + align-items: center; + justify-content: space-between; + } .link { display: flex; padding-bottom: var(--Spacing-x6); @@ -50,13 +76,6 @@ border-radius: var(--Corner-radius-Medium); border: 1px solid var(--Base-Border-Subtle); } - .mapLinkText { - display: flex; - align-items: center; - justify-content: center; - gap: var(--Spacing-x-half); - padding: var(--Spacing-x-one-and-half) var(--Spacing-x0); - } .main { flex-direction: row; gap: var(--Spacing-x5); 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 ddf1d9347..b7fbf345c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -19,7 +19,11 @@ import { } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" +import Alert from "@/components/TempDesignSystem/Alert" +import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" +import Preamble from "@/components/TempDesignSystem/Text/Preamble" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import TrackingSDK from "@/components/TrackingSDK" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -32,6 +36,7 @@ import { TrackingSDKHotelInfo, TrackingSDKPageData, } from "@/types/components/tracking" +import { AlertTypeEnum } from "@/types/enums/alert" import { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ @@ -99,17 +104,44 @@ export default async function SelectHotelPage({ return ( <>
-
{city.name}
- +
+
+ {city.name} + {hotels.length} hotels +
+ +
+
- + {hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available + +
+ + +
+ + ) : (
-
- {intl.formatMessage({ id: "Show map" })} - -
- - + )}
- +
+ {!hotels.length && ( + + )} + +
({ - coordinates: { - lat: hotel.hotelData.location.latitude, - lng: hotel.hotelData.location.longitude, - }, - name: hotel.hotelData.name, - publicPrice: hotel.price?.regularAmount ?? null, - memberPrice: hotel.price?.memberAmount ?? null, - currency: hotel.price?.currency || null, - images: [ - hotel.hotelData.hotelContent.images, - ...(hotel.hotelData.gallery?.heroImages ?? []), - ], - amenities: hotel.hotelData.detailedFacilities.slice(0, 3), - ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, - })) -} - -export function getCentralCoordinates(hotels: HotelPin[]) { - const centralCoordinates = hotels.reduce( - (acc, pin) => { - acc.lat += pin.coordinates.lat - acc.lng += pin.coordinates.lng - return acc - }, - { lat: 0, lng: 0 } - ) - - centralCoordinates.lat /= hotels.length - centralCoordinates.lng /= hotels.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 99554d1c1..fd9db4d6c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -18,7 +18,7 @@ import { setLang } from "@/i18n/serverContext" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function SelectRatePage({ params, @@ -49,11 +49,11 @@ export default async function SelectRatePage({ searchParams.fromDate && dt(searchParams.fromDate).isAfter(dt().subtract(1, "day")) ? searchParams.fromDate - : dt().utc().format("YYYY-MM-D") + : 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-D") + : 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 @@ -94,9 +94,16 @@ export default async function SelectRatePage({ const roomCategories = hotelData?.included + const noRoomsAvailable = roomsAvailability.roomConfigurations.reduce( + (acc, room) => { + return acc && room.status === "NotAvailable" + }, + true + ) + return ( <> - + + return } diff --git a/app/[lang]/(live)/@bookingwidget/loading.tsx b/app/[lang]/(live)/@bookingwidget/loading.tsx index 5e05ba68c..c45dee2ad 100644 --- a/app/[lang]/(live)/@bookingwidget/loading.tsx +++ b/app/[lang]/(live)/@bookingwidget/loading.tsx @@ -1,17 +1,11 @@ import { env } from "@/env/server" -import LoadingSpinner from "@/components/LoadingSpinner" - -import styles from "./loading.module.css" +import { BookingWidgetSkeleton } from "@/components/BookingWidget/Client" export default function LoadingBookingWidget() { if (env.HIDE_FOR_NEXT_RELEASE) { return null } - return ( -
- -
- ) + return } diff --git a/app/[lang]/(live)/@footer/loading.tsx b/app/[lang]/(live)/@footer/loading.tsx index 029d8ce71..9078cc122 100644 --- a/app/[lang]/(live)/@footer/loading.tsx +++ b/app/[lang]/(live)/@footer/loading.tsx @@ -1,11 +1,17 @@ import { env } from "@/env/server" import CurrentLoadingSpinner from "@/components/Current/LoadingSpinner" -import LoadingSpinner from "@/components/LoadingSpinner" +import { FooterDetailsSkeleton } from "@/components/Footer/Details" +import { FooterNavigationSkeleton } from "@/components/Footer/Navigation" export default function LoadingFooter() { if (env.HIDE_FOR_NEXT_RELEASE) { return } - return + return ( +
+ + +
+ ) } diff --git a/app/[lang]/(live)/loading.tsx b/app/[lang]/(live)/loading.tsx index c739b6635..92ff5739e 100644 --- a/app/[lang]/(live)/loading.tsx +++ b/app/[lang]/(live)/loading.tsx @@ -1,5 +1,5 @@ import LoadingSpinner from "@/components/LoadingSpinner" export default function Loading() { - return + return } diff --git a/app/[lang]/(live-current)/layout.tsx b/app/[lang]/(live-current)/layout.tsx index 539d345ba..3867b76d1 100644 --- a/app/[lang]/(live-current)/layout.tsx +++ b/app/[lang]/(live-current)/layout.tsx @@ -68,7 +68,6 @@ export default async function RootLayout({ {header} - {children}