diff --git a/.env b/.env deleted file mode 100644 index d116a87a8..000000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -# See update-dotenv.mjs -AUTH_URL="REPLACE-ON-NETLIFY-BUILD" -NEXTAUTH_URL="REPLACE-ON-NETLIFY-BUILD" -PUBLIC_URL="REPLACED-ON-NETLIFY-BUILD" diff --git a/.env.test b/.env.test index d2d1037ac..ce2b20278 100644 --- a/.env.test +++ b/.env.test @@ -42,3 +42,4 @@ GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test" GOOGLE_STATIC_MAP_ID="test" GOOGLE_DYNAMIC_MAP_ID="test" HIDE_FOR_NEXT_RELEASE="true" +SALESFORCE_PREFERENCE_BASE_URL="test" diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts new file mode 100644 index 000000000..a0539d351 --- /dev/null +++ b/actions/registerUserBookingFlow.ts @@ -0,0 +1,67 @@ +"use server" + +import { parsePhoneNumber } from "libphonenumber-js" +import { z } from "zod" + +import { serviceServerActionProcedure } from "@/server/trpc" + +import { phoneValidator } from "@/utils/phoneValidator" + +const registerUserPayload = z.object({ + firstName: z.string(), + lastName: z.string(), + dateOfBirth: z.string(), + address: z.object({ + countryCode: z.string(), + zipCode: z.string(), + }), + email: z.string(), + phoneNumber: phoneValidator("Phone is required"), +}) + +export const registerUserBookingFlow = serviceServerActionProcedure + .input(registerUserPayload) + .mutation(async function ({ ctx, input }) { + const payload = { + ...input, + language: ctx.lang, + phoneNumber: parsePhoneNumber(input.phoneNumber) + .formatNational() + .replace(/\s+/g, ""), + } + + // TODO: Consume the API to register the user as soon as passwordless signup is enabled. + // let apiResponse + // try { + // apiResponse = await api.post(api.endpoints.v1.profile, { + // body: payload, + // headers: { + // Authorization: `Bearer ${ctx.serviceToken}`, + // }, + // }) + // } catch (error) { + // console.error("Unexpected error", error) + // return { success: false, error: "Unexpected error" } + // } + + // if (!apiResponse.ok) { + // const text = await apiResponse.text() + // console.error(text) + // console.error( + // "registerUserBookingFlow api error", + // JSON.stringify({ + // query: input, + // error: { + // status: apiResponse.status, + // statusText: apiResponse.statusText, + // error: text, + // }, + // }) + // ) + // return { success: false, error: "API error" } + // } + // const json = await apiResponse.json() + // console.log("registerUserBookingFlow: json", json) + + return { success: true, data: payload } + }) diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index 2bb2f0a52..db32e1c48 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -4,6 +4,7 @@ import { AuthError } from "next-auth" import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { signOut } from "@/auth" @@ -11,6 +12,8 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { + const publicURL = getPublicURL(request) + let redirectTo: string = "" const returnUrl = request.headers.get("x-returnurl") @@ -39,7 +42,7 @@ export async function GET( // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { console.log(`[logout] make redirectTo absolute, from ${redirectTo}`) - redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + redirectTo = new URL(redirectTo, publicURL).href console.log(`[logout] make redirectTo absolute, to ${redirectTo}`) } diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.module.css b/app/[lang]/(live)/(protected)/my-pages/layout.module.css index a2ff660c4..199a19362 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.module.css +++ b/app/[lang]/(live)/(protected)/my-pages/layout.module.css @@ -1,5 +1,4 @@ .layout { - background-color: var(--Base-Background-Primary-Normal); display: grid; font-family: var(--typography-Body-Regular-fontFamily); gap: var(--Spacing-x3); @@ -9,6 +8,10 @@ margin: 0 auto; } +.container { + background-color: var(--Base-Background-Primary-Normal); +} + .content { display: grid; padding-bottom: var(--Spacing-x9); diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index 9677f014b..f5b84dbd7 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -9,12 +9,14 @@ export default async function MyPagesLayout({ breadcrumbs: React.ReactNode }>) { return ( -
- {breadcrumbs} -
- - {children} +
+
+ {breadcrumbs} +
+ + {children} +
-
+ ) } diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx index 1cc5c648f..43f1d66ef 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx @@ -1,6 +1,4 @@ -import { ArrowRightIcon } from "@/components/Icons" import ManagePreferencesButton from "@/components/Profile/ManagePreferencesButton" -import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx index 73496fe4e..13512d701 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/layout.tsx @@ -1,3 +1,5 @@ +import { env } from "@/env/server" + import Divider from "@/components/TempDesignSystem/Divider" import type { ProfileLayoutProps } from "@/types/components/myPages/myProfile/layout" @@ -15,7 +17,7 @@ export default function ProfileLayout({ {profile} {creditCards} - {communication} + {env.HIDE_FOR_NEXT_RELEASE ? null : communication}
) diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css index 9cfb8be93..2ec5fbe51 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.module.css @@ -1,9 +1,11 @@ .layout { - background-color: var(--Base-Background-Primary-Normal); display: grid; font-family: var(--typography-Body-Regular-fontFamily); gap: var(--Spacing-x3); grid-template-rows: auto 1fr; position: relative; - margin: 0 auto; +} + +.container { + background-color: var(--Base-Background-Primary-Normal); } diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx index 15fe17b28..76f26fe9f 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx @@ -16,9 +16,11 @@ export default function ContentTypeLayout({ } >) { return ( -
- {breadcrumbs} - {children} -
+
+
+ {breadcrumbs} + {children} +
+
) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx index 8ced3a7ce..5f62c69c9 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx @@ -2,19 +2,22 @@ import { redirect } from "next/navigation" import { serverClient } from "@/lib/trpc/server" +import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek" import Summary from "@/components/HotelReservation/EnterDetails/Summary" import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" import { setLang } from "@/i18n/serverContext" import styles from "./layout.module.css" +import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" export default async function StepLayout({ children, params, -}: React.PropsWithChildren>) { +}: React.PropsWithChildren>) { setLang(params.lang) const hotel = await serverClient().hotel.hotelData.get({ hotelId: "811", @@ -26,15 +29,18 @@ export default async function StepLayout({ } return ( -
- -
- - {children} - -
-
+ +
+ +
+ + {children} + +
+ +
+
) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx index 6f51260e4..5f2666c8e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx @@ -1,117 +1,69 @@ -"use client" - import { notFound } from "next/navigation" -import { useState } from "react" -import { useIntl } from "react-intl" -import { trpc } from "@/lib/trpc/client" +import { getProfileSafely } from "@/lib/trpc/memoizedRequests" +import { serverClient } from "@/lib/trpc/server" import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" +import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" +import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import Payment from "@/components/HotelReservation/SelectRate/Payment" -import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion" -import LoadingSpinner from "@/components/LoadingSpinner" +import { getIntl } from "@/i18n" +import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, PageArgs } from "@/types/params" -enum StepEnum { - selectBed = "select-bed", - breakfast = "breakfast", - details = "details", - payment = "payment", -} - function isValidStep(step: string): step is StepEnum { return Object.values(StepEnum).includes(step as StepEnum) } -export default function StepPage({ +export default async function StepPage({ params, }: PageArgs) { - const { step } = params - const [activeStep, setActiveStep] = useState(step) - const intl = useIntl() + const { step, lang } = params - if (!isValidStep(activeStep)) { + const intl = await getIntl() + + const hotel = await serverClient().hotel.hotelData.get({ + hotelId: "811", + language: lang, + }) + + const user = await getProfileSafely() + + if (!isValidStep(step) || !hotel) { return notFound() } - const { data: hotel, isLoading: loadingHotel } = - trpc.hotel.hotelData.get.useQuery({ - hotelId: "811", - language: params.lang, - }) - - const { data: userData } = trpc.user.getSafely.useQuery() - - if (loadingHotel) { - return - } - - if (!hotel) { - // TODO: handle case with hotel missing - return notFound() - } - - switch (activeStep) { - case StepEnum.breakfast: - //return
Select BREAKFAST
- case StepEnum.details: - //return
Select DETAILS
- case StepEnum.payment: - //return
Select PAYMENT
- case StepEnum.selectBed: - // return
Select BED
- } - - function onNav(step: StepEnum) { - setActiveStep(step) - if (typeof window !== "undefined") { - window.history.pushState({}, "", step) - } - } - - let user = null - if (userData && !("error" in userData)) { - user = userData - } - return (
+
diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css index 35fc821c4..2771aef75 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/page.module.css @@ -4,6 +4,8 @@ padding: var(--Spacing-x4); background-color: var(--Scandic-Brand-Warm-White); min-height: 100dvh; + max-width: var(--max-width); + margin: 0 auto; } .section { @@ -11,10 +13,4 @@ flex-direction: column; gap: var(--Spacing-x4); width: 100%; - max-width: 365px; -} -@media screen and (min-width: 1367px) { - .section { - max-width: 525px; - } } diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/layout.module.css index aaf8d1c3a..0969a7151 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/layout.module.css @@ -1,6 +1,4 @@ .layout { min-height: 100dvh; - max-width: var(--max-width); - margin: 0 auto; background-color: var(--Base-Background-Primary-Normal); } diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css index 3a8bcfe07..acc942e21 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css @@ -4,6 +4,9 @@ padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4); background-color: var(--Scandic-Brand-Warm-White); min-height: 100dvh; + flex-direction: column; + max-width: var(--max-width); + margin: 0 auto; } .section { @@ -15,3 +18,9 @@ display: flex; padding: var(--Spacing-x2) var(--Spacing-x0); } + +@media (min-width: 768px) { + .main { + flex-direction: row; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css index 3266c418d..464c8ce65 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css @@ -7,13 +7,12 @@ } .content { - max-width: 1134px; - margin-top: var(--Spacing-x5); - margin-left: auto; - margin-right: auto; + max-width: var(--max-width); + margin: 0 auto; display: flex; - justify-content: space-between; + flex-direction: column; gap: var(--Spacing-x7); + padding: var(--Spacing-x2); } .main { diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx index 796c0fedb..a9103d2c4 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx @@ -1,7 +1,8 @@ +import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" -import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" +import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -15,27 +16,49 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - // TODO: Use real endpoint. - const hotel = tempHotelData.data.attributes + const selecetRoomParams = new URLSearchParams(searchParams) + const selecetRoomParamsObject = + getHotelReservationQueryParams(selecetRoomParams) + const adults = selecetRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selecetRoomParamsObject.room[0].child.length // TODO: Handle multiple rooms + + const [hotelData, roomConfigurations, user] = await Promise.all([ + serverClient().hotel.hotelData.get({ + hotelId: searchParams.hotel, + language: params.lang, + include: ["RoomCategories"], + }), + serverClient().hotel.availability.rooms({ + hotelId: parseInt(searchParams.hotel, 10), + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults: adults, + children: children, + }), + getProfileSafely(), + ]) - const roomConfigurations = await serverClient().hotel.availability.rooms({ - hotelId: parseInt(searchParams.hotel, 10), - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, - }) if (!roomConfigurations) { - return "No rooms found" + return "No rooms found" // TODO: Add a proper error message } + if (!hotelData) { + return "No hotel data found" // TODO: Add a proper error message + } + + const roomCategories = hotelData?.included + return (
- {/* TODO: Add Hotel Listing Card */} -
Hotel Listing Card TBI
-
+ {/* TODO: Add Hotel Listing Card */} +
Hotel Listing Card TBI
- +
diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts index b038205e6..2d91c0d5f 100644 --- a/app/[lang]/(live)/(public)/login/route.ts +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -4,6 +4,7 @@ import { AuthError } from "next-auth" import { Lang } from "@/constants/languages" import { env } from "@/env/server" import { internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { signIn } from "@/auth" @@ -11,9 +12,7 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } + const publicURL = getPublicURL(request) let redirectHeaders: Headers | undefined = undefined let redirectTo: string @@ -54,7 +53,7 @@ export async function GET( // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { console.log(`[login] make redirectTo absolute, from ${redirectTo}`) - redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + redirectTo = new URL(redirectTo, publicURL).href console.log(`[login] make redirectTo absolute, to ${redirectTo}`) } @@ -131,7 +130,7 @@ export async function GET( * because user might choose to do Email link login. * */ // The `for_origin` param is used to make Curity email login functionality working. - for_origin: env.PUBLIC_URL, + for_origin: publicURL, // This is new param set for differentiate between the Magic link login of New web and current web version: "2", } diff --git a/app/[lang]/(live)/(public)/verifymagiclink/route.ts b/app/[lang]/(live)/(public)/verifymagiclink/route.ts index 12c4b132f..81fa277c3 100644 --- a/app/[lang]/(live)/(public)/verifymagiclink/route.ts +++ b/app/[lang]/(live)/(public)/verifymagiclink/route.ts @@ -5,6 +5,7 @@ import { Lang } from "@/constants/languages" import { login } from "@/constants/routes/handleAuth" import { env } from "@/env/server" import { badRequest, internalServerError } from "@/server/errors/next" +import { getPublicURL } from "@/server/utils" import { signIn } from "@/auth" @@ -12,9 +13,7 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } + const publicURL = getPublicURL(request) const loginKey = request.nextUrl.searchParams.get("loginKey") if (!loginKey) { @@ -44,7 +43,7 @@ export async function GET( console.log( `[verifymagiclink] make redirectTo absolute, from ${redirectTo}` ) - redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + redirectTo = new URL(redirectTo, publicURL).href console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`) } @@ -69,7 +68,7 @@ export async function GET( ui_locales: context.params.lang, scope: ["openid", "profile"].join(" "), loginKey: loginKey, - for_origin: env.PUBLIC_URL, + for_origin: publicURL, acr_values: "abc", version: "2", } diff --git a/app/api/web/add-card-callback/[lang]/route.ts b/app/api/web/add-card-callback/[lang]/route.ts index 63eb19942..e664124ed 100644 --- a/app/api/web/add-card-callback/[lang]/route.ts +++ b/app/api/web/add-card-callback/[lang]/route.ts @@ -4,14 +4,17 @@ import { env } from "process" import { Lang } from "@/constants/languages" import { profile } from "@/constants/routes/myPages" import { serverClient } from "@/lib/trpc/server" +import { getPublicURL } from "@/server/utils" export async function GET( request: NextRequest, { params }: { params: { lang: string } } ) { + const publicURL = getPublicURL(request) + console.log(`[add-card] callback started`) const lang = params.lang as Lang - const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`) + const returnUrl = new URL(`${publicURL}/${profile[lang ?? Lang.en]}`) try { const searchParams = request.nextUrl.searchParams diff --git a/app/api/web/envtest-dynamic-edge/route.ts b/app/api/web/envtest-dynamic-edge/route.ts deleted file mode 100644 index d2a12f7bf..000000000 --- a/app/api/web/envtest-dynamic-edge/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" - -export const runtime = "edge" diff --git a/app/api/web/envtest-dynamic-load-file/route.ts b/app/api/web/envtest-dynamic-load-file/route.ts deleted file mode 100644 index 115f12f77..000000000 --- a/app/api/web/envtest-dynamic-load-file/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ path: "./.env" }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-dynamic-load/route.ts b/app/api/web/envtest-dynamic-load/route.ts deleted file mode 100644 index 18c964526..000000000 --- a/app/api/web/envtest-dynamic-load/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import "dotenv/config" - -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-dynamic-override/route.ts b/app/api/web/envtest-dynamic-override/route.ts deleted file mode 100644 index 4d45b8e5f..000000000 --- a/app/api/web/envtest-dynamic-override/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ debug: true, override: true }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-dynamic/route.ts b/app/api/web/envtest-dynamic/route.ts deleted file mode 100644 index 386817938..000000000 --- a/app/api/web/envtest-dynamic/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} - -export const dynamic = "force-dynamic" diff --git a/app/api/web/envtest-static-load-file/route.ts b/app/api/web/envtest-static-load-file/route.ts deleted file mode 100644 index f1a83978a..000000000 --- a/app/api/web/envtest-static-load-file/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ path: "./.env" }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/envtest-static-load/route.ts b/app/api/web/envtest-static-load/route.ts deleted file mode 100644 index 4e360a891..000000000 --- a/app/api/web/envtest-static-load/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import "dotenv/config" - -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/envtest-static-override/route.ts b/app/api/web/envtest-static-override/route.ts deleted file mode 100644 index d844016c5..000000000 --- a/app/api/web/envtest-static-override/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { config } from "dotenv" -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -config({ debug: true, override: true }) - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/envtest-static/route.ts b/app/api/web/envtest-static/route.ts deleted file mode 100644 index 1c71b77ca..000000000 --- a/app/api/web/envtest-static/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextResponse } from "next/server" - -import { env } from "@/env/server" - -import type { NextRequest } from "next/server" - -export async function GET(request: NextRequest) { - const e = process.env - console.log({ process_env: process.env }) - - const urlVar = "PUBLIC_URL" - const nextAuthUrlVar = "NEXTAUTH_URL" - const nextAuthUrlVar2 = "AUTH_URL" - const envTestVar = "ENVTEST" - - const values = { - env_url: env.PUBLIC_URL, - static_url: process.env.PUBLIC_URL, - dynamic_url: e[urlVar], - env_envtest: env.ENVTEST, - static_envtest: process.env.ENVTEST, - dynamic_envtest: e[envTestVar], - env_nextauth: env.NEXTAUTH_URL, - static_nextauth: process.env.NEXTAUTH_URL, - dynamic_nextauth: e[nextAuthUrlVar], - env_nextauth2: env.AUTH_URL, - static_nextauth2: process.env.AUTH_URL, - dynamic_nextauth2: e[nextAuthUrlVar2], - } - - console.log(values) - - return NextResponse.json(values) -} diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts index 0b8133c43..4154aff14 100644 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -6,20 +6,21 @@ import { bookingConfirmation, payment, } from "@/constants/routes/hotelReservation" +import { getPublicURL } from "@/server/utils" export async function GET( request: NextRequest, { params }: { params: { lang: string; status: string } } ): Promise { + const publicURL = getPublicURL(request) + console.log(`[payment-callback] callback started`) const lang = params.lang as Lang const status = params.status - const returnUrl = new URL(`${env.PUBLIC_URL}/${payment[lang]}`) + const returnUrl = new URL(`${publicURL}/${payment[lang]}`) if (status === "success") { - const confirmationUrl = new URL( - `${env.PUBLIC_URL}/${bookingConfirmation[lang]}` - ) + const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`) console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) return NextResponse.redirect(confirmationUrl) } diff --git a/app/globals.css b/app/globals.css index 70606b14d..5fff0324b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -107,13 +107,15 @@ --main-menu-mobile-height: 75px; --main-menu-desktop-height: 118px; - --booking-widget-desktop-height: 95px; + --booking-widget-mobile-height: 75px; + --booking-widget-desktop-height: 77px; --hotel-page-map-desktop-width: 23.75rem; /* Z-INDEX */ --header-z-index: 10; --menu-overlay-z-index: 10; --dialog-z-index: 9; + --sidepeek-z-index: 11; } * { diff --git a/auth.ts b/auth.ts index 0de343626..3adc360f8 100644 --- a/auth.ts +++ b/auth.ts @@ -109,6 +109,7 @@ const curityProvider = { } satisfies OIDCConfig export const config = { + basePath: "/api/web/auth", debug: env.NEXTAUTH_DEBUG, providers: [curityProvider], redirectProxyUrl: env.NEXTAUTH_REDIRECT_PROXY_URL, @@ -233,4 +234,4 @@ export const { auth, signIn, signOut, -} = NextAuth(config) \ No newline at end of file +} = NextAuth(config) diff --git a/components/Blocks/Accordion/accordion.module.css b/components/Blocks/Accordion/accordion.module.css new file mode 100644 index 000000000..0dd49a0b7 --- /dev/null +++ b/components/Blocks/Accordion/accordion.module.css @@ -0,0 +1,7 @@ +.accordion:not(.allVisible) :nth-child(n + 6) { + display: none; +} + +.accordion:not(.allVisible) :nth-child(5) { + border: none; +} diff --git a/components/Blocks/Accordion/index.tsx b/components/Blocks/Accordion/index.tsx new file mode 100644 index 000000000..8828555d1 --- /dev/null +++ b/components/Blocks/Accordion/index.tsx @@ -0,0 +1,53 @@ +"use client" +import { useState } from "react" + +import JsonToHtml from "@/components/JsonToHtml" +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import Accordion from "@/components/TempDesignSystem/Accordion" +import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" +import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" + +import styles from "./accordion.module.css" + +import type { AccordionProps } from "@/types/components/blocks/Accordion" +import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" + +export default function AccordionSection({ accordion, title }: AccordionProps) { + const showToggleButton = accordion.length > 5 + const [allAccordionsVisible, setAllAccordionsVisible] = + useState(!showToggleButton) + + function toggleAccordions() { + setAllAccordionsVisible((state) => !state) + } + + return ( + + {title && } + + {accordion.map((acc) => ( + + + + ))} + + + {showToggleButton ? ( + + ) : null} + + ) +} diff --git a/components/Blocks/CardsGrid.tsx b/components/Blocks/CardsGrid.tsx index 134eeaa41..1c07b73ec 100644 --- a/components/Blocks/CardsGrid.tsx +++ b/components/Blocks/CardsGrid.tsx @@ -6,12 +6,29 @@ import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" import TeaserCard from "@/components/TempDesignSystem/TeaserCard" import type { CardsGridProps } from "@/types/components/blocks/cardsGrid" -import { CardsGridEnum } from "@/types/enums/cardsGrid" +import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid" +import type { StackableGridProps } from "../TempDesignSystem/Grids/Stackable/stackable" export default function CardsGrid({ cards_grid, firstItem = false, }: CardsGridProps) { + let columns: StackableGridProps["columns"] + + switch (cards_grid.layout) { + case CardsGridLayoutEnum.ONE_COLUMN: + columns = 1 + break + case CardsGridLayoutEnum.TWO_COLUMNS: + columns = 2 + break + case CardsGridLayoutEnum.THREE_COLUMNS: + columns = 3 + break + default: + columns = 3 + } + return ( - + {cards_grid.cards.map((card) => { switch (card.__typename) { case CardsGridEnum.cards.Card: @@ -43,6 +60,7 @@ export default function CardsGrid({ primaryButton={card.primaryButton} secondaryButton={card.secondaryButton} sidePeekButton={card.sidePeekButton} + sidePeekContent={card.sidePeekContent} image={card.image} /> ) diff --git a/components/Blocks/DynamicContent/index.tsx b/components/Blocks/DynamicContent/index.tsx index 31ba4009a..21b2616ac 100644 --- a/components/Blocks/DynamicContent/index.tsx +++ b/components/Blocks/DynamicContent/index.tsx @@ -1,3 +1,5 @@ +import { env } from "@/env/server" + import HowItWorks from "@/components/Blocks/DynamicContent/HowItWorks" import LoyaltyLevels from "@/components/Blocks/DynamicContent/LoyaltyLevels" import Overview from "@/components/Blocks/DynamicContent/Overview" @@ -26,7 +28,9 @@ export default async function DynamicContent({ case DynamicContentEnum.Blocks.components.earn_and_burn: return case DynamicContentEnum.Blocks.components.expiring_points: - return + return env.HIDE_FOR_NEXT_RELEASE ? null : ( + + ) case DynamicContentEnum.Blocks.components.how_it_works: return ( diff --git a/components/Blocks/Shortcuts/index.tsx b/components/Blocks/Shortcuts/index.tsx deleted file mode 100644 index 1188c06fe..000000000 --- a/components/Blocks/Shortcuts/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ArrowRightIcon } from "@/components/Icons" -import SectionContainer from "@/components/Section/Container" -import SectionHeader from "@/components/Section/Header" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" - -import styles from "./shortcuts.module.css" - -import type { ShortcutsProps } from "@/types/components/myPages/myPage/shortcuts" - -export default function Shortcuts({ - firstItem = false, - shortcuts, - subtitle, - title, -}: ShortcutsProps) { - return ( - - -
- {shortcuts.map((shortcut) => ( - - - {shortcut.text ? shortcut.text : shortcut.title} - - - - ))} -
-
- ) -} diff --git a/components/Blocks/Shortcuts/shortcuts.module.css b/components/Blocks/Shortcuts/shortcuts.module.css deleted file mode 100644 index a82b672fb..000000000 --- a/components/Blocks/Shortcuts/shortcuts.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.links { - display: grid; - background-color: var(--Scandic-Brand-Pale-Peach); - border-radius: var(--Corner-radius-Medium); - border: 1px solid var(--Base-Border-Subtle); -} - -.arrowRight { - height: 24px; - width: 24px; -} diff --git a/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx b/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx new file mode 100644 index 000000000..2647a18b5 --- /dev/null +++ b/components/Blocks/ShortcutsList/ShortcutsListItems/index.tsx @@ -0,0 +1,32 @@ +import { ArrowRightIcon } from "@/components/Icons" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" + +import styles from "./shortcutsListItems.module.css" + +import type { ShortcutsListItemsProps } from "@/types/components/blocks/shortcuts" + +export default function ShortcutsListItems({ + shortcutsListItems, + className, +}: ShortcutsListItemsProps) { + return ( +
    + {shortcutsListItems.map((shortcut) => ( +
  • + + + {shortcut.text || shortcut.title} + + + +
  • + ))} +
+ ) +} diff --git a/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css b/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css new file mode 100644 index 000000000..b2f150257 --- /dev/null +++ b/components/Blocks/ShortcutsList/ShortcutsListItems/shortcutsListItems.module.css @@ -0,0 +1,11 @@ +.link { + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.listItem { + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.listItem:last-child { + border-bottom: none; +} diff --git a/components/Blocks/ShortcutsList/index.tsx b/components/Blocks/ShortcutsList/index.tsx new file mode 100644 index 000000000..12c718229 --- /dev/null +++ b/components/Blocks/ShortcutsList/index.tsx @@ -0,0 +1,51 @@ +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" + +import ShortcutsListItems from "./ShortcutsListItems" + +import styles from "./shortcutsList.module.css" + +import type { ShortcutsListProps } from "@/types/components/blocks/shortcuts" + +export default function ShortcutsList({ + firstItem = false, + shortcuts, + subtitle, + title, + hasTwoColumns, +}: ShortcutsListProps) { + const middleIndex = Math.ceil(shortcuts.length / 2) + const leftColumn = shortcuts.slice(0, middleIndex) + const rightColumn = shortcuts.slice(middleIndex) + + const classNames = + hasTwoColumns && shortcuts.length > 1 + ? { + section: styles.twoColumnSection, + leftColumn: styles.leftColumn, + rightColumn: styles.rightColumn, + } + : { + section: styles.oneColumnSection, + leftColumn: + shortcuts.length === 1 + ? styles.leftColumnBorderBottomNone + : styles.leftColumnBorderBottom, + } + + return ( + + +
+ + +
+
+ ) +} diff --git a/components/Blocks/ShortcutsList/shortcutsList.module.css b/components/Blocks/ShortcutsList/shortcutsList.module.css new file mode 100644 index 000000000..26dd448ac --- /dev/null +++ b/components/Blocks/ShortcutsList/shortcutsList.module.css @@ -0,0 +1,33 @@ +.oneColumnSection, +.twoColumnSection { + display: grid; + border-radius: var(--Corner-radius-Medium); + border: 1px solid var(--Base-Border-Subtle); + overflow: hidden; +} + +.leftColumn, +.leftColumnBorderBottom { + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.leftColumnBorderBottomNone { + border-bottom: none; +} + +@media screen and (min-width: 1367px) { + .twoColumnSection { + grid-template-columns: 1fr 1fr; + column-gap: var(--Spacing-x2); + border-radius: 0; + border: none; + } + + .leftColumn, + .rightColumn { + height: fit-content; + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Medium); + overflow: hidden; + } +} diff --git a/components/Blocks/UspGrid/renderOptions.tsx b/components/Blocks/UspGrid/renderOptions.tsx index a4033fec5..96a0f7fa1 100644 --- a/components/Blocks/UspGrid/renderOptions.tsx +++ b/components/Blocks/UspGrid/renderOptions.tsx @@ -1,9 +1,7 @@ import Link from "@/components/TempDesignSystem/Link" -import { removeMultipleSlashes } from "@/utils/url" import styles from "./uspgrid.module.css" -import { EmbedEnum } from "@/types/requests/utils/embeds" import type { EmbedByUid } from "@/types/transitionTypes/jsontohtml" import { RTEItemTypeEnum, RTETypeEnum } from "@/types/transitionTypes/rte/enums" import type { diff --git a/components/Blocks/index.tsx b/components/Blocks/index.tsx index 2423508d1..f165b50ba 100644 --- a/components/Blocks/index.tsx +++ b/components/Blocks/index.tsx @@ -1,10 +1,11 @@ import CardsGrid from "@/components/Blocks/CardsGrid" import DynamicContent from "@/components/Blocks/DynamicContent" -import Shortcuts from "@/components/Blocks/Shortcuts" +import ShortcutsList from "@/components/Blocks/ShortcutsList" import TextCols from "@/components/Blocks/TextCols" import UspGrid from "@/components/Blocks/UspGrid" import JsonToHtml from "@/components/JsonToHtml" +import AccordionSection from "./Accordion" import Table from "./Table" import type { BlocksProps } from "@/types/components/blocks" @@ -14,6 +15,14 @@ export default function Blocks({ blocks }: BlocksProps) { return blocks.map((block, idx) => { const firstItem = idx === 0 switch (block.typename) { + case BlocksEnums.block.Accordion: + return ( + + ) case BlocksEnums.block.CardsGrid: return ( ) case BlocksEnums.block.Table: @@ -64,6 +74,7 @@ export default function Blocks({ blocks }: BlocksProps) { ) case BlocksEnums.block.UspGrid: return + default: return null } diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index 48f08f323..eded233f0 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -7,7 +7,7 @@ import { dt } from "@/lib/dt" import Form from "@/components/Forms/BookingWidget" import { bookingWidgetSchema } from "@/components/Forms/BookingWidget/schema" -import { CloseLarge } from "@/components/Icons" +import { CloseLargeIcon } from "@/components/Icons" import { debounce } from "@/utils/debounce" import MobileToggleButton from "./MobileToggleButton" @@ -98,7 +98,7 @@ export default function BookingWidgetClient({ onClick={closeMobileSearch} type="button" > - +
diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index 461057bf1..22ff009ee 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -42,7 +42,7 @@ box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05); position: sticky; top: 0; - z-index: 9; + z-index: 10; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/Breadcrumbs/breadcrumbs.module.css b/components/Breadcrumbs/breadcrumbs.module.css index 84729a81e..9523fd4c9 100644 --- a/components/Breadcrumbs/breadcrumbs.module.css +++ b/components/Breadcrumbs/breadcrumbs.module.css @@ -3,6 +3,9 @@ padding-left: var(--Spacing-x2); padding-right: var(--Spacing-x2); padding-top: var(--Spacing-x2); + max-width: var(--max-width); + margin: 0 auto; + width: 100%; } .list { diff --git a/components/Breadcrumbs/index.tsx b/components/Breadcrumbs/index.tsx index 752f77d5f..1939e47fa 100644 --- a/components/Breadcrumbs/index.tsx +++ b/components/Breadcrumbs/index.tsx @@ -48,7 +48,7 @@ export default async function Breadcrumbs() { return (
  • - + {breadcrumb.title}
  • diff --git a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css index 1e6046459..f7dcea5fc 100644 --- a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css +++ b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css @@ -6,8 +6,7 @@ display: grid; gap: var(--Spacing-x-one-and-half); height: fit-content; - width: 100%; - max-width: 300px; + width: min(100%, 300px); } .amenityItemList { diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css b/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css index 7c97fb3bd..be60e0f7f 100644 --- a/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css +++ b/components/ContentType/HotelPage/Facilities/CardGrid/cardGrid.module.css @@ -1,3 +1,7 @@ +.cardContainer { + scroll-margin-top: var(--hotel-page-scroll-margin-top); +} + .spanOne { grid-column: span 1; } diff --git a/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx b/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx index 8bb1dff40..f14270fea 100644 --- a/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx +++ b/components/ContentType/HotelPage/Facilities/CardGrid/index.tsx @@ -26,7 +26,7 @@ export default function FacilitiesCardGrid({ } return ( -
    +
    {facilitiesCardGrid.map((card: FacilityCardType) => ( diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index f6118a2fc..360d0f8df 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -11,10 +11,10 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { IntroSectionProps } from "./types" - import styles from "./introSection.module.css" +import type { IntroSectionProps } from "./types" + export default async function IntroSection({ hotelName, hotelDescription, diff --git a/components/ContentType/HotelPage/IntroSection/introSection.module.css b/components/ContentType/HotelPage/IntroSection/introSection.module.css index 1697b7ee8..5e9ec4b80 100644 --- a/components/ContentType/HotelPage/IntroSection/introSection.module.css +++ b/components/ContentType/HotelPage/IntroSection/introSection.module.css @@ -2,7 +2,7 @@ display: grid; gap: var(--Spacing-x2); position: relative; - max-width: var(--max-width-text-block); + max-width: 607px; /* Max width according to Figma */ } .mainContent { diff --git a/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx b/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx index a04082702..6dbd27425 100644 --- a/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx +++ b/components/ContentType/HotelPage/Map/MobileMapToggle/index.tsx @@ -1,5 +1,4 @@ "use client" - import { useIntl } from "react-intl" import useHotelPageStore from "@/stores/hotel-page" @@ -21,7 +20,6 @@ export default function MobileMapToggle() { onClick={closeDynamicMap} > 3 + const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton) + const scrollRef = useRef(null) const mappedRooms = rooms .map((room) => { - const size = `${room.attributes.roomSize.min} - ${room.attributes.roomSize.max} m²` + const size = `${room.roomSize.min} - ${room.roomSize.max} m²` const personLabel = - room.attributes.occupancy.total === 1 + room.occupancy.total === 1 ? intl.formatMessage({ id: "hotelPages.rooms.roomCard.person" }) : intl.formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) - const subtitle = `${size} (${room.attributes.occupancy.total} ${personLabel})` + const subtitle = `${size} (${room.occupancy.total} ${personLabel})` return { id: room.id, - images: room.attributes.content.images, - title: room.attributes.name, + images: room.images, + title: room.name, subtitle: subtitle, - sortOrder: room.attributes.sortOrder, + sortOrder: room.sortOrder, popularChoice: null, } }) .sort((a, b) => a.sortOrder - b.sortOrder) - function handleToggleShowMore() { + function handleShowMore() { if (scrollRef.current && allRoomsVisible) { scrollRef.current.scrollIntoView({ behavior: "smooth" }) } - - setAllRoomsVisible((previousState) => !previousState) + setAllRoomsVisible((state) => !state) } return ( - -
    + +
    - - {mappedRooms.map( - ({ id, images, title, subtitle, popularChoice }, index) => ( -
    2 ? styles.hiddenRoomCard : "" - } - > - -
    - ) - )} + + {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( +
    + +
    + ))}
    -
    - -
    + {showToggleButton ? ( + + ) : null}
    ) } diff --git a/components/ContentType/HotelPage/Rooms/rooms.module.css b/components/ContentType/HotelPage/Rooms/rooms.module.css index 66ff176ff..7e9ad6ac5 100644 --- a/components/ContentType/HotelPage/Rooms/rooms.module.css +++ b/components/ContentType/HotelPage/Rooms/rooms.module.css @@ -1,12 +1,22 @@ +.roomsContainer { + position: relative; + scroll-margin-top: var(--hotel-page-scroll-margin-top); +} + +.scrollRef { + position: absolute; + top: calc(-1 * var(--hotel-page-scroll-margin-top)); +} + .ctaContainer { display: flex; justify-content: center; } -.hiddenRoomCard { - display: none; -} - .showMoreButton.showLess .chevron { transform: rotate(180deg); } + +.grid:not(.allVisible) :nth-child(n + 4) { + display: none; +} diff --git a/components/ContentType/HotelPage/Rooms/types.ts b/components/ContentType/HotelPage/Rooms/types.ts deleted file mode 100644 index b4c320af2..000000000 --- a/components/ContentType/HotelPage/Rooms/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RoomData } from "@/types/hotel" - -export type RoomsProps = { - rooms: RoomData[] -} diff --git a/components/ContentType/HotelPage/TabNavigation/index.tsx b/components/ContentType/HotelPage/TabNavigation/index.tsx index 346db0521..8e80459b4 100644 --- a/components/ContentType/HotelPage/TabNavigation/index.tsx +++ b/components/ContentType/HotelPage/TabNavigation/index.tsx @@ -1,8 +1,12 @@ "use client" + +import { useRouter } from "next/navigation" +import { useEffect } from "react" import { useIntl } from "react-intl" import Link from "@/components/TempDesignSystem/Link" import useHash from "@/hooks/useHash" +import useScrollSpy from "@/hooks/useScrollSpy" import styles from "./tabNavigation.module.css" @@ -11,50 +15,78 @@ import { type TabNavigationProps, } from "@/types/components/hotelPage/tabNavigation" -export default function TabNavigation({ restaurantTitle }: TabNavigationProps) { +export default function TabNavigation({ + restaurantTitle, + hasActivities, + hasFAQ, +}: TabNavigationProps) { const hash = useHash() const intl = useIntl() + const router = useRouter() - const hotelTabLinks: { href: HotelHashValues | string; text: string }[] = [ + const tabLinks: { hash: HotelHashValues; text: string }[] = [ { - href: HotelHashValues.overview, + hash: HotelHashValues.overview, text: intl.formatMessage({ id: "Overview" }), }, - { href: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) }, + { hash: HotelHashValues.rooms, text: intl.formatMessage({ id: "Rooms" }) }, { - href: HotelHashValues.restaurant, + hash: HotelHashValues.restaurant, text: intl.formatMessage({ id: restaurantTitle }, { count: 1 }), }, { - href: HotelHashValues.meetings, + hash: HotelHashValues.meetings, text: intl.formatMessage({ id: "Meetings & Conferences" }), }, { - href: HotelHashValues.wellness, + hash: HotelHashValues.wellness, text: intl.formatMessage({ id: "Wellness & Exercise" }), }, - { - href: HotelHashValues.activities, - text: intl.formatMessage({ id: "Activities" }), - }, - { href: HotelHashValues.faq, text: intl.formatMessage({ id: "FAQ" }) }, + ...(hasActivities + ? [ + { + hash: HotelHashValues.activities, + text: intl.formatMessage({ id: "Activities" }), + }, + ] + : []), + ...(hasFAQ + ? [ + { + hash: HotelHashValues.faq, + text: intl.formatMessage({ id: "FAQ" }), + }, + ] + : []), ] + const { activeSectionId, pauseScrollSpy } = useScrollSpy( + tabLinks.map(({ hash }) => hash) + ) + + useEffect(() => { + if (activeSectionId) { + router.replace(`#${activeSectionId}`, { scroll: false }) + } + }, [activeSectionId, router]) + return (
    -
    +
    - - {/* eslint-disable import/no-named-as-default-member */} - - {/* TODO: Render amenities as per the design. */} - Read more about the amenities here - - - Some additional information about the hotel - - - {/* TODO */} - Restaurant & Bar - - - {/* TODO */} - Wellness & Exercise - - - {/* TODO */} - Activities - - - {/* TODO */} - Meetings & Conferences - - {/* eslint-enable import/no-named-as-default-member */} - +
    + {faq && ( + + )}
    {googleMapsApiKey ? ( <> @@ -139,6 +104,51 @@ export default async function HotelPage() { /> ) : null} + + {/* eslint-disable import/no-named-as-default-member */} + + {/* TODO: Render amenities as per the design. */} + Read more about the amenities here + + + Some additional information about the hotel + + + {/* TODO */} + Restaurant & Bar + + + {/* TODO */} + Wellness & Exercise + + + {/* TODO */} + Activities + + + {/* TODO */} + Meetings & Conferences + + {/* eslint-enable import/no-named-as-default-member */} + ) } diff --git a/components/ContentType/LoyaltyPage/loyaltyPage.module.css b/components/ContentType/LoyaltyPage/loyaltyPage.module.css index 8ceb15da3..d21b48d68 100644 --- a/components/ContentType/LoyaltyPage/loyaltyPage.module.css +++ b/components/ContentType/LoyaltyPage/loyaltyPage.module.css @@ -3,11 +3,12 @@ padding-bottom: var(--Spacing-x9); padding-left: var(--Spacing-x0); padding-right: var(--Spacing-x0); - position: relative; - justify-content: center; - align-items: flex-start; + container-name: loyalty-page; container-type: inline-size; + max-width: var(--max-width); + margin: 0 auto; + width: 100%; } .blocks { diff --git a/components/DatePicker/Screen/Desktop.tsx b/components/DatePicker/Screen/Desktop.tsx index db290137e..9056f9183 100644 --- a/components/DatePicker/Screen/Desktop.tsx +++ b/components/DatePicker/Screen/Desktop.tsx @@ -61,9 +61,9 @@ export default function DatePickerDesktop({ locale={locale} mode="range" numberOfMonths={2} - onSelect={handleOnSelect} + onDayClick={handleOnSelect} pagedNavigation - required + required={false} selected={selectedDate} startMonth={currentDate} weekStartsOn={1} @@ -82,7 +82,7 @@ export default function DatePickerDesktop({ size="small" theme="base" > - + {intl.formatMessage({ id: "Select dates" })} diff --git a/components/DatePicker/Screen/Mobile.tsx b/components/DatePicker/Screen/Mobile.tsx index c56a79cd6..0407f9b63 100644 --- a/components/DatePicker/Screen/Mobile.tsx +++ b/components/DatePicker/Screen/Mobile.tsx @@ -6,7 +6,7 @@ import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" import { dt } from "@/lib/dt" -import { CloseLarge } from "@/components/Icons" +import { CloseLargeIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -78,7 +78,7 @@ export default function DatePickerMobile({ mode="range" /** Showing full year or what's left of it */ numberOfMonths={12} - onSelect={handleOnSelect} + onDayClick={handleOnSelect} required selected={selectedDate} startMonth={startMonth} @@ -127,7 +127,7 @@ export default function DatePickerMobile({ ))} {children} diff --git a/components/DatePicker/Screen/desktop.module.css b/components/DatePicker/Screen/desktop.module.css index afbb286a7..aa02537a5 100644 --- a/components/DatePicker/Screen/desktop.module.css +++ b/components/DatePicker/Screen/desktop.module.css @@ -47,8 +47,8 @@ td.rangeStart[aria-selected="true"] button.dayButton:hover { } td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, -td.rangeStart[aria-selected="true"]:not([data-outside="true"]) - button.dayButton { +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.day[aria-selected="true"] button.dayButton { background: var(--Primary-Light-On-Surface-Accent); border: none; color: var(--Base-Button-Inverted-Fill-Normal); @@ -75,6 +75,7 @@ td.rangeMiddle[aria-selected="true"] button.dayButton { background: var(--Base-Background-Primary-Normal); border: none; border-radius: 0; + color: var(--UI-Text-High-contrast); } td.day[data-disabled="true"], diff --git a/components/DatePicker/Screen/mobile.module.css b/components/DatePicker/Screen/mobile.module.css index 4f820fd5f..ef3f97ca0 100644 --- a/components/DatePicker/Screen/mobile.module.css +++ b/components/DatePicker/Screen/mobile.module.css @@ -113,8 +113,8 @@ td.rangeStart[aria-selected="true"] button.dayButton:hover { } td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton, -td.rangeStart[aria-selected="true"]:not([data-outside="true"]) - button.dayButton { +td.rangeStart[aria-selected="true"]:not([data-outside="true"]) button.dayButton, +td.day[aria-selected="true"] button.dayButton { background: var(--Primary-Light-On-Surface-Accent); border: none; color: var(--Base-Button-Inverted-Fill-Normal); @@ -141,6 +141,7 @@ td.rangeMiddle[aria-selected="true"] button.dayButton { background: var(--Base-Background-Primary-Normal); border: none; border-radius: 0; + color: var(--UI-Text-High-contrast); } td.day[data-disabled="true"], diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css index b11ab3b77..b2e23cd41 100644 --- a/components/DatePicker/date-picker.module.css +++ b/components/DatePicker/date-picker.module.css @@ -22,6 +22,11 @@ .hideWrapper { background-color: var(--Main-Grey-White); + display: none; +} + +.container[data-isopen="true"] .hideWrapper { + display: block; } @media screen and (max-width: 1366px) { diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index 586da5208..503c0c6ac 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -14,8 +14,6 @@ import DatePickerMobile from "./Screen/Mobile" import styles from "./date-picker.module.css" -import type { DateRange } from "react-day-picker" - import type { DatePickerFormProps } from "@/types/components/datepicker" const locales = { @@ -33,6 +31,8 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const { register, setValue } = useFormContext() const ref = useRef(null) + const [isSelectingFrom, setIsSelectingFrom] = useState(true) + function close() { setIsOpen(false) } @@ -41,11 +41,29 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { setIsOpen((prevIsOpen) => !prevIsOpen) } - function handleSelectDate(selected: DateRange) { - setValue(name, { - from: dt(selected.from).format("YYYY-MM-DD"), - to: dt(selected.to).format("YYYY-MM-DD"), - }) + function handleSelectDate(selected: Date) { + if (isSelectingFrom) { + setValue(name, { + from: dt(selected).format("YYYY-MM-DD"), + to: undefined, + }) + setIsSelectingFrom(false) + } else { + const fromDate = dt(selectedDate.from) + const toDate = dt(selected) + if (toDate.isAfter(fromDate)) { + setValue(name, { + from: selectedDate.from, + to: toDate.format("YYYY-MM-DD"), + }) + } else { + setValue(name, { + from: toDate.format("YYYY-MM-DD"), + to: selectedDate.from, + }) + } + setIsSelectingFrom(true) + } } useEffect(() => { @@ -64,7 +82,9 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { const selectedFromDate = dt(selectedDate.from) .locale(lang) .format("ddd D MMM") - const selectedToDate = dt(selectedDate.to).locale(lang).format("ddd D MMM") + const selectedToDate = !!selectedDate.to + ? dt(selectedDate.to).locale(lang).format("ddd D MMM") + : "" return (
    diff --git a/components/Footer/Navigation/MainNav/index.tsx b/components/Footer/Navigation/MainNav/index.tsx index 91d674d60..e220af4a2 100644 --- a/components/Footer/Navigation/MainNav/index.tsx +++ b/components/Footer/Navigation/MainNav/index.tsx @@ -17,6 +17,7 @@ export default function FooterMainNav({ mainLinks }: FooterMainNavProps) { color="burgundy" href={link.url} className={styles.mainNavigationLink} + target={link.openInNewTab ? "_blank" : undefined} > {link.title} diff --git a/components/Footer/Navigation/SecondaryNav/index.tsx b/components/Footer/Navigation/SecondaryNav/index.tsx index 3f631f809..42266f915 100644 --- a/components/Footer/Navigation/SecondaryNav/index.tsx +++ b/components/Footer/Navigation/SecondaryNav/index.tsx @@ -56,26 +56,13 @@ export default function FooterSecondaryNav({
      {link?.links?.map((link) => (
    • - {link.isExternal ? ( - - {link.title} - - ) : ( - - {link.title} - - )} + + {link.title} +
    • ))}
    diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx index 116934531..3b4391998 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/ClearSearchButton/index.tsx @@ -33,7 +33,7 @@ export default function ClearSearchButton({ type="button" > - + {intl.formatMessage({ id: "Clear searches" })} diff --git a/components/Forms/BookingWidget/FormContent/Search/SearchList/Dialog/dialog.module.css b/components/Forms/BookingWidget/FormContent/Search/SearchList/Dialog/dialog.module.css index e1ca8f23d..e41253da6 100644 --- a/components/Forms/BookingWidget/FormContent/Search/SearchList/Dialog/dialog.module.css +++ b/components/Forms/BookingWidget/FormContent/Search/SearchList/Dialog/dialog.module.css @@ -1,23 +1,18 @@ .dialog { background-color: var(--Base-Surface-Primary-light-Normal); border-radius: var(--Corner-radius-Large); - box-shadow: 0 0 14px 6px rgba(0, 0, 0, 0.1); + display: flex; flex-direction: column; left: 0; list-style: none; - max-height: 380px; overflow-y: auto; padding: var(--Spacing-x2) var(--Spacing-x3); - position: absolute; - /** - * var(--Spacing-x4) to account for padding inside - * the bookingwidget and to add the padding for the - * box itself - */ - top: calc(100% + var(--Spacing-x4)); - width: 360px; - z-index: 99; + position: fixed; + top: 170px; + width: 100%; + height: calc(100% - 200px); + z-index: 10010; } .default { @@ -31,3 +26,20 @@ .search { gap: var(--Spacing-x3); } + +@media (min-width: 768px) { + .dialog { + position: absolute; + width: 360px; + /** + * var(--Spacing-x4) to account for padding inside + * the bookingwidget and to add the padding for the + * box itself + */ + top: calc(100% + var(--Spacing-x4)); + z-index: 99; + box-shadow: 0 0 14px 6px rgba(0, 0, 0, 0.1); + max-height: 380px; + height: auto; + } +} diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index 98c5b36e8..12d5e740c 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -10,7 +10,6 @@ import { import { useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" -import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Input from "../Input" @@ -49,15 +48,6 @@ export default function Search({ locations }: SearchProps) { dispatch({ type: ActionType.CLEAR_HISTORY_LOCATIONS }) } - function handleOnBlur() { - if (!value && state.searchData?.name) { - setValue(name, state.searchData.name) - // Always need to manually trigger - // revalidation when setting value r-h-f - trigger() - } - } - function handleOnChange( evt: FormEvent | ChangeEvent ) { @@ -139,7 +129,9 @@ export default function Search({ locations }: SearchProps) {
    @@ -155,7 +147,6 @@ export default function Search({ locations }: SearchProps) { }), ...register(name, { onBlur: function () { - handleOnBlur() closeMenu() }, onChange: handleOnChange, diff --git a/components/Forms/BookingWidget/FormContent/Search/search.module.css b/components/Forms/BookingWidget/FormContent/Search/search.module.css index 7aa9cda02..0b9ecfa8c 100644 --- a/components/Forms/BookingWidget/FormContent/Search/search.module.css +++ b/components/Forms/BookingWidget/FormContent/Search/search.module.css @@ -4,6 +4,7 @@ border-width: 1px; border-radius: var(--Corner-radius-Small); padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + position: relative; } .container:hover, @@ -23,8 +24,3 @@ p { color: var(--UI-Text-Active); } - -.container:hover:has(input:not(:active, :focus, :focus-within)) - input::-webkit-search-cancel-button { - display: none; -} diff --git a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx index 0a9841a43..067f94c0c 100644 --- a/components/Forms/BookingWidget/FormContent/Voucher/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Voucher/index.tsx @@ -1,7 +1,7 @@ "use client" import { useIntl } from "react-intl" -import Body from "@/components/TempDesignSystem/Text/Body" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Caption from "@/components/TempDesignSystem/Text/Caption" import { Tooltip } from "@/components/TempDesignSystem/Tooltip" @@ -34,7 +34,7 @@ export default function Voucher() { >
    @@ -60,17 +60,12 @@ export default function FormContent({
    + + {count} + + +
    + ) +} diff --git a/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx new file mode 100644 index 000000000..99c1324ab --- /dev/null +++ b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx @@ -0,0 +1,136 @@ +"use client" +import { useFormContext } from "react-hook-form" +import { useIntl } from "react-intl" + +import { useGuestsRoomsStore } from "@/stores/guests-rooms" + +import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" +import Button from "../TempDesignSystem/Button" +import Divider from "../TempDesignSystem/Divider" +import Subtitle from "../TempDesignSystem/Text/Subtitle" +import { Tooltip } from "../TempDesignSystem/Tooltip" +import AdultSelector from "./AdultSelector" +import ChildSelector from "./ChildSelector" + +import styles from "./guests-rooms-picker.module.css" + +import { BookingWidgetSchema } from "@/types/components/bookingWidget" +import { GuestsRoomsPickerProps } from "@/types/components/bookingWidget/guestsRoomsPicker" + +export default function GuestsRoomsPicker({ + closePicker, +}: GuestsRoomsPickerProps) { + const intl = useIntl() + const doneLabel = intl.formatMessage({ id: "Done" }) + const roomLabel = intl.formatMessage({ id: "Room" }) + const disabledBookingOptionsHeader = intl.formatMessage({ + id: "Disabled booking options header", + }) + const disabledBookingOptionsText = intl.formatMessage({ + id: "Disabled booking options text", + }) + const addRoomLabel = intl.formatMessage({ id: "Add Room" }) + + const { getFieldState } = useFormContext() + + const rooms = useGuestsRoomsStore((state) => state.rooms) + + // Not in MVP + // const increaseRoom = useGuestsRoomsStore.use.increaseRoom() + // const decreaseRoom = useGuestsRoomsStore.use.decreaseRoom() + + return ( +
    +
    + +
    +
    + {rooms.map((room, index) => ( +
    +
    + + {roomLabel} {index + 1} + + + +
    + {/* Not in MVP + {index > 0 ? ( + + ) : null} */} + +
    + ))} +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    +
    +
    +
    + + {rooms.length < 4 ? ( + + ) : null} + +
    + + +
    +
    + ) +} diff --git a/components/GuestsRoomsPicker/guests-rooms-picker.module.css b/components/GuestsRoomsPicker/guests-rooms-picker.module.css new file mode 100644 index 000000000..971699d95 --- /dev/null +++ b/components/GuestsRoomsPicker/guests-rooms-picker.module.css @@ -0,0 +1,148 @@ +.container { + overflow: hidden; + position: relative; + &[data-isopen="true"] { + overflow: visible; + } +} +.roomContainer { + display: grid; + gap: var(--Spacing-x2); +} +.roomDetailsContainer { + display: grid; + gap: var(--Spacing-x2); + padding-bottom: var(--Spacing-x1); +} +.hideWrapper { + background-color: var(--Main-Grey-White); +} +.roomHeading { + margin-bottom: var(--Spacing-x1); +} +.btn { + background: none; + border: none; + cursor: pointer; + outline: none; + padding: 0; + width: 100%; +} +.body { + opacity: 0.8; +} +.footer { + display: grid; + gap: var(--Spacing-x1); + grid-template-columns: auto; + margin-top: var(--Spacing-x2); +} + +@media screen and (max-width: 1366px) { + .hideWrapper { + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 100%; + transition: top 300ms ease; + z-index: 10002; + overflow: hidden; + } + + .container[data-isopen="true"] .hideWrapper { + top: 20px; + } + + .pickerContainer { + --header-height: 72px; + --sticky-button-height: 140px; + display: grid; + grid-template-areas: + "header" + "content"; + grid-template-rows: var(--header-height) calc(100dvh - var(--header-height)); + position: relative; + } + .contentContainer { + grid-area: content; + overflow-y: scroll; + scroll-snap-type: y mandatory; + } + + .header { + background-color: var(--Main-Grey-White); + display: grid; + grid-area: header; + padding: var(--Spacing-x3) var(--Spacing-x2); + } + + .close { + background: none; + border: none; + cursor: pointer; + display: flex; + justify-self: flex-end; + padding: 0; + } + + .roomContainer { + padding: 0 var(--Spacing-x2); + } + .roomContainer:last-of-type { + padding-bottom: calc(var(--sticky-button-height) + 20px); + } + + .footer { + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 7.5%, + #ffffff 82.5% + ); + padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x7); + position: sticky; + bottom: 0; + width: 100%; + z-index: 10; + } + + .footer .hideOnMobile { + display: none; + } + + .addRoomMobileContainer { + display: grid; + width: 150px; + margin: 0 auto; + padding-bottom: calc(var(--sticky-button-height) + 20px); + } +} + +@media screen and (min-width: 1367px) { + .hideWrapper { + border-radius: var(--Corner-radius-Large); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + left: calc((var(--Spacing-x1) + var(--Spacing-x2)) * -1); + max-width: calc(100vw - 20px); + padding: var(--Spacing-x2) var(--Spacing-x3); + position: absolute; + top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4)); + width: 360px; + max-height: calc(100dvh - 77px - var(--Spacing-x6)); + overflow-y: auto; + } + + .header { + display: none; + } + + .footer { + grid-template-columns: auto auto; + } + + .footer .hideOnDesktop, + .addRoomMobileContainer { + display: none; + } +} diff --git a/components/GuestsRoomsPicker/index.tsx b/components/GuestsRoomsPicker/index.tsx new file mode 100644 index 000000000..f12ecebde --- /dev/null +++ b/components/GuestsRoomsPicker/index.tsx @@ -0,0 +1,80 @@ +"use client" + +import { useCallback, useEffect, useRef, useState } from "react" +import { useIntl } from "react-intl" + +import { useGuestsRoomsStore } from "@/stores/guests-rooms" + +import { guestRoomsSchema } from "@/components/Forms/BookingWidget/schema" +import Body from "@/components/TempDesignSystem/Text/Body" + +import GuestsRoomsPicker from "./GuestsRoomsPicker" + +import styles from "./guests-rooms-picker.module.css" + +export default function GuestsRoomsPickerForm() { + const intl = useIntl() + const [isOpen, setIsOpen] = useState(false) + const { rooms, adultCount, childCount, setIsValidated } = useGuestsRoomsStore( + (state) => ({ + rooms: state.rooms, + adultCount: state.adultCount, + childCount: state.childCount, + setIsValidated: state.setIsValidated, + }) + ) + const ref = useRef(null) + function handleOnClick() { + setIsOpen((prevIsOpen) => !prevIsOpen) + } + const closePicker = useCallback(() => { + const guestRoomsValidData = guestRoomsSchema.safeParse(rooms) + if (guestRoomsValidData.success) { + setIsOpen(false) + setIsValidated(false) + } else { + setIsValidated(true) + } + }, [rooms, setIsValidated, setIsOpen]) + + useEffect(() => { + function handleClickOutside(evt: Event) { + const target = evt.target as HTMLElement + if (ref.current && target && !ref.current.contains(target)) { + closePicker() + } + } + document.addEventListener("click", handleClickOutside) + return () => { + document.removeEventListener("click", handleClickOutside) + } + }, [closePicker]) + + return ( +
    + +
    + +
    +
    + ) +} diff --git a/components/HotelReservation/ReadMore/Contact/contact.module.css b/components/HotelReservation/Contact/contact.module.css similarity index 93% rename from components/HotelReservation/ReadMore/Contact/contact.module.css rename to components/HotelReservation/Contact/contact.module.css index 53f6e01f3..768e01ca2 100644 --- a/components/HotelReservation/ReadMore/Contact/contact.module.css +++ b/components/HotelReservation/Contact/contact.module.css @@ -32,8 +32,8 @@ } .ecoLabel { - display: grid; - grid-template-columns: auto 1fr; + display: flex; + align-items: center; column-gap: var(--Spacing-x-one-and-half); grid-column: 2 / 3; grid-row: 3 / 4; diff --git a/components/HotelReservation/ReadMore/Contact/index.tsx b/components/HotelReservation/Contact/index.tsx similarity index 84% rename from components/HotelReservation/ReadMore/Contact/index.tsx rename to components/HotelReservation/Contact/index.tsx index 46491fb3d..2f4e8ccd5 100644 --- a/components/HotelReservation/ReadMore/Contact/index.tsx +++ b/components/HotelReservation/Contact/index.tsx @@ -24,20 +24,26 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Address" })} - {hotel.address.streetAddress} - {hotel.address.city} + + {`${hotel.address.streetAddress}, ${hotel.address.city}`} +
  • {intl.formatMessage({ id: "Driving directions" })} - {intl.formatMessage({ id: "Google Maps" })} + + Google Maps +
  • {intl.formatMessage({ id: "Email" })} - + {hotel.contactInformation.email}
  • @@ -45,7 +51,10 @@ export default function Contact({ hotel }: ContactProps) { {intl.formatMessage({ id: "Contact us" })} - + {hotel.contactInformation.phoneNumber} diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 955ce740b..106aaa80a 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -1,9 +1,12 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" +import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { useEnterDetailsStore } from "@/stores/enter-details" + import { KingBedIcon } from "@/components/Icons" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" @@ -16,8 +19,14 @@ import { bedTypeEnum } from "@/types/enums/bedType" export default function BedType() { const intl = useIntl() + const bedType = useEnterDetailsStore((state) => state.data.bedType) const methods = useForm({ + defaultValues: bedType + ? { + bedType, + } + : undefined, criteriaMode: "all", mode: "all", resolver: zodResolver(bedTypeSchema), @@ -28,15 +37,32 @@ export default function BedType() { { id: "Included (based on availability)" }, { b: (str) => {str} } ) + const completeStep = useEnterDetailsStore((state) => state.completeStep) + + const onSubmit = useCallback( + (values: BedTypeSchema) => { + completeStep(values) + }, + [completeStep] + ) + + useEffect(() => { + if (methods.formState.isSubmitting) { + return + } + + const subscription = methods.watch(() => methods.handleSubmit(onSubmit)()) + return () => subscription.unsubscribe() + }, [methods, onSubmit]) return ( - + state.data.breakfast) + const methods = useForm({ + defaultValues: breakfast ? { breakfast } : undefined, criteriaMode: "all", mode: "all", resolver: zodResolver(breakfastSchema), reValidateMode: "onChange", }) + const completeStep = useEnterDetailsStore((state) => state.completeStep) + + const onSubmit = useCallback( + (values: BreakfastSchema) => { + completeStep(values) + }, + [completeStep] + ) + + useEffect(() => { + if (methods.formState.isSubmitting) { + return + } + const subscription = methods.watch(() => methods.handleSubmit(onSubmit)()) + return () => subscription.unsubscribe() + }, [methods, onSubmit]) + return ( - + { + // In order to avoid hydration errors the state needs to be set as side effect, + // since the join value can come from search params + setIsJoinChecked(joinValue) + }, [joinValue]) + + const list = [ + { title: intl.formatMessage({ id: "Earn bonus nights & points" }) }, + { title: intl.formatMessage({ id: "Get member benefits & offers" }) }, + { title: intl.formatMessage({ id: "Join at no cost" }) }, + ] + + return ( +
    + + {isJoinChecked ? ( +
    +
    +
    + + {intl.formatMessage({ id: "Birth date" })} * + +
    + + +
    +
    + + + {intl.formatMessage({ + id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", + })}{" "} + + {intl.formatMessage({ id: "Scandic's Privacy Policy." })} + + + +
    +
    + ) : null} +
    + ) +} diff --git a/components/HotelReservation/EnterDetails/Details/Signup/signup.module.css b/components/HotelReservation/EnterDetails/Details/Signup/signup.module.css new file mode 100644 index 000000000..1db11dfb8 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Details/Signup/signup.module.css @@ -0,0 +1,15 @@ +.container { + display: grid; + grid-column: 1/-1; + gap: var(--Spacing-x3); +} + +.additionalFormData { + display: grid; + gap: var(--Spacing-x4); +} + +.dateField { + display: grid; + gap: var(--Spacing-x1); +} diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 15792e384..35c16304b 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -1,16 +1,21 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" +import { useCallback } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { useEnterDetailsStore } from "@/stores/enter-details" + +import { registerUserBookingFlow } from "@/actions/registerUserBookingFlow" import Button from "@/components/TempDesignSystem/Button" -import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" import Phone from "@/components/TempDesignSystem/Form/Phone" import Body from "@/components/TempDesignSystem/Text/Body" +import { toast } from "@/components/TempDesignSystem/Toasts" import { detailsSchema, signedInDetailsSchema } from "./schema" +import Signup from "./Signup" import styles from "./details.module.css" @@ -19,22 +24,33 @@ import type { DetailsSchema, } from "@/types/components/enterDetails/details" +const formID = "enter-details" export default function Details({ user }: DetailsProps) { const intl = useIntl() - - const list = [ - { title: intl.formatMessage({ id: "Earn bonus nights & points" }) }, - { title: intl.formatMessage({ id: "Get member benefits & offers" }) }, - { title: intl.formatMessage({ id: "Join at no cost" }) }, - ] + const initialData = useEnterDetailsStore((state) => ({ + countryCode: state.data.countryCode, + email: state.data.email, + firstName: state.data.firstName, + lastName: state.data.lastName, + phoneNumber: state.data.phoneNumber, + join: state.data.join, + dateOfBirth: state.data.dateOfBirth, + zipCode: state.data.zipCode, + termsAccepted: state.data.termsAccepted, + })) const methods = useForm({ defaultValues: { - countryCode: user?.address?.countryCode ?? "", - email: user?.email ?? "", - firstname: user?.firstName ?? "", - lastname: user?.lastName ?? "", - phoneNumber: user?.phoneNumber ?? "", + countryCode: user?.address?.countryCode ?? initialData.countryCode, + email: user?.email ?? initialData.email, + firstName: user?.firstName ?? initialData.firstName, + lastName: user?.lastName ?? initialData.lastName, + phoneNumber: user?.phoneNumber ?? initialData.phoneNumber, + //@ts-expect-error: We use a literal for join to be true or false, which does not convert to a boolean + join: initialData.join, + dateOfBirth: initialData.dateOfBirth, + zipCode: initialData.zipCode, + termsAccepted: initialData.termsAccepted, }, criteriaMode: "all", mode: "all", @@ -42,6 +58,44 @@ export default function Details({ user }: DetailsProps) { reValidateMode: "onChange", }) + const completeStep = useEnterDetailsStore((state) => state.completeStep) + + // const errorMessage = intl.formatMessage({ + // id: "An error occurred. Please try again.", + // }) + + const onSubmit = useCallback( + async function (values: DetailsSchema) { + if (values.join) { + const signupVals = { + firstName: values.firstName, + lastName: values.lastName, + email: values.email, + phoneNumber: values.phoneNumber, + address: { + zipCode: values.zipCode, + countryCode: values.countryCode, + }, + dateOfBirth: values.dateOfBirth, + } + + const res = await registerUserBookingFlow(signupVals) + if (!res.success) { + // if (res.error) { + // toast.error(res.error) + // } else { + // toast.error(errorMessage) + // } + return + } + console.log("Signed up user: ", res) + } + completeStep(values) + }, + + [completeStep] + ) + return (
    @@ -50,16 +104,20 @@ export default function Details({ user }: DetailsProps) { {intl.formatMessage({ id: "Guest information" })} - + @@ -84,31 +142,16 @@ export default function Details({ user }: DetailsProps) { readOnly={!!user} registerOptions={{ required: true }} /> + {user ? null : }
    - {user ? null : ( - - )} diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 92f1a5629..039b22a61 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -2,18 +2,49 @@ import { z } from "zod" import { phoneValidator } from "@/utils/phoneValidator" -export const detailsSchema = z.object({ +export const baseDetailsSchema = z.object({ countryCode: z.string(), email: z.string().email(), - firstname: z.string(), - lastname: z.string(), + firstName: z.string(), + lastName: z.string(), phoneNumber: phoneValidator(), }) +export const notJoinDetailsSchema = baseDetailsSchema.merge( + z.object({ + join: z.literal(false), + zipCode: z.string().optional(), + dateOfBirth: z.string().optional(), + termsAccepted: z.boolean().default(false), + }) +) + +export const joinDetailsSchema = baseDetailsSchema.merge( + z.object({ + join: z.literal(true), + zipCode: z.string().min(1, { message: "Zip code is required" }), + dateOfBirth: z.string(), + termsAccepted: z.literal(true, { + errorMap: (err, ctx) => { + switch (err.code) { + case "invalid_literal": + return { message: "You must accept the terms and conditions" } + } + return { message: ctx.defaultError } + }, + }), + }) +) + +export const detailsSchema = z.discriminatedUnion("join", [ + notJoinDetailsSchema, + joinDetailsSchema, +]) + export const signedInDetailsSchema = z.object({ countryCode: z.string().optional(), email: z.string().email().optional(), - firstname: z.string().optional(), - lastname: z.string().optional(), + firstName: z.string().optional(), + lastName: z.string().optional(), phoneNumber: phoneValidator().optional(), }) diff --git a/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx new file mode 100644 index 000000000..0ce1b1080 --- /dev/null +++ b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useCallback, useEffect } from "react" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +export default function HistoryStateManager() { + const setCurrentStep = useEnterDetailsStore((state) => state.setCurrentStep) + const currentStep = useEnterDetailsStore((state) => state.currentStep) + + const handleBackButton = useCallback( + (event: PopStateEvent) => { + if (event.state.step) { + setCurrentStep(event.state.step) + } + }, + [setCurrentStep] + ) + + useEffect(() => { + window.addEventListener("popstate", handleBackButton) + + return () => { + window.removeEventListener("popstate", handleBackButton) + } + }, [handleBackButton]) + + useEffect(() => { + if (!window.history.state.step) { + window.history.replaceState( + { step: currentStep }, + "", + document.location.href + ) + } + }, [currentStep]) + + return null +} diff --git a/components/HotelReservation/EnterDetails/Provider/index.tsx b/components/HotelReservation/EnterDetails/Provider/index.tsx new file mode 100644 index 000000000..6e4d6cc2c --- /dev/null +++ b/components/HotelReservation/EnterDetails/Provider/index.tsx @@ -0,0 +1,26 @@ +"use client" +import { PropsWithChildren, useRef } from "react" + +import { + EnterDetailsContext, + type EnterDetailsStore, + initEditDetailsState, +} from "@/stores/enter-details" + +import { StepEnum } from "@/types/components/enterDetails/step" + +export default function EnterDetailsProvider({ + step, + children, +}: PropsWithChildren<{ step: StepEnum }>) { + const initialStore = useRef() + if (!initialStore.current) { + initialStore.current = initEditDetailsState(step) + } + + return ( + + {children} + + ) +} diff --git a/components/HotelReservation/SelectRate/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx similarity index 53% rename from components/HotelReservation/SelectRate/SectionAccordion/index.tsx rename to components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index a56902248..52aa11685 100644 --- a/components/HotelReservation/SelectRate/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -1,9 +1,11 @@ "use client" -import { useEffect, useRef } from "react" +import { useEffect, useRef, useState } from "react" import { useIntl } from "react-intl" +import { useEnterDetailsStore } from "@/stores/enter-details" + import { CheckIcon, ChevronDownIcon } from "@/components/Icons" -import Link from "@/components/TempDesignSystem/Link" +import Button from "@/components/TempDesignSystem/Button" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -13,46 +15,36 @@ import { SectionAccordionProps } from "@/types/components/hotelReservation/selec export default function SectionAccordion({ header, - isOpen, - isCompleted, label, - path, + step, children, }: React.PropsWithChildren) { const intl = useIntl() + const currentStep = useEnterDetailsStore((state) => state.currentStep) + const [isComplete, setIsComplete] = useState(false) + const [isOpen, setIsOpen] = useState(false) - const contentRef = useRef(null) - const circleRef = useRef(null) + const isValid = useEnterDetailsStore((state) => state.isValid[step]) + const navigate = useEnterDetailsStore((state) => state.navigate) useEffect(() => { - const content = contentRef.current - const circle = circleRef.current - if (content) { - if (isOpen) { - content.style.maxHeight = `${content.scrollHeight}px` - } else { - content.style.maxHeight = "0" - } - } + // We need to set the state on mount because of hydration errors + setIsComplete(isValid) + }, [isValid]) - if (circle) { - if (isOpen) { - circle.style.backgroundColor = `var(--UI-Text-Placeholder);` - } else { - circle.style.backgroundColor = `var(--Base-Surface-Subtle-Hover);` - } - } - }, [isOpen]) + useEffect(() => { + setIsOpen(currentStep === step) + }, [currentStep, step]) + + function onModify() { + navigate(step) + } return ( -
    +
    -
    - {isCompleted ? ( +
    + {isComplete ? ( ) : null}
    @@ -63,6 +55,7 @@ export default function SectionAccordion({

    {header}

    @@ -75,16 +68,21 @@ export default function SectionAccordion({ {label}
    - {isCompleted && !isOpen && ( - + {isComplete && !isOpen && ( + )} -
    - {children} -
    +
    {children}
    ) diff --git a/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css similarity index 87% rename from components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css rename to components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 653c98fad..93398378d 100644 --- a/components/HotelReservation/SelectRate/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -17,13 +17,18 @@ border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } +.wrapper:last-child .main { + border-bottom: none; +} + .main { - display: flex; - flex-direction: column; + display: grid; gap: var(--Spacing-x3); width: 100%; border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-bottom: var(--Spacing-x3); + transition: 0.4s ease-out; + grid-template-rows: 2em 0fr; } .headerContainer { @@ -66,8 +71,10 @@ background-color: var(--Base-Surface-Subtle-Hover); } +.wrapper[data-open="true"] .main { + grid-template-rows: 2em 1fr; +} + .content { overflow: hidden; - transition: max-height 0.4s ease-out; - max-height: 0; } diff --git a/components/HotelReservation/EnterDetails/SidePeek/enterDetailsSidePeek.module.css b/components/HotelReservation/EnterDetails/SidePeek/enterDetailsSidePeek.module.css new file mode 100644 index 000000000..be80e5dbd --- /dev/null +++ b/components/HotelReservation/EnterDetails/SidePeek/enterDetailsSidePeek.module.css @@ -0,0 +1,5 @@ +.spacing { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} diff --git a/components/HotelReservation/EnterDetails/SidePeek/index.tsx b/components/HotelReservation/EnterDetails/SidePeek/index.tsx new file mode 100644 index 000000000..f7d280fed --- /dev/null +++ b/components/HotelReservation/EnterDetails/SidePeek/index.tsx @@ -0,0 +1,46 @@ +"use client" + +import { useIntl } from "react-intl" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +import Contact from "@/components/HotelReservation/Contact" +import Divider from "@/components/TempDesignSystem/Divider" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" + +import styles from "./enterDetailsSidePeek.module.css" + +import { + SidePeekEnum, + SidePeekProps, +} from "@/types/components/enterDetails/sidePeek" + +export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) { + const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek) + const close = useEnterDetailsStore((state) => state.closeSidePeek) + + const intl = useIntl() + return ( + +
    + + +
    + {hotel.hotelContent.texts.descriptions.medium} + {hotel.hotelContent.texts.facilityInformation + .split(/[\n\r]/g) + .filter((p) => p) + .map((paragraph, idx) => ( + {paragraph} + ))} +
    +
    +
    + ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx b/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx new file mode 100644 index 000000000..2f022f857 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Summary/ToggleSidePeek.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useIntl } from "react-intl" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +import { ChevronRightSmallIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" + +export default function ToggleSidePeek() { + const intl = useIntl() + const openSidePeek = useEnterDetailsStore((state) => state.openSidePeek) + + return ( + + ) +} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 8efc418a1..18121dad3 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -1,13 +1,14 @@ import { dt } from "@/lib/dt" -import { ArrowRightIcon, ChevronRightSmallIcon } from "@/components/Icons" +import { ArrowRightIcon } from "@/components/Icons" import Divider from "@/components/TempDesignSystem/Divider" -import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import ToggleSidePeek from "./ToggleSidePeek" + import styles from "./summary.module.css" // TEMP @@ -21,7 +22,6 @@ const rooms = [ export default async function Summary() { const intl = await getIntl() const lang = getLang() - const fromDate = dt().locale(lang).format("ddd, D MMM") const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM") const diff = dt(toDate).diff(fromDate, "days") @@ -75,19 +75,8 @@ export default async function Summary() { {toDate} - - {intl.formatMessage({ id: "See room details" })} - - + +
    diff --git a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/hotelDetailSidePeek.module.css b/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/hotelDetailSidePeek.module.css deleted file mode 100644 index fb400465e..000000000 --- a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/hotelDetailSidePeek.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.buttons { - display: flex; - gap: var(--Spacing-x3); -} diff --git a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/index.tsx b/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/index.tsx deleted file mode 100644 index d4e3eb829..000000000 --- a/components/HotelReservation/HotelSelectionHeader/HotelDetailSidePeek/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import { useState } from "react" -import { useIntl } from "react-intl" - -import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall" -import Button from "@/components/TempDesignSystem/Button" -import SidePeek from "@/components/TempDesignSystem/SidePeek" - -import styles from "./hotelDetailSidePeek.module.css" - -export default function HotelDetailSidePeek() { - const intl = useIntl() - const [isOpen, setIsOpen] = useState(false) - - function toggleSidePeek() { - setIsOpen(!isOpen) - } - - return ( - <> -
    - - -
    - setIsOpen(false)} - > -
    TBD
    -
    - - ) -} diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/components/HotelReservation/HotelSelectionHeader/index.tsx index 222dceda1..69b4134eb 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/components/HotelReservation/HotelSelectionHeader/index.tsx @@ -6,8 +6,6 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" -import HotelDetailSidePeek from "./HotelDetailSidePeek" - import styles from "./hotelSelectionHeader.module.css" import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader" @@ -46,7 +44,6 @@ export default function HotelSelectionHeader({ {hotel.hotelContent.texts.descriptions.short} -
    diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index e988ec40d..5814eae54 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -11,7 +11,7 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek" import Body from "@/components/TempDesignSystem/Text/Body" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Contact from "./Contact" +import Contact from "../Contact" import styles from "./readMore.module.css" diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/index.tsx b/components/HotelReservation/SelectRate/Payment/PaymentOption/index.tsx new file mode 100644 index 000000000..a44501ae4 --- /dev/null +++ b/components/HotelReservation/SelectRate/Payment/PaymentOption/index.tsx @@ -0,0 +1,43 @@ +import Image from "next/image" +import { useFormContext } from "react-hook-form" + +import { PAYMENT_METHOD_ICONS } from "@/constants/booking" + +import Body from "@/components/TempDesignSystem/Text/Body" + +import { PaymentOptionProps } from "./paymentOption" + +import styles from "./paymentOption.module.css" + +export default function PaymentOption({ + name, + value, + label, +}: PaymentOptionProps) { + const { register } = useFormContext() + return ( + + ) +} diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css b/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css new file mode 100644 index 000000000..9d9e72248 --- /dev/null +++ b/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css @@ -0,0 +1,37 @@ +.paymentOption { + position: relative; + background-color: var(--UI-Input-Controls-Surface-Normal); + padding: var(--Spacing-x3); + border: 1px solid var(--Base-Border-Normal); + border-radius: var(--Corner-radius-Medium); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--Spacing-x2); + cursor: pointer; +} + +.paymentOption .radio { + width: 24px; + height: 24px; + border: 1px solid var(--Base-Border-Normal); + border-radius: 50%; + cursor: pointer; +} + +.paymentOption input:checked + .radio { + border: 8px solid var(--UI-Input-Controls-Fill-Selected); +} + +.titleContainer { + display: flex; + align-items: center; + gap: var(--Spacing-x-one-and-half); + pointer-events: none; +} + +.paymentOptionIcon { + position: absolute; + right: var(--Spacing-x3); + top: calc(50% - 16px); +} diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts b/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts new file mode 100644 index 000000000..5d77f6560 --- /dev/null +++ b/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts @@ -0,0 +1,10 @@ +import { RegisterOptions } from "react-hook-form" + +import { PaymentMethodEnum } from "@/constants/booking" + +export interface PaymentOptionProps { + name: string + value: PaymentMethodEnum + label: string + registerOptions?: RegisterOptions +} diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/SelectRate/Payment/index.tsx index 8216e8631..b1bddd84c 100644 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ b/components/HotelReservation/SelectRate/Payment/index.tsx @@ -1,21 +1,36 @@ "use client" +import { zodResolver } from "@hookform/resolvers/zod" import { useRouter } from "next/navigation" import { useEffect, useState } from "react" +import { Label as AriaLabel } from "react-aria-components" +import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { BOOKING_CONFIRMATION_NUMBER, BookingStatusEnum, + PAYMENT_METHOD_TITLES, + PaymentMethodEnum, } from "@/constants/booking" +import { + bookingTermsAndConditions, + privacyPolicy, +} from "@/constants/currentWebHrefs" import { trpc } from "@/lib/trpc/client" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" +import Checkbox from "@/components/TempDesignSystem/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import Caption from "@/components/TempDesignSystem/Text/Caption" import { toast } from "@/components/TempDesignSystem/Toasts" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import useLang from "@/hooks/useLang" +import PaymentOption from "./PaymentOption" +import { PaymentFormData, paymentSchema } from "./schema" + import styles from "./payment.module.css" import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" @@ -28,17 +43,21 @@ export default function Payment({ hotel }: PaymentProps) { const lang = useLang() const intl = useIntl() const [confirmationNumber, setConfirmationNumber] = useState("") - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState("") + + const methods = useForm({ + defaultValues: { + paymentMethod: PaymentMethodEnum.card, + smsConfirmation: false, + termsAndConditions: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(paymentSchema), + }) const initiateBooking = trpc.booking.booking.create.useMutation({ onSuccess: (result) => { if (result?.confirmationNumber) { - // Planet doesn't support query params so we have to store values in session storage - sessionStorage.setItem( - BOOKING_CONFIRMATION_NUMBER, - result.confirmationNumber - ) - setConfirmationNumber(result.confirmationNumber) } else { // TODO: add proper error message @@ -60,12 +79,14 @@ export default function Payment({ hotel }: PaymentProps) { ) useEffect(() => { - if (bookingStatus?.data?.paymentUrl) { + if (confirmationNumber && bookingStatus?.data?.paymentUrl) { + // Planet doesn't support query params so we have to store values in session storage + sessionStorage.setItem(BOOKING_CONFIRMATION_NUMBER, confirmationNumber) router.push(bookingStatus.data.paymentUrl) } - }, [bookingStatus, router]) + }, [confirmationNumber, bookingStatus, router]) - function handleSubmit() { + function handleSubmit(data: PaymentFormData) { initiateBooking.mutate({ hotelId: hotel.operaId, checkInDate: "2024-12-10", @@ -91,11 +112,11 @@ export default function Payment({ hotel }: PaymentProps) { petFriendly: true, accessibility: true, }, - smsConfirmationRequested: true, + smsConfirmationRequested: data.smsConfirmation, }, ], payment: { - paymentMethod: selectedPaymentMethod, + paymentMethod: data.paymentMethod, cardHolder: { email: "test.user@scandichotels.com", name: "Test User", @@ -117,45 +138,80 @@ export default function Payment({ hotel }: PaymentProps) { } return ( -
    -
    -
    - + +
    +
    + {hotel.merchantInformationData.alternatePaymentOptions.map( - (paymentOption) => ( - + (paymentMethod) => ( + ) )}
    -
    - -
    + + + {intl.formatMessage({ + id: "I would like to get my booking confirmation via sms", + })} + + + + + + + {intl.formatMessage( + { + id: "booking.terms", + }, + { + termsLink: (str) => ( + + {str} + + ), + privacyLink: (str) => ( + + {str} + + ), + } + )} + + + + + ) } diff --git a/components/HotelReservation/SelectRate/Payment/payment.module.css b/components/HotelReservation/SelectRate/Payment/payment.module.css index 9200ce8f3..d55cfd359 100644 --- a/components/HotelReservation/SelectRate/Payment/payment.module.css +++ b/components/HotelReservation/SelectRate/Payment/payment.module.css @@ -1,18 +1,27 @@ -.paymentItemContainer { - max-width: 480px; +.paymentContainer { display: flex; flex-direction: column; - gap: var(--Spacing-x1); - padding-bottom: var(--Spacing-x4); + gap: var(--Spacing-x3); + max-width: 480px; } -.paymentItem { - background-color: var(--Base-Background-Normal); - padding: var(--Spacing-x3); - border: 1px solid var(--Base-Border-Normal); - border-radius: var(--Corner-radius-Medium); +.paymentOptionContainer { display: flex; - align-items: center; - gap: var(--Spacing-x2); - cursor: pointer; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.submitButton { + align-self: flex-start; +} + +.paymentContainer .link { + font-weight: 500; + font-size: var(--Typography-Caption-Regular-fontSize); +} + +.terms { + display: flex; + flex-direction: row; + gap: var(--Spacing-x-one-and-half); } diff --git a/components/HotelReservation/SelectRate/Payment/schema.ts b/components/HotelReservation/SelectRate/Payment/schema.ts new file mode 100644 index 000000000..ccade33c5 --- /dev/null +++ b/components/HotelReservation/SelectRate/Payment/schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod" + +import { PaymentMethodEnum } from "@/constants/booking" + +export const paymentSchema = z.object({ + paymentMethod: z.nativeEnum(PaymentMethodEnum), + smsConfirmation: z.boolean(), + termsAndConditions: z.boolean().refine((value) => value === true, { + message: "You must accept the terms and conditions", + }), +}) + +export interface PaymentFormData extends z.output {} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx new file mode 100644 index 000000000..62979d2c7 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx @@ -0,0 +1,36 @@ +import { Button, Dialog, OverlayArrow, Popover } from "react-aria-components" + +import { CloseIcon } from "@/components/Icons" + +import styles from "./popover.module.css" + +import { PricePopoverProps } from "@/types/components/hotelReservation/selectRate/pricePopover" + +export default function PricePopover({ + children, + ...props +}: PricePopoverProps) { + return ( + + + + + + + + + {children} + + + ) +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css new file mode 100644 index 000000000..bb60ba100 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css @@ -0,0 +1,12 @@ +.arrow { + top: -6px; +} + +.closeButton { + position: absolute; + top: 5px; + right: 5px; + background: none; + border: none; + cursor: pointer; +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx new file mode 100644 index 000000000..76f366adb --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/index.tsx @@ -0,0 +1,99 @@ +import { useIntl } from "react-intl" + +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./priceList.module.css" + +import { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" + +export default function PriceList({ + publicPrice = {}, + memberPrice = {}, +}: PriceListProps) { + const intl = useIntl() + + const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = + publicPrice + const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = + memberPrice + + const showRequestedPrice = publicRequestedPrice && memberRequestedPrice + + return ( +
    +
    +
    + + {intl.formatMessage({ id: "Standard price" })} + +
    +
    + {publicLocalPrice ? ( +
    + + {publicLocalPrice.pricePerNight} + + + {publicLocalPrice.currency} + +
    + ) : ( + + {intl.formatMessage({ id: "n/a" })} + + )} +
    +
    + +
    +
    + + {intl.formatMessage({ id: "Member price" })} + +
    +
    + {memberLocalPrice ? ( +
    + + {memberLocalPrice.pricePerNight} + + + {memberLocalPrice.currency} + +
    + ) : ( + + - {intl.formatMessage({ id: "Currency Code" })} + + )} +
    +
    + +
    +
    + + {intl.formatMessage({ id: "Approx." })} + +
    +
    + {showRequestedPrice ? ( + + {publicRequestedPrice.pricePerNight}/ + {memberRequestedPrice.pricePerNight}{" "} + {publicRequestedPrice.currency} + + ) : ( + - / - EUR + )} +
    +
    +
    + ) +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css new file mode 100644 index 000000000..7320cf1be --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/PriceList/priceList.module.css @@ -0,0 +1,14 @@ +.priceRow { + display: flex; + justify-content: space-between; + padding: var(--Spacing-x-quarter) 0; +} + +.priceTable { + margin: 0; +} + +.price { + display: flex; + gap: var(--Spacing-x-half); +} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css index cf6c7b165..6d7bc5daf 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/flexibilityOption.module.css @@ -1,15 +1,80 @@ -.card { - font-size: 14px; - border-radius: var(--Corner-radius-Medium); - border: 1px solid var(--Base-Border-Normal); +.card, +.disabledCard { + border-radius: var(--Corner-radius-Large); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + background-color: var(--Base-Surface-Secondary-light-Normal); + position: relative; + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); } -input[type="radio"]:checked + .card { +.disabledCard { + opacity: 0.6; +} + +.disabledCard:hover { + cursor: not-allowed; +} + +.card:hover { + cursor: pointer; background-color: var(--Base-Surface-Primary-light-Hover-alt); } +.checkIcon { + display: none; +} +input[type="radio"]:checked + .card { + border: 1px solid var(--Primary-Dark-On-Surface-Divider); + background-color: var(--Base-Surface-Primary-light-Hover-alt); +} +input[type="radio"]:checked + .card .checkIcon { + display: block; + position: absolute; + top: -10px; + right: -10px; +} .header { display: flex; - justify-content: space-between; + gap: var(--Spacing-x-half); +} + +.header .infoIcon, +.header .infoIcon path { + stroke: var(--UI-Text-Medium-contrast); + fill: transparent; +} + +.button { + background: none; + border: none; + cursor: pointer; + grid-area: chevron; + height: 100%; + justify-self: flex-end; + padding: 0; +} + +.popover { + background-color: var(--Main-Grey-White); + + border-radius: var(--Corner-radius-Medium); + left: 0px; + max-height: 400px; + padding: var(--Spacing-x2); + top: calc(55px + var(--Spacing-x1)); + width: 100%; + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); +} + +.popover section:focus-visible { + outline: none; +} +.popover .popoverText { + margin-bottom: var(--Spacing-x-half); +} +.popover .popoverHeading { + margin-bottom: var(--Spacing-x1); + font-weight: 600; /* TODO: Remove when this is updated in Design system */ } diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index cda09ded4..a523305ae 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -1,9 +1,13 @@ "use client" -import { useIntl } from "react-intl" +import { useState } from "react" +import { Button, DialogTrigger } from "react-aria-components" -import Body from "@/components/TempDesignSystem/Text/Body" +import { CheckCircleIcon, InfoCircleIcon } from "@/components/Icons" import Caption from "@/components/TempDesignSystem/Text/Caption" +import PricePopover from "./Popover" +import PriceTable from "./PriceList" + import styles from "./flexibilityOption.module.css" import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" @@ -12,59 +16,109 @@ export default function FlexibilityOption({ product, name, paymentTerm, + priceInformation, + roomType, + roomTypeCode, + handleSelectRate, }: FlexibilityOptionProps) { - const intl = useIntl() + const [rootDiv, setRootDiv] = useState(undefined) + const [isPopoverOpen, setIsPopoverOpen] = useState(false) - if (!product) { - // TODO: Implement empty state when this rate can't be booked - return
    TBI: Rate not available
    + function setRef(node: Element | null) { + if (node) { + setRootDiv(node) + } } - const { productType } = product - const { public: publicPrice, member: memberPrice } = productType - const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } = - publicPrice - const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } = - memberPrice + if (!product) { + return ( +
    +
    + + {name} + ({paymentTerm}) +
    + +
    + ) + } + + const { public: publicPrice, member: memberPrice } = product.productType + + function onChange() { + const rate = { + roomTypeCode, + roomType, + priceName: name, + public: publicPrice, + member: memberPrice, + } + handleSelectRate(rate) + } return (