From 76a93db67e39f99e3b46f75b5dd1800dddd1a762 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 13 Nov 2024 20:59:16 +0100 Subject: [PATCH 001/101] fix(SW-449): Moved normalizeDate outside middleware --- middlewares/dateFormat.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/middlewares/dateFormat.ts b/middlewares/dateFormat.ts index e2e5c0a9e..b1ee329f1 100644 --- a/middlewares/dateFormat.ts +++ b/middlewares/dateFormat.ts @@ -3,23 +3,24 @@ import { NextMiddleware, NextResponse } from "next/server" import { MiddlewareMatcher } from "@/types/middleware" /* -Middleware function to normalize date formats to support -YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D -in the URL as parameters (toDate and fromDate) + Middleware function to normalize date formats to support + YYYY-MM-D and YYYY-MM-DD since the current web uses YYYY-MM-D + in the URL as parameters (toDate and fromDate) */ + +function normalizeDate(date: string): string { + const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/ + if (datePattern.test(date)) { + const [year, month, day] = date.split("-").map(Number) + return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}` + } + return date +} + export const middleware: NextMiddleware = (request) => { const url = request.nextUrl.clone() const { searchParams } = url - function normalizeDate(date: string): string { - const datePattern = /^\d{4}-\d{1,2}-\d{1,2}$/ - if (datePattern.test(date)) { - const [year, month, day] = date.split("-").map(Number) - return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}` - } - return date - } - if (searchParams.has("fromDate")) { const fromDate = searchParams.get("fromDate")! searchParams.set("fromDate", normalizeDate(fromDate)) From ce453d8b1d38106cfcef867debc00704fdc76ddd Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Tue, 12 Nov 2024 15:30:59 +0100 Subject: [PATCH 002/101] feat(SW-817): hide apple pay if not supported --- .../EnterDetails/Payment/index.tsx | 16 +++++++++++++++- types/window.d.ts | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 9eb7508ec..303fc2d5c 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -81,6 +81,8 @@ export default function Payment({ const { toDate, fromDate, rooms: rooms, hotel } = roomData const [confirmationNumber, setConfirmationNumber] = useState("") + const [availablePaymentOptions, setAvailablePaymentOptions] = + useState(otherPaymentOptions) const methods = useForm({ defaultValues: { @@ -118,6 +120,18 @@ export default function Payment({ retryInterval ) + useEffect(() => { + if (window.ApplePaySession) { + setAvailablePaymentOptions(otherPaymentOptions) + } else { + setAvailablePaymentOptions( + otherPaymentOptions.filter( + (option) => option !== PaymentMethodEnum.applePay + ) + ) + } + }, [otherPaymentOptions, setAvailablePaymentOptions]) + useEffect(() => { if (bookingStatus?.data?.paymentUrl) { router.push(bookingStatus.data.paymentUrl) @@ -260,7 +274,7 @@ export default function Payment({ value={PaymentMethodEnum.card} label={intl.formatMessage({ id: "Credit card" })} /> - {otherPaymentOptions.map((paymentMethod) => ( + {availablePaymentOptions.map((paymentMethod) => ( void) | undefined } From 9c7ac78e1431ccf9c8353feeb3bc0a06868b1df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= Date: Mon, 18 Nov 2024 08:44:49 +0100 Subject: [PATCH 003/101] fix: make parallel routes not render blocking fix: add loading to parallel routes --- .../my-pages/@breadcrumbs/[...path]/loading.tsx | 5 +++++ .../my-pages/@breadcrumbs/[...path]/page.tsx | 9 +-------- .../[contentType]/[uid]/@breadcrumbs/loading.tsx | 5 +++++ .../[contentType]/[uid]/@preview/loading.tsx | 11 +++++++++++ .../(live)/@bookingwidget/[...path]/loading.tsx | 1 + .../[...path]}/page.tsx | 0 .../@bookingwidget/[contentType]/[uid]/page.tsx | 1 - app/[lang]/(live)/@bookingwidget/default.tsx | 15 ++++++++++++++- .../hotelreservation/[...paths]/page.tsx | 1 - .../@bookingwidget/my-pages/[...path]/page.tsx | 1 - app/[lang]/(live)/@footer/[...path]/loading.tsx | 1 + .../[...paths] => @footer/[...path]}/page.tsx | 0 .../(live)/@footer/[contentType]/[uid]/page.tsx | 1 - app/[lang]/(live)/@footer/default.tsx | 1 - .../(live)/@footer/my-pages/[...path]/page.tsx | 1 - app/[lang]/(live)/@header/[...path]/loading.tsx | 1 + .../[...path]}/page.tsx | 0 app/[lang]/(live)/@header/[...paths]/page.tsx | 1 - .../(live)/@header/[contentType]/[uid]/page.tsx | 1 - app/[lang]/(live)/@header/default.tsx | 1 - app/[lang]/(live)/@header/loading.tsx | 5 +++++ .../(live)/@header/my-pages/[...path]/page.tsx | 1 - app/[lang]/(live)/@header/page.tsx | 9 +-------- .../(live)/@sitewidealert/[...path]/loading.tsx | 1 + .../[...path]}/page.tsx | 0 .../(live)/@sitewidealert/[...paths]/page.tsx | 1 - .../@sitewidealert/[contentType]/[uid]/page.tsx | 1 - app/[lang]/(live)/@sitewidealert/default.tsx | 1 - app/[lang]/(live)/@sitewidealert/loading.tsx | 3 +++ .../@sitewidealert/my-pages/[...path]/page.tsx | 1 - app/[lang]/(live)/@sitewidealert/page.tsx | 8 +------- 31 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx create mode 100644 app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx create mode 100644 app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx create mode 100644 app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx rename app/[lang]/(live)/{(public)/[contentType]/[uid]/@breadcrumbs/[...paths] => @bookingwidget/[...path]}/page.tsx (100%) delete mode 100644 app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx delete mode 100644 app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx delete mode 100644 app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx create mode 100644 app/[lang]/(live)/@footer/[...path]/loading.tsx rename app/[lang]/(live)/{@bookingwidget/[...paths] => @footer/[...path]}/page.tsx (100%) delete mode 100644 app/[lang]/(live)/@footer/[contentType]/[uid]/page.tsx delete mode 100644 app/[lang]/(live)/@footer/default.tsx delete mode 100644 app/[lang]/(live)/@footer/my-pages/[...path]/page.tsx create mode 100644 app/[lang]/(live)/@header/[...path]/loading.tsx rename app/[lang]/(live)/{@bookingwidget/hotelreservation => @header/[...path]}/page.tsx (100%) delete mode 100644 app/[lang]/(live)/@header/[...paths]/page.tsx delete mode 100644 app/[lang]/(live)/@header/[contentType]/[uid]/page.tsx delete mode 100644 app/[lang]/(live)/@header/default.tsx create mode 100644 app/[lang]/(live)/@header/loading.tsx delete mode 100644 app/[lang]/(live)/@header/my-pages/[...path]/page.tsx create mode 100644 app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx rename app/[lang]/(live)/{@footer/[...paths] => @sitewidealert/[...path]}/page.tsx (100%) delete mode 100644 app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx delete mode 100644 app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx delete mode 100644 app/[lang]/(live)/@sitewidealert/default.tsx create mode 100644 app/[lang]/(live)/@sitewidealert/loading.tsx delete mode 100644 app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx new file mode 100644 index 000000000..aed94918c --- /dev/null +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx @@ -0,0 +1,5 @@ +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index 6775fd188..a5b818f77 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -1,7 +1,4 @@ -import { Suspense } from "react" - import Breadcrumbs from "@/components/Breadcrumbs" -import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -9,9 +6,5 @@ import { LangParams, PageArgs } from "@/types/params" export default function AllBreadcrumbs({ params }: PageArgs) { setLang(params.lang) - return ( - }> - - - ) + return } diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx new file mode 100644 index 000000000..aed94918c --- /dev/null +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx @@ -0,0 +1,5 @@ +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" + +export default function Loading() { + return +} diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx new file mode 100644 index 000000000..029d8ce71 --- /dev/null +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/loading.tsx @@ -0,0 +1,11 @@ +import { env } from "@/env/server" + +import CurrentLoadingSpinner from "@/components/Current/LoadingSpinner" +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingFooter() { + if (env.HIDE_FOR_NEXT_RELEASE) { + return + } + return +} diff --git a/app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx b/app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx new file mode 100644 index 000000000..1c031d2a1 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx @@ -0,0 +1 @@ +export { default } from "../loading" diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/[...path]/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/[...paths]/page.tsx rename to app/[lang]/(live)/@bookingwidget/[...path]/page.tsx diff --git a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/default.tsx b/app/[lang]/(live)/@bookingwidget/default.tsx index 83ec2818e..6f2a78256 100644 --- a/app/[lang]/(live)/@bookingwidget/default.tsx +++ b/app/[lang]/(live)/@bookingwidget/default.tsx @@ -1 +1,14 @@ -export { default } from "./page" +import { Suspense } from "react" + +import Loading from "./loading" +import Page from "./page" + +import { PageArgs } from "@/types/params" + +export default function Default(props: PageArgs<{}, URLSearchParams>) { + return ( + }> + + + ) +} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@bookingwidget/my-pages/[...path]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@footer/[...path]/loading.tsx b/app/[lang]/(live)/@footer/[...path]/loading.tsx new file mode 100644 index 000000000..1c031d2a1 --- /dev/null +++ b/app/[lang]/(live)/@footer/[...path]/loading.tsx @@ -0,0 +1 @@ +export { default } from "../loading" diff --git a/app/[lang]/(live)/@bookingwidget/[...paths]/page.tsx b/app/[lang]/(live)/@footer/[...path]/page.tsx similarity index 100% rename from app/[lang]/(live)/@bookingwidget/[...paths]/page.tsx rename to app/[lang]/(live)/@footer/[...path]/page.tsx diff --git a/app/[lang]/(live)/@footer/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@footer/[contentType]/[uid]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@footer/[contentType]/[uid]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@footer/default.tsx b/app/[lang]/(live)/@footer/default.tsx deleted file mode 100644 index 83ec2818e..000000000 --- a/app/[lang]/(live)/@footer/default.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./page" diff --git a/app/[lang]/(live)/@footer/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@footer/my-pages/[...path]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@footer/my-pages/[...path]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@header/[...path]/loading.tsx b/app/[lang]/(live)/@header/[...path]/loading.tsx new file mode 100644 index 000000000..1c031d2a1 --- /dev/null +++ b/app/[lang]/(live)/@header/[...path]/loading.tsx @@ -0,0 +1 @@ +export { default } from "../loading" diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@header/[...path]/page.tsx similarity index 100% rename from app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx rename to app/[lang]/(live)/@header/[...path]/page.tsx diff --git a/app/[lang]/(live)/@header/[...paths]/page.tsx b/app/[lang]/(live)/@header/[...paths]/page.tsx deleted file mode 100644 index 03a82e5f5..000000000 --- a/app/[lang]/(live)/@header/[...paths]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../page" diff --git a/app/[lang]/(live)/@header/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@header/[contentType]/[uid]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@header/[contentType]/[uid]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@header/default.tsx b/app/[lang]/(live)/@header/default.tsx deleted file mode 100644 index 83ec2818e..000000000 --- a/app/[lang]/(live)/@header/default.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./page" diff --git a/app/[lang]/(live)/@header/loading.tsx b/app/[lang]/(live)/@header/loading.tsx new file mode 100644 index 000000000..a54009331 --- /dev/null +++ b/app/[lang]/(live)/@header/loading.tsx @@ -0,0 +1,5 @@ +import HeaderFallback from "@/components/Current/Header/HeaderFallback" + +export default function LoadingFooter() { + return +} diff --git a/app/[lang]/(live)/@header/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@header/my-pages/[...path]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@header/my-pages/[...path]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@header/page.tsx b/app/[lang]/(live)/@header/page.tsx index 51e91035d..9d777ffef 100644 --- a/app/[lang]/(live)/@header/page.tsx +++ b/app/[lang]/(live)/@header/page.tsx @@ -1,9 +1,6 @@ -import { Suspense } from "react" - import { env } from "@/env/server" import CurrentHeader from "@/components/Current/Header" -import HeaderFallback from "@/components/Current/Header/HeaderFallback" import Header from "@/components/Header" import { setLang } from "@/i18n/serverContext" @@ -13,11 +10,7 @@ export default function HeaderPage({ params }: PageArgs) { setLang(params.lang) if (env.HIDE_FOR_NEXT_RELEASE) { - return ( - }> - - - ) + return } return
diff --git a/app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx b/app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx new file mode 100644 index 000000000..1c031d2a1 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx @@ -0,0 +1 @@ +export { default } from "../loading" diff --git a/app/[lang]/(live)/@footer/[...paths]/page.tsx b/app/[lang]/(live)/@sitewidealert/[...path]/page.tsx similarity index 100% rename from app/[lang]/(live)/@footer/[...paths]/page.tsx rename to app/[lang]/(live)/@sitewidealert/[...path]/page.tsx diff --git a/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx b/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx deleted file mode 100644 index 03a82e5f5..000000000 --- a/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../page" diff --git a/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@sitewidealert/default.tsx b/app/[lang]/(live)/@sitewidealert/default.tsx deleted file mode 100644 index 83ec2818e..000000000 --- a/app/[lang]/(live)/@sitewidealert/default.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./page" diff --git a/app/[lang]/(live)/@sitewidealert/loading.tsx b/app/[lang]/(live)/@sitewidealert/loading.tsx new file mode 100644 index 000000000..f15322a81 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null +} diff --git a/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx deleted file mode 100644 index 2ebaca014..000000000 --- a/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../../page" diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx index be7ae2256..7c208ea69 100644 --- a/app/[lang]/(live)/@sitewidealert/page.tsx +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -1,5 +1,3 @@ -import { Suspense } from "react" - import { env } from "@/env/server" import SitewideAlert, { preload } from "@/components/SitewideAlert" @@ -15,9 +13,5 @@ export default function SitewideAlertPage({ params }: PageArgs) { setLang(params.lang) preload() - return ( - - - - ) + return } From f0c7aa349c671918b38e0a9d0af759548ae86db8 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Mon, 18 Nov 2024 10:54:35 +0100 Subject: [PATCH 004/101] fix: update booking service schemas --- .../(standard)/[step]/page.tsx | 8 ++--- .../BookingConfirmation/Details/index.tsx | 2 +- .../EnterDetails/Payment/index.tsx | 6 ++++ server/routers/booking/input.ts | 36 ++++++++++++++----- server/routers/booking/output.ts | 24 +++++++++++-- .../hotelReservation/selectRate/section.ts | 2 +- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 70aef0ada..ae04c61a6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -107,10 +107,10 @@ export default async function StepPage({ id: "Select payment method", }) - const roomPrice = - user && roomAvailability.memberRate - ? roomAvailability.memberRate?.localPrice.pricePerStay - : roomAvailability.publicRate!.localPrice.pricePerStay + const roomPrice = { + memberPrice: roomAvailability.memberRate?.localPrice.pricePerStay, + publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay, + } return (
diff --git a/components/HotelReservation/BookingConfirmation/Details/index.tsx b/components/HotelReservation/BookingConfirmation/Details/index.tsx index 956ad8e45..5d23e55a8 100644 --- a/components/HotelReservation/BookingConfirmation/Details/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Details/index.tsx @@ -49,7 +49,7 @@ export default async function Details({
  • {intl.formatMessage({ id: "Cancellation policy" })} - N/A + {booking.rateDefinition.cancellationText}
  • {intl.formatMessage({ id: "Rebooking" })} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 303fc2d5c..84caf0967 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -77,6 +77,9 @@ export default function Payment({ breakfast, bedType, membershipNo, + join, + dateOfBirth, + zipCode, } = userData const { toDate, fromDate, rooms: rooms, hotel } = roomData @@ -181,6 +184,9 @@ export default function Payment({ phoneNumber, countryCode, membershipNumber: membershipNo, + becomeMember: join, + dateOfBirth, + postalCode: zipCode, }, packages: { breakfast: breakfast !== BreakfastPackageEnum.NO_BREAKFAST, diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 4c7d802ef..f838d201f 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -2,6 +2,15 @@ import { z } from "zod" import { ChildBedTypeEnum } from "@/constants/booking" +const signupSchema = z.discriminatedUnion("becomeMember", [ + z.object({ + dateOfBirth: z.string(), + postalCode: z.string(), + becomeMember: z.literal(true), + }), + z.object({ becomeMember: z.literal(false) }), +]) + const roomsSchema = z.array( z.object({ adults: z.number().int().nonnegative(), @@ -15,14 +24,17 @@ const roomsSchema = z.array( .default([]), rateCode: z.string(), roomTypeCode: z.coerce.string(), - guest: z.object({ - firstName: z.string(), - lastName: z.string(), - email: z.string().email(), - phoneNumber: z.string(), - countryCode: z.string(), - membershipNumber: z.string().optional(), - }), + guest: z.intersection( + z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + phoneNumber: z.string(), + countryCode: z.string(), + membershipNumber: z.string().optional(), + }), + signupSchema + ), smsConfirmationRequested: z.boolean(), packages: z.object({ breakfast: z.boolean(), @@ -30,7 +42,13 @@ const roomsSchema = z.array( petFriendly: z.boolean(), accessibility: z.boolean(), }), - roomPrice: z.number().or(z.string().transform((val) => Number(val))), + roomPrice: z.object({ + publicPrice: z.number().or(z.string().transform((val) => Number(val))), + memberPrice: z + .number() + .or(z.string().transform((val) => Number(val))) + .optional(), + }), }) ) diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 5fd34ac00..83a185e1e 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -15,7 +15,18 @@ export const createBookingSchema = z cancellationNumber: z.string().nullable(), reservationStatus: z.string(), paymentUrl: z.string().nullable(), - metadata: z.any(), // TODO: define metadata schema (not sure what it does) + metadata: z + .object({ + errorCode: z.number().optional(), + errorMessage: z.string().optional(), + priceChangedMetadata: z + .object({ + roomPrice: z.number().optional(), + totalPrice: z.number().optional(), + }) + .optional(), + }) + .nullable(), }), type: z.string(), id: z.string(), @@ -77,7 +88,16 @@ export const bookingConfirmationSchema = z guest: guestSchema, hotelId: z.string(), packages: z.array(packageSchema), - rateCode: z.string(), + rateDefinition: z.object({ + rateCode: z.string(), + title: z.string().nullable(), + breakfastIncluded: z.boolean(), + isMemberRate: z.boolean(), + generalTerms: z.array(z.string()).optional(), + cancellationRule: z.string().optional(), + cancellationText: z.string().optional(), + mustBeGuaranteed: z.boolean(), + }), reservationStatus: z.string(), roomPrice: z.number().int(), roomTypeCode: z.string(), diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 578819fb1..05d86ff6c 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -28,7 +28,7 @@ export interface BreakfastSelectionProps extends SectionProps { export interface DetailsProps extends SectionProps {} export interface PaymentProps { - roomPrice: number + roomPrice: { publicPrice: number; memberPrice: number | undefined } otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean From cf0173ef57258e774fd1bc9fc4de163ff0ea9667 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Wed, 13 Nov 2024 16:49:36 +0100 Subject: [PATCH 005/101] feat(SW-880): add wellness and exercise sidepeek --- .../SidePeeks/WellnessAndExercise/index.tsx | 71 +++++++++++++++++++ .../wellnessAndExercise.module.css | 41 +++++++++++ components/ContentType/HotelPage/index.tsx | 10 +-- i18n/dictionaries/da.json | 5 ++ i18n/dictionaries/de.json | 5 ++ i18n/dictionaries/en.json | 5 ++ i18n/dictionaries/fi.json | 5 ++ i18n/dictionaries/no.json | 5 ++ i18n/dictionaries/sv.json | 5 ++ server/routers/hotels/query.ts | 1 + .../hotelPage/sidepeek/wellnessAndExercise.ts | 5 ++ 11 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css create mode 100644 types/components/hotelPage/sidepeek/wellnessAndExercise.ts diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx new file mode 100644 index 000000000..642664cee --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -0,0 +1,71 @@ +import { wellnessAndExercise } from "@/constants/routes/hotelPageParams" + +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./wellnessAndExercise.module.css" + +import { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" + +export default async function WellnessAndExerciseSidePeek({ + healthFacilities, +}: WellnessAndExerciseSidePeekProps) { + const intl = await getIntl() + const lang = getLang() + + return ( + +
    + {healthFacilities.map((facility) => ( +
    + {facility.content.images[0]?.metaData.altText} +
    + + {facility.type} + +
    + + {intl.formatMessage({ id: " Opening Hours" })} + + + {facility.openingDetails.openingHours.ordinary.alwaysOpen + ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Mon-Fri" })} ${facility.openingDetails.openingHours.ordinary.openingTime}-${facility.openingDetails.openingHours.ordinary.closingTime}`} + + + {facility.openingDetails.openingHours.weekends.alwaysOpen + ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} + + Placeholder text +
    +
    +
    + ))} +
    +
    + +
    +
    + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css new file mode 100644 index 000000000..0c33da7cd --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css @@ -0,0 +1,41 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); + margin-bottom: calc( + var(--Spacing-x4) * 2 + 80px + ); /* Creates space between the wrapper and buttonContainer */ +} + +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.image { + width: 100%; + height: 270px; + object-fit: cover; + border-radius: var(--Corner-radius-Medium); +} + +.information { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.body { + margin-top: var(--Spacing-x1); +} + +.buttonContainer { + background-color: var(--Base-Background-Primary-Normal); + border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x4) var(--Spacing-x2); + width: 100%; + position: absolute; + left: 0; + bottom: 0; +} diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 97969867b..eb9d1dd37 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -16,6 +16,7 @@ import MapCard from "./Map/MapCard" import MapWithCardWrapper from "./Map/MapWithCard" import MobileMapToggle from "./Map/MobileMapToggle" import StaticMap from "./Map/StaticMap" +import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise" import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" import IntroSection from "./IntroSection" @@ -52,6 +53,7 @@ export default async function HotelPage() { facilities, faq, alerts, + healthFacilities, } = hotelData const topThreePois = pointsOfInterest.slice(0, 3) @@ -145,13 +147,7 @@ export default async function HotelPage() { {/* TODO */} Restaurant & Bar - - {/* TODO */} - Wellness & Exercise - + Date: Thu, 14 Nov 2024 11:35:02 +0100 Subject: [PATCH 006/101] feat(SW-880): add function for type mapping --- .../HotelPage/SidePeeks/Utils/getType.ts | 17 +++++++++++++++++ .../SidePeeks/WellnessAndExercise/index.tsx | 12 +++++++----- i18n/dictionaries/da.json | 4 ++++ i18n/dictionaries/de.json | 4 ++++ i18n/dictionaries/en.json | 4 ++++ i18n/dictionaries/fi.json | 4 ++++ i18n/dictionaries/no.json | 4 ++++ i18n/dictionaries/sv.json | 4 ++++ 8 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/Utils/getType.ts diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts new file mode 100644 index 000000000..33e39b220 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -0,0 +1,17 @@ +import { getIntl } from "@/i18n" + +export async function getType(type: string) { + const intl = await getIntl() + switch (type) { + case "OutdoorPool": + return intl.formatMessage({ id: "Outdoor pool" }) + case "Sauna": + return intl.formatMessage({ id: "Sauna" }) + case "Relax": + return intl.formatMessage({ id: "Relax" }) + case "Gym": + return intl.formatMessage({ id: "Gym" }) + default: + return type + } +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index 642664cee..ee9f01b85 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -10,6 +10,8 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { getType } from "../Utils/getType" + import styles from "./wellnessAndExercise.module.css" import { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" @@ -36,19 +38,19 @@ export default async function WellnessAndExerciseSidePeek({ width={200} />
    - - {facility.type} + + {getType(facility.type)}
    - + {intl.formatMessage({ id: " Opening Hours" })} - + {facility.openingDetails.openingHours.ordinary.alwaysOpen ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Mon-Fri" })} ${facility.openingDetails.openingHours.ordinary.openingTime}-${facility.openingDetails.openingHours.ordinary.closingTime}`} - + {facility.openingDetails.openingHours.weekends.alwaysOpen ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 44e24d47d..232348983 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -143,6 +143,7 @@ "Guarantee booking with credit card": "Garantere booking med kreditkort", "Guest information": "Gæsteinformation", "Guests & Rooms": "Gæster & værelser", + "Gym": "Fitnesscenter", "Hi": "Hei", "Highest level": "Højeste niveau", "Hospital": "Hospital", @@ -238,6 +239,7 @@ "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", "Opening Hours": "Åbningstider", + "Outdoor pool": "Udendørs pool", "Overview": "Oversigt", "PETR": "Kæledyr", "Parking": "Parkering", @@ -278,6 +280,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Læs mere om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Slap af", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Request bedtype": "Anmod om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -290,6 +293,7 @@ "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sat-Sun": "Lør-Søn", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index a8e785126..d025df242 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -143,6 +143,7 @@ "Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren", "Guest information": "Informationen für Gäste", "Guests & Rooms": "Gäste & Zimmer", + "Gym": "Fitnessstudio", "Hi": "Hallo", "Highest level": "Höchstes Level", "Hospital": "Krankenhaus", @@ -236,6 +237,7 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Opening Hours": "Öffnungszeiten", + "Outdoor pool": "Außenpool", "Overview": "Übersicht", "PETR": "Haustier", "Parking": "Parken", @@ -276,6 +278,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lesen Sie mehr über das Hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Entspannen", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", "Request bedtype": "Bettentyp anfragen", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -289,6 +292,7 @@ "Rooms": "Räume", "Rooms & Guests": "Zimmer & Gäste", "Sat-Sun": "Sa-So", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index ba17eaf15..c7d7336a5 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -155,6 +155,7 @@ "Guest": "Guest", "Guest information": "Guest information", "Guests & Rooms": "Guests & Rooms", + "Gym": "Gym", "Hi": "Hi", "Highest level": "Highest level", "Hospital": "Hospital", @@ -255,6 +256,7 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Opening Hours": "Opening Hours", + "Outdoor pool": "Outdoor pool", "Overview": "Overview", "PETR": "Pet", "Parking": "Parking", @@ -304,6 +306,7 @@ "Read more about wellness & exercise": "Read more about wellness & exercise", "Rebooking": "Rebooking", "Reference #{bookingNr}": "Reference #{bookingNr}", + "Relax": "Relax", "Remove card from member profile": "Remove card from member profile", "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -317,6 +320,7 @@ "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sat-Sun": "Sat-Sun", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Save", "Save card to profile": "Save card to profile", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 2210591e5..add731307 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -143,6 +143,7 @@ "Guarantee booking with credit card": "Varmista varaus luottokortilla", "Guest information": "Vieraan tiedot", "Guests & Rooms": "Vieraat & Huoneet", + "Gym": "Kuntosali", "Hi": "Hi", "Highest level": "Korkein taso", "Hospital": "Sairaala", @@ -238,6 +239,7 @@ "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", "Opening Hours": "Aukioloajat", + "Outdoor pool": "Ulkouima-allas", "Overview": "Yleiskatsaus", "PETR": "Lemmikki", "Parking": "Pysäköinti", @@ -278,6 +280,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lue lisää hotellista", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Rentoutua", "Remove card from member profile": "Poista kortti jäsenprofiilista", "Request bedtype": "Pyydä sänkytyyppiä", "Restaurant": "{count, plural, one {#Ravintola} other {#Restaurants}}", @@ -291,6 +294,7 @@ "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", "Sat-Sun": "La-Su", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5482b868a..4c6415e42 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -142,6 +142,7 @@ "Guarantee booking with credit card": "Garantere booking med kredittkort", "Guest information": "Informasjon til gjester", "Guests & Rooms": "Gjester & rom", + "Gym": "Treningsstudio", "Hi": "Hei", "Highest level": "Høyeste nivå", "Hospital": "Sykehus", @@ -236,6 +237,7 @@ "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", "Opening Hours": "Åpningstider", + "Outdoor pool": "Utendørs basseng", "Overview": "Oversikt", "PETR": "Kjæledyr", "Parking": "Parkering", @@ -276,6 +278,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Les mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Slappe av", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Request bedtype": "Be om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -288,6 +291,7 @@ "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sat-Sun": "Lør-Søn", + "Sauna": "Badstue", "Sauna and gym": "Sauna and gym", "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 612e84812..47c8096ca 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -142,6 +142,7 @@ "Guarantee booking with credit card": "Garantera bokning med kreditkort", "Guest information": "Information till gästerna", "Guests & Rooms": "Gäster & rum", + "Gym": "Gym", "Hi": "Hej", "Highest level": "Högsta nivå", "Hospital": "Sjukhus", @@ -236,6 +237,7 @@ "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", "Opening Hours": "Öppettider", + "Outdoor pool": "Utomhuspool", "Overview": "Översikt", "PETR": "Husdjur", "Parking": "Parkering", @@ -276,6 +278,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Läs mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Koppla av", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurang} other {#Restauranger}}", @@ -288,6 +291,7 @@ "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sat-Sun": "Lör-Sön", + "Sauna": "Bastu", "Sauna and gym": "Sauna and gym", "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", From c7327e07bf045c27d5700eb1f4912a7f8929fb62 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 14 Nov 2024 14:10:51 +0100 Subject: [PATCH 007/101] feat(SW-880): refactor getType --- .../HotelPage/SidePeeks/Utils/getType.ts | 15 +++++++++++---- .../SidePeeks/WellnessAndExercise/index.tsx | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts index 33e39b220..47cbf00da 100644 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -2,15 +2,22 @@ import { getIntl } from "@/i18n" export async function getType(type: string) { const intl = await getIntl() + + /* TODO: Get full list of types */ + const outdoorPool = intl.formatMessage({ id: "Outdoor pool" }) + const sauna = intl.formatMessage({ id: "Sauna" }) + const relax = intl.formatMessage({ id: "Relax" }) + const gym = intl.formatMessage({ id: "Gym" }) + switch (type) { case "OutdoorPool": - return intl.formatMessage({ id: "Outdoor pool" }) + return outdoorPool case "Sauna": - return intl.formatMessage({ id: "Sauna" }) + return sauna case "Relax": - return intl.formatMessage({ id: "Relax" }) + return relax case "Gym": - return intl.formatMessage({ id: "Gym" }) + return gym default: return type } diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index ee9f01b85..6bd73d703 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -55,7 +55,9 @@ export default async function WellnessAndExerciseSidePeek({ ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} - Placeholder text + + {/* TODO: Determine what details should be displayed about the facility type */} +
    From 7a8ce0d8f6a4ae7c35434821d11b697a6b29ce69 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 14 Nov 2024 14:21:01 +0100 Subject: [PATCH 008/101] feat(SW-880): add import type --- components/ContentType/HotelPage/SidePeeks/Utils/getType.ts | 2 +- .../HotelPage/SidePeeks/WellnessAndExercise/index.tsx | 6 +++--- types/components/hotelPage/sidepeek/wellnessAndExercise.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts index 47cbf00da..9cab458c7 100644 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -1,6 +1,6 @@ import { getIntl } from "@/i18n" -export async function getType(type: string) { +export async function getFacilityType(type: string) { const intl = await getIntl() /* TODO: Get full list of types */ diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index 6bd73d703..dee9d73e1 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -10,11 +10,11 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { getType } from "../Utils/getType" +import { getFacilityType } from "../Utils/getType" import styles from "./wellnessAndExercise.module.css" -import { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" +import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" export default async function WellnessAndExerciseSidePeek({ healthFacilities, @@ -39,7 +39,7 @@ export default async function WellnessAndExerciseSidePeek({ />
    - {getType(facility.type)} + {getFacilityType(facility.type)}
    diff --git a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts index f64ccd003..a75499f2d 100644 --- a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts +++ b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts @@ -1,4 +1,4 @@ -import { Hotel } from "@/types/hotel" +import type { Hotel } from "@/types/hotel" export type WellnessAndExerciseSidePeekProps = { healthFacilities: Hotel["healthFacilities"] From 5377a43f5677f7dc10f66851f57c5a11712baf5b Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 14 Nov 2024 22:38:53 +0100 Subject: [PATCH 009/101] feat(SW-880): add translations --- .../ContentType/HotelPage/SidePeeks/Utils/getType.ts | 7 ++++++- i18n/dictionaries/da.json | 2 ++ i18n/dictionaries/de.json | 2 ++ i18n/dictionaries/en.json | 2 ++ i18n/dictionaries/fi.json | 2 ++ i18n/dictionaries/no.json | 2 ++ i18n/dictionaries/sv.json | 2 ++ 7 files changed, 18 insertions(+), 1 deletion(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts index 9cab458c7..1a6f87a63 100644 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -3,21 +3,26 @@ import { getIntl } from "@/i18n" export async function getFacilityType(type: string) { const intl = await getIntl() - /* TODO: Get full list of types */ const outdoorPool = intl.formatMessage({ id: "Outdoor pool" }) + const indoorPool = intl.formatMessage({ id: "Indoor pool" }) const sauna = intl.formatMessage({ id: "Sauna" }) const relax = intl.formatMessage({ id: "Relax" }) const gym = intl.formatMessage({ id: "Gym" }) + const jacuzzi = intl.formatMessage({ id: "Jacuzzi" }) switch (type) { case "OutdoorPool": return outdoorPool + case "IndoorPool": + return indoorPool case "Sauna": return sauna case "Relax": return relax case "Gym": return gym + case "Jacuzzi": + return jacuzzi default: return type } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 232348983..14c46b7be 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -160,7 +160,9 @@ "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", "Included": "Inkluderet", + "Indoor pool": "Indendørs pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", + "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", "Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index d025df242..403f0db22 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -160,7 +160,9 @@ "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", "Included": "Iinklusive", + "Indoor pool": "Innenpool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", + "Jacuzzi": "Whirlpool", "Join Scandic Friends": "Treten Sie Scandic Friends bei", "Join at no cost": "Kostenlos beitreten", "Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index c7d7336a5..2bb99e5f2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -172,7 +172,9 @@ "In crib": "In crib", "In extra bed": "In extra bed", "Included": "Included", + "Indoor pool": "Indoor pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", + "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", "Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index add731307..bfaa69ec6 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -160,7 +160,9 @@ "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", "Included": "Sisälly hintaan", + "Indoor pool": "Sisäuima-allas", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", + "Jacuzzi": "Poreallas", "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", "Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 4c6415e42..b593f1c83 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -158,7 +158,9 @@ "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", "Included": "Inkludert", + "Indoor pool": "Innendørs basseng", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", + "Jacuzzi": "Boblebad", "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", "Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 47c8096ca..05c775020 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -158,7 +158,9 @@ "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", "Included": "Inkluderad", + "Indoor pool": "Inomhuspool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", + "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Gå med i Scandic Friends", "Join at no cost": "Gå med utan kostnad", "Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.", From 4002d63c5976b10ad65b4bdcf78bc302a885c7ea Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 15 Nov 2024 14:21:52 +0100 Subject: [PATCH 010/101] feat(SW-880): update facility type rendering --- .../HotelPage/SidePeeks/Utils/getType.ts | 29 ------------- .../SidePeeks/WellnessAndExercise/index.tsx | 42 ++++++++++--------- i18n/dictionaries/da.json | 4 +- i18n/dictionaries/de.json | 4 +- i18n/dictionaries/en.json | 4 +- i18n/dictionaries/fi.json | 4 +- i18n/dictionaries/no.json | 4 +- i18n/dictionaries/sv.json | 4 +- .../hotelPage/sidepeek/wellnessAndExercise.ts | 1 + 9 files changed, 35 insertions(+), 61 deletions(-) delete mode 100644 components/ContentType/HotelPage/SidePeeks/Utils/getType.ts diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts deleted file mode 100644 index 1a6f87a63..000000000 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getIntl } from "@/i18n" - -export async function getFacilityType(type: string) { - const intl = await getIntl() - - const outdoorPool = intl.formatMessage({ id: "Outdoor pool" }) - const indoorPool = intl.formatMessage({ id: "Indoor pool" }) - const sauna = intl.formatMessage({ id: "Sauna" }) - const relax = intl.formatMessage({ id: "Relax" }) - const gym = intl.formatMessage({ id: "Gym" }) - const jacuzzi = intl.formatMessage({ id: "Jacuzzi" }) - - switch (type) { - case "OutdoorPool": - return outdoorPool - case "IndoorPool": - return indoorPool - case "Sauna": - return sauna - case "Relax": - return relax - case "Gym": - return gym - case "Jacuzzi": - return jacuzzi - default: - return type - } -} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index dee9d73e1..5314e3f18 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -10,14 +10,13 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { getFacilityType } from "../Utils/getType" - import styles from "./wellnessAndExercise.module.css" import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" export default async function WellnessAndExerciseSidePeek({ healthFacilities, + buttonUrl, }: WellnessAndExerciseSidePeekProps) { const intl = await getIntl() const lang = getLang() @@ -30,16 +29,20 @@ export default async function WellnessAndExerciseSidePeek({
    {healthFacilities.map((facility) => (
    - {facility.content.images[0]?.metaData.altText} + {facility.content.images[0]?.imageSizes.medium && ( + {facility.content.images[0].metaData.altText + )}
    - {getFacilityType(facility.type)} + + {intl.formatMessage({ id: `${facility.type}` })} +
    @@ -55,21 +58,20 @@ export default async function WellnessAndExerciseSidePeek({ ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} - - {/* TODO: Determine what details should be displayed about the facility type */} -
    ))}
    -
    - -
    + {buttonUrl && ( +
    + +
    + )} ) } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 14c46b7be..c453589ea 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -160,7 +160,7 @@ "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", "Included": "Inkluderet", - "Indoor pool": "Indendørs pool", + "IndoorPool": "Indendørs pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Tilmeld dig Scandic Friends", @@ -241,7 +241,7 @@ "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", "Opening Hours": "Åbningstider", - "Outdoor pool": "Udendørs pool", + "OutdoorPool": "Udendørs pool", "Overview": "Oversigt", "PETR": "Kæledyr", "Parking": "Parkering", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 403f0db22..04d750494 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -160,7 +160,7 @@ "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", "Included": "Iinklusive", - "Indoor pool": "Innenpool", + "IndoorPool": "Innenpool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "Jacuzzi": "Whirlpool", "Join Scandic Friends": "Treten Sie Scandic Friends bei", @@ -239,7 +239,7 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Opening Hours": "Öffnungszeiten", - "Outdoor pool": "Außenpool", + "OutdoorPool": "Außenpool", "Overview": "Übersicht", "PETR": "Haustier", "Parking": "Parken", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2bb99e5f2..09cf02ec9 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -172,7 +172,7 @@ "In crib": "In crib", "In extra bed": "In extra bed", "Included": "Included", - "Indoor pool": "Indoor pool", + "IndoorPool": "Indoor pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Join Scandic Friends", @@ -258,7 +258,7 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Opening Hours": "Opening Hours", - "Outdoor pool": "Outdoor pool", + "OutdoorPool": "Outdoor pool", "Overview": "Overview", "PETR": "Pet", "Parking": "Parking", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index bfaa69ec6..64903ea74 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -160,7 +160,7 @@ "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", "Included": "Sisälly hintaan", - "Indoor pool": "Sisäuima-allas", + "IndoorPool": "Sisäuima-allas", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", "Jacuzzi": "Poreallas", "Join Scandic Friends": "Liity jäseneksi", @@ -241,7 +241,7 @@ "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", "Opening Hours": "Aukioloajat", - "Outdoor pool": "Ulkouima-allas", + "OutdoorPool": "Ulkouima-allas", "Overview": "Yleiskatsaus", "PETR": "Lemmikki", "Parking": "Pysäköinti", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index b593f1c83..ef15b0823 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -158,7 +158,7 @@ "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", "Included": "Inkludert", - "Indoor pool": "Innendørs basseng", + "IndoorPool": "Innendørs basseng", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", "Jacuzzi": "Boblebad", "Join Scandic Friends": "Bli med i Scandic Friends", @@ -239,7 +239,7 @@ "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", "Opening Hours": "Åpningstider", - "Outdoor pool": "Utendørs basseng", + "OutdoorPool": "Utendørs basseng", "Overview": "Oversikt", "PETR": "Kjæledyr", "Parking": "Parkering", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 05c775020..8be596fc1 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -158,7 +158,7 @@ "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", "Included": "Inkluderad", - "Indoor pool": "Inomhuspool", + "IndoorPool": "Inomhuspool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Gå med i Scandic Friends", @@ -239,7 +239,7 @@ "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", "Opening Hours": "Öppettider", - "Outdoor pool": "Utomhuspool", + "OutdoorPool": "Utomhuspool", "Overview": "Översikt", "PETR": "Husdjur", "Parking": "Parkering", diff --git a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts index a75499f2d..828f3ee8b 100644 --- a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts +++ b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts @@ -2,4 +2,5 @@ import type { Hotel } from "@/types/hotel" export type WellnessAndExerciseSidePeekProps = { healthFacilities: Hotel["healthFacilities"] + buttonUrl?: string } From b5704dee216712ab10be315d0eb6ebcb2ec2f1e1 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 18 Nov 2024 10:24:45 +0100 Subject: [PATCH 011/101] feat(SW-880): create facility card component --- .../FacilityCard/facilityCard.module.css | 22 ++++++++ .../FacilityCard/index.tsx | 54 +++++++++++++++++++ .../SidePeeks/WellnessAndExercise/index.tsx | 47 ++++------------ .../wellnessAndExercise.module.css | 23 -------- components/ContentType/HotelPage/index.tsx | 5 +- .../hotelPage/sidepeek/facilityCard.ts | 19 +++++++ 6 files changed, 109 insertions(+), 61 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx create mode 100644 types/components/hotelPage/sidepeek/facilityCard.ts diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css new file mode 100644 index 000000000..5abac9f32 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css @@ -0,0 +1,22 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.image { + width: 100%; + height: 270px; + object-fit: cover; + border-radius: var(--Corner-radius-Medium); +} + +.information { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.body { + margin-top: var(--Spacing-x1); +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx new file mode 100644 index 000000000..ba3925baf --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx @@ -0,0 +1,54 @@ +import Image from "@/components/Image" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./facilityCard.module.css" + +import type { FacilityCardProps } from "@/types/components/hotelPage/sidepeek/facilityCard" + +export default async function FacilityCard({ + imgUrl, + imgAltText, + facilityType, + ordinaryOpeningTimes, + weekendOpeningTimes, +}: FacilityCardProps) { + const intl = await getIntl() + return ( +
    + {imgUrl && ( + {imgAltText + )} +
    + + + {intl.formatMessage({ id: `${facilityType}` })} + + +
    + + {intl.formatMessage({ id: " Opening Hours" })} + + + {ordinaryOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`} + + + {weekendOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`} + +
    +
    +
    + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index 5314e3f18..d7f381d9b 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -1,15 +1,13 @@ import { wellnessAndExercise } from "@/constants/routes/hotelPageParams" -import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import SidePeek from "@/components/TempDesignSystem/SidePeek" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import FacilityCard from "./FacilityCard" + import styles from "./wellnessAndExercise.module.css" import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" @@ -28,39 +26,14 @@ export default async function WellnessAndExerciseSidePeek({ >
    {healthFacilities.map((facility) => ( -
    - {facility.content.images[0]?.imageSizes.medium && ( - {facility.content.images[0].metaData.altText - )} -
    - - - {intl.formatMessage({ id: `${facility.type}` })} - - -
    - - {intl.formatMessage({ id: " Opening Hours" })} - - - {facility.openingDetails.openingHours.ordinary.alwaysOpen - ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Mon-Fri" })} ${facility.openingDetails.openingHours.ordinary.openingTime}-${facility.openingDetails.openingHours.ordinary.closingTime}`} - - - {facility.openingDetails.openingHours.weekends.alwaysOpen - ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} - -
    -
    -
    + ))}
    {buttonUrl && ( diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css index 0c33da7cd..11a410f13 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css @@ -7,29 +7,6 @@ ); /* Creates space between the wrapper and buttonContainer */ } -.content { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.image { - width: 100%; - height: 270px; - object-fit: cover; - border-radius: var(--Corner-radius-Medium); -} - -.information { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); -} - -.body { - margin-top: var(--Spacing-x1); -} - .buttonContainer { background-color: var(--Base-Background-Primary-Normal); border-top: 1px solid var(--Base-Border-Subtle); diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index eb9d1dd37..a565ea117 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -147,7 +147,10 @@ export default async function HotelPage() { {/* TODO */} Restaurant & Bar - + Date: Mon, 18 Nov 2024 11:23:34 +0100 Subject: [PATCH 012/101] feat(SW-880): refactor facility component --- .../facility.module.css} | 2 +- .../WellnessAndExercise/Facility/index.tsx | 64 +++++++++++++++++++ .../FacilityCard/index.tsx | 54 ---------------- .../SidePeeks/WellnessAndExercise/index.tsx | 11 +--- .../components/hotelPage/sidepeek/facility.ts | 5 ++ .../hotelPage/sidepeek/facilityCard.ts | 19 ------ 6 files changed, 72 insertions(+), 83 deletions(-) rename components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/{FacilityCard/facilityCard.module.css => Facility/facility.module.css} (95%) create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx delete mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx create mode 100644 types/components/hotelPage/sidepeek/facility.ts delete mode 100644 types/components/hotelPage/sidepeek/facilityCard.ts diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/facility.module.css similarity index 95% rename from components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css rename to components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/facility.module.css index 5abac9f32..22aea5bce 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/facility.module.css @@ -17,6 +17,6 @@ gap: var(--Spacing-x-one-and-half); } -.body { +.openingHours { margin-top: var(--Spacing-x1); } diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx new file mode 100644 index 000000000..474ccb901 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx @@ -0,0 +1,64 @@ +import Image from "@/components/Image" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./facility.module.css" + +import { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility" + +export default async function Facility({ data }: FacilityProps) { + const intl = await getIntl() + const imgUrl = data.content.images[0]?.imageSizes.medium + const imgAltText = data.content.images[0]?.metaData.altText + const facilityType = data.type + const ordinaryOpeningTimes = { + alwaysOpen: data.openingDetails.openingHours.ordinary.alwaysOpen, + openingTime: data.openingDetails.openingHours.ordinary.openingTime, + closingTime: data.openingDetails.openingHours.ordinary.closingTime, + } + const weekendOpeningTimes = { + alwaysOpen: data.openingDetails.openingHours.weekends.alwaysOpen, + openingTime: data.openingDetails.openingHours.weekends.openingTime, + closingTime: data.openingDetails.openingHours.weekends.closingTime, + } + + return ( +
    + {imgUrl && ( + {imgAltText + )} +
    + + + {intl.formatMessage({ id: `${facilityType}` })} + + +
    + + {intl.formatMessage({ id: " Opening Hours" })} + +
    + + {ordinaryOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`} + + + {weekendOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`} + +
    +
    +
    +
    + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx deleted file mode 100644 index ba3925baf..000000000 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Image from "@/components/Image" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" - -import styles from "./facilityCard.module.css" - -import type { FacilityCardProps } from "@/types/components/hotelPage/sidepeek/facilityCard" - -export default async function FacilityCard({ - imgUrl, - imgAltText, - facilityType, - ordinaryOpeningTimes, - weekendOpeningTimes, -}: FacilityCardProps) { - const intl = await getIntl() - return ( -
    - {imgUrl && ( - {imgAltText - )} -
    - - - {intl.formatMessage({ id: `${facilityType}` })} - - -
    - - {intl.formatMessage({ id: " Opening Hours" })} - - - {ordinaryOpeningTimes.alwaysOpen - ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`} - - - {weekendOpeningTimes.alwaysOpen - ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`} - -
    -
    -
    - ) -} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index d7f381d9b..97ac09a91 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -6,7 +6,7 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import FacilityCard from "./FacilityCard" +import Facility from "./Facility" import styles from "./wellnessAndExercise.module.css" @@ -26,14 +26,7 @@ export default async function WellnessAndExerciseSidePeek({ >
    {healthFacilities.map((facility) => ( - + ))}
    {buttonUrl && ( diff --git a/types/components/hotelPage/sidepeek/facility.ts b/types/components/hotelPage/sidepeek/facility.ts new file mode 100644 index 000000000..6cb6f0796 --- /dev/null +++ b/types/components/hotelPage/sidepeek/facility.ts @@ -0,0 +1,5 @@ +import type { Hotel } from "@/types/hotel" + +export type FacilityProps = { + data: Hotel["healthFacilities"][number] +} diff --git a/types/components/hotelPage/sidepeek/facilityCard.ts b/types/components/hotelPage/sidepeek/facilityCard.ts deleted file mode 100644 index fbf6e9942..000000000 --- a/types/components/hotelPage/sidepeek/facilityCard.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type FacilityCardProps = { - imgUrl: string - imgAltText: string - facilityType: string - ordinaryOpeningTimes: { - alwaysOpen: boolean - isClosed: boolean - openingTime?: string - closingTime?: string - sortOrder?: number - } - weekendOpeningTimes: { - alwaysOpen: boolean - isClosed: boolean - openingTime?: string - closingTime?: string - sortOrder?: number - } -} From dc5746a902e0711dce94068fd7757f363bb06bec Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 18 Nov 2024 12:13:23 +0100 Subject: [PATCH 013/101] feat(SW-880): refactor variables --- .../WellnessAndExercise/Facility/index.tsx | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx index 474ccb901..e00ed5964 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx @@ -10,26 +10,16 @@ import { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility" export default async function Facility({ data }: FacilityProps) { const intl = await getIntl() - const imgUrl = data.content.images[0]?.imageSizes.medium - const imgAltText = data.content.images[0]?.metaData.altText - const facilityType = data.type - const ordinaryOpeningTimes = { - alwaysOpen: data.openingDetails.openingHours.ordinary.alwaysOpen, - openingTime: data.openingDetails.openingHours.ordinary.openingTime, - closingTime: data.openingDetails.openingHours.ordinary.closingTime, - } - const weekendOpeningTimes = { - alwaysOpen: data.openingDetails.openingHours.weekends.alwaysOpen, - openingTime: data.openingDetails.openingHours.weekends.openingTime, - closingTime: data.openingDetails.openingHours.weekends.closingTime, - } + const image = data.content.images[0] + const ordinaryOpeningTimes = data.openingDetails.openingHours.ordinary + const weekendOpeningTimes = data.openingDetails.openingHours.weekends return (
    - {imgUrl && ( + {image.imageSizes.medium && ( {imgAltText - - {intl.formatMessage({ id: `${facilityType}` })} - + {intl.formatMessage({ id: `${data.type}` })}
    From 24124209d9a25fcc60cef849a2656257f415a926 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 18 Nov 2024 14:05:07 +0100 Subject: [PATCH 014/101] feat(SW-705): Updated URLs for hotelreservation --- .../HotelReservation/HotelCard/index.tsx | 6 +++--- constants/routes/hotelReservation.js | 13 +++++------- next.config.js | 21 ------------------- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 4c478d275..f996578c7 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -3,7 +3,7 @@ import { useParams } from "next/dist/client/components/navigation" import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" -import { selectHotelMap } from "@/constants/routes/hotelReservation" +import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" @@ -150,9 +150,9 @@ export default function HotelCard({ size="small" className={styles.button} > - {/* TODO: Localize link and also use correct search params */} + {/* TODO: use correct search params */} diff --git a/constants/routes/hotelReservation.js b/constants/routes/hotelReservation.js index 2224b17b6..94a9cef18 100644 --- a/constants/routes/hotelReservation.js +++ b/constants/routes/hotelReservation.js @@ -1,14 +1,13 @@ /** @type {import('@/types/routes').LangRoute} */ export const hotelReservation = { en: "/en/hotelreservation", - sv: "/sv/hotellbokning", - no: "/no/hotell-reservasjon", - fi: "/fi/hotellivaraus", - da: "/da/hotel-reservation", - de: "/de/hotelreservierung", + sv: "/sv/hotelreservation", + no: "/no/hotelreservation", + fi: "/fi/hotelreservation", + da: "/da/hotelreservation", + de: "/de/hotelreservation", } -// TODO: Translate paths export const selectHotel = { en: `${hotelReservation.en}/select-hotel`, sv: `${hotelReservation.sv}/select-hotel`, @@ -18,7 +17,6 @@ export const selectHotel = { de: `${hotelReservation.de}/select-hotel`, } -// TODO: Translate paths export const selectRate = { en: `${hotelReservation.en}/select-rate`, sv: `${hotelReservation.sv}/select-rate`, @@ -68,7 +66,6 @@ export const payment = { de: `${hotelReservation.de}/payment`, } -// TODO: Translate paths export const selectHotelMap = { en: `${selectHotel.en}/map`, sv: `${selectHotel.sv}/map`, diff --git a/next.config.js b/next.config.js index c65772ab7..29012f230 100644 --- a/next.config.js +++ b/next.config.js @@ -2,7 +2,6 @@ import createJiti from "jiti" import { fileURLToPath } from "url" import { login, logout } from "./constants/routes/handleAuth.js" -import { hotelReservation } from "./constants/routes/hotelReservation.js" import { myPages } from "./constants/routes/myPages.js" const path = fileURLToPath(new URL(import.meta.url)) @@ -278,26 +277,6 @@ const nextConfig = { source: `${myPages.sv}/:path*`, destination: `/sv/my-pages/:path*`, }, - { - source: `${hotelReservation.da}/:path*`, - destination: "/da/hotelreservation/:path*", - }, - { - source: `${hotelReservation.de}/:path*`, - destination: "/de/hotelreservation/:path*", - }, - { - source: `${hotelReservation.fi}/:path*`, - destination: "/fi/hotelreservation/:path*", - }, - { - source: `${hotelReservation.no}/:path*`, - destination: "/no/hotelreservation/:path*", - }, - { - source: `${hotelReservation.sv}/:path*`, - destination: "/sv/hotelreservation/:path*", - }, ], } }, From d18bc45b19ef1b3595f2c104184bc75513bbb6a9 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Mon, 18 Nov 2024 14:02:32 +0000 Subject: [PATCH 015/101] Merged in feat/SW-342-filtering-and-sorting-mobile (pull request #919) Feat/SW-342 filtering and sorting mobile * feat(SW-342): add sort and filter on mobile * Use zustand for state management * Add count and translations * Clear filters * Small fixes * Fixes Approved-by: Pontus Dreij --- .../(standard)/select-hotel/page.module.css | 22 +++- .../(standard)/select-hotel/page.tsx | 8 +- .../HotelCardListing/index.tsx | 19 ++- .../filterAndSortModal.module.css | 99 ++++++++++++++++ .../SelectHotel/FilterAndSortModal/index.tsx | 87 ++++++++++++++ .../FilterCheckbox/filterCheckbox.module.css | 29 +++++ .../HotelFilter/FilterCheckbox/index.tsx | 35 ++++++ .../HotelFilter/hotelFilter.module.css | 7 -- .../SelectHotel/HotelFilter/index.tsx | 111 ++++++++++-------- .../HotelSorter/hotelSorter.module.css | 9 -- .../SelectHotel/HotelSorter/index.tsx | 25 ++-- .../MobileMapButtonContainer/index.tsx | 31 ++--- .../mobileMapButtonContainer.module.css | 4 +- i18n/dictionaries/da.json | 3 + i18n/dictionaries/de.json | 3 + i18n/dictionaries/en.json | 3 + i18n/dictionaries/fi.json | 3 + i18n/dictionaries/no.json | 3 + i18n/dictionaries/sv.json | 3 + stores/hotel-filters.ts | 27 +++++ .../selectHotel/filterAndSortModal.ts | 5 + .../selectHotel/filterCheckbox.ts | 6 + .../selectHotel/hotelFilters.ts | 1 + .../selectHotel/hotelSorter.ts | 4 + 24 files changed, 434 insertions(+), 113 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css create mode 100644 components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx create mode 100644 components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css create mode 100644 components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx delete mode 100644 components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css create mode 100644 stores/hotel-filters.ts create mode 100644 types/components/hotelReservation/selectHotel/filterAndSortModal.ts create mode 100644 types/components/hotelReservation/selectHotel/filterCheckbox.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index 8bf36ee38..e42544196 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -20,10 +20,13 @@ gap: var(--Spacing-x1); } +.sorter { + display: none; +} + .sideBar { display: flex; flex-direction: column; - max-width: 340px; } .link { @@ -47,6 +50,10 @@ gap: var(--Spacing-x3); } +.filter { + display: none; +} + @media (min-width: 768px) { .main { padding: var(--Spacing-x5); @@ -58,6 +65,11 @@ var(--Spacing-x5); } + .sorter { + display: block; + width: 339px; + } + .title { margin: 0 auto; display: flex; @@ -65,6 +77,14 @@ align-items: center; justify-content: space-between; } + + .sideBar { + max-width: 340px; + } + .filter { + display: block; + } + .link { display: flex; padding-bottom: var(--Spacing-x6); diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 0493c9d70..791773f94 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -74,9 +74,11 @@ export default async function SelectHotelPage({ {city.name} {hotels.length} hotels
    - +
    + +
    - +
  • @@ -118,7 +120,7 @@ export default async function SelectHotelPage({ />
    )} - +
    {!hotels.length && ( diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 4ba65ed9c..c4a5a1eee 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -2,7 +2,7 @@ import { useSearchParams } from "next/navigation" import { useMemo } from "react" -import Title from "@/components/TempDesignSystem/Text/Title" +import { useHotelFilterStore } from "@/stores/hotel-filters" import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" @@ -22,6 +22,8 @@ export default function HotelCardListing({ onHotelCardHover, }: HotelCardListingProps) { const searchParams = useSearchParams() + const activeFilters = useHotelFilterStore((state) => state.activeFilters) + const setResultCount = useHotelFilterStore((state) => state.setResultCount) const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, @@ -57,17 +59,22 @@ export default function HotelCardListing({ }, [hotelData, sortBy]) const hotels = useMemo(() => { - const appliedFilters = searchParams.get("filters")?.split(",") - if (!appliedFilters || appliedFilters.length === 0) return sortedHotels + if (activeFilters.length === 0) { + setResultCount(sortedHotels.length) + return sortedHotels + } - return sortedHotels.filter((hotel) => - appliedFilters.every((appliedFilterId) => + const filteredHotels = sortedHotels.filter((hotel) => + activeFilters.every((appliedFilterId) => hotel.hotelData.detailedFacilities.some( (facility) => facility.id.toString() === appliedFilterId ) ) ) - }, [searchParams, sortedHotels]) + + setResultCount(filteredHotels.length) + return filteredHotels + }, [activeFilters, sortedHotels, setResultCount]) return (
    diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css new file mode 100644 index 000000000..6768d2c56 --- /dev/null +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css @@ -0,0 +1,99 @@ +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} +@keyframes modal-slide-up { + from { + bottom: -100%; + } + + to { + bottom: 0; + } +} + +.overlay { + align-items: center; + background: rgba(0, 0, 0, 0.5); + display: flex; + height: var(--visual-viewport-height); + justify-content: center; + left: 0; + position: fixed; + top: 0; + width: 100vw; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.modal { + position: absolute; + left: 0; + bottom: 0; + height: calc(100dvh - 20px); + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); + width: 100%; + + &[data-entering] { + animation: modal-slide-up 200ms; + } + + &[data-existing] { + animation: modal-slide-up 200ms reverse; + } +} + +.content { + flex-direction: column; + gap: var(--Spacing-x3); + display: flex; + height: 100%; +} + +.sorter { + padding: var(--Spacing-x2); + flex: 0 0 auto; +} + +.filters { + padding: var(--Spacing-x2); + flex: 1 1 auto; + overflow-y: auto; +} + +.header { + text-align: right; + padding: var(--Spacing-x-one-and-half); + flex: 0 0 auto; +} + +.close { + background: none; + border: none; + cursor: pointer; + justify-self: flex-end; + padding: 0; +} + +.footer { + display: flex; + flex-direction: column-reverse; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); + flex: 0 0 auto; + border-top: 1px solid var(--Base-Border-Subtle); +} diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx new file mode 100644 index 000000000..be1a1bc9c --- /dev/null +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -0,0 +1,87 @@ +"use client" + +import { + Dialog as AriaDialog, + DialogTrigger, + Modal, + ModalOverlay, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { useHotelFilterStore } from "@/stores/hotel-filters" + +import { CloseLargeIcon, FilterIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import HotelFilter from "../HotelFilter" +import HotelSorter from "../HotelSorter" + +import styles from "./filterAndSortModal.module.css" + +import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal" + +export default function FilterAndSortModal({ + filters, +}: FilterAndSortModalProps) { + const intl = useIntl() + const resultCount = useHotelFilterStore((state) => state.resultCount) + const setFilters = useHotelFilterStore((state) => state.setFilters) + + return ( + <> + + + + + + {({ close }) => ( + <> +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    + + )} +
    +
    +
    +
    + + ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css new file mode 100644 index 000000000..4b3b94787 --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css @@ -0,0 +1,29 @@ +.container { + display: flex; + flex-direction: column; + color: var(--text-color); +} + +.container[data-selected] .checkbox { + border: none; + background: var(--UI-Input-Controls-Fill-Selected); +} + +.checkboxContainer { + display: flex; + align-items: center; + gap: var(--Spacing-x-one-and-half); +} + +.checkbox { + width: 24px; + height: 24px; + min-width: 24px; + border: 1px solid var(--UI-Input-Controls-Border-Normal); + border-radius: 4px; + transition: all 200ms; + display: flex; + align-items: center; + justify-content: center; + forced-color-adjust: none; +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx new file mode 100644 index 000000000..6767f666b --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx @@ -0,0 +1,35 @@ +"use client" + +import { Checkbox as AriaCheckbox } from "react-aria-components" + +import CheckIcon from "@/components/Icons/Check" + +import styles from "./filterCheckbox.module.css" + +import type { FilterCheckboxProps } from "@/types/components/hotelReservation/selectHotel/filterCheckbox" + +export default function FilterCheckbox({ + isSelected, + name, + id, + onChange, +}: FilterCheckboxProps) { + return ( + onChange(id)} + > + {({ isSelected }) => ( + <> + + + {isSelected && } + + {name} + + + )} + + ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css index c81b31cbd..8a4fcebff 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css +++ b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css @@ -1,6 +1,5 @@ .container { min-width: 272px; - display: none; } .container form { @@ -39,9 +38,3 @@ height: 1.25rem; margin: 0; } - -@media (min-width: 768px) { - .container { - display: block; - } -} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index a3b68b28e..c428894a3 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -1,37 +1,42 @@ "use client" import { usePathname, useSearchParams } from "next/navigation" -import { useCallback, useEffect } from "react" -import { FormProvider, useForm } from "react-hook-form" +import { useEffect } from "react" import { useIntl } from "react-intl" -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import { useHotelFilterStore } from "@/stores/hotel-filters" + import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Title from "@/components/TempDesignSystem/Text/Title" +import FilterCheckbox from "./FilterCheckbox" + import styles from "./hotelFilter.module.css" import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -export default function HotelFilter({ filters }: HotelFiltersProps) { +export default function HotelFilter({ className, filters }: HotelFiltersProps) { const intl = useIntl() const searchParams = useSearchParams() const pathname = usePathname() + const toggleFilter = useHotelFilterStore((state) => state.toggleFilter) + const setFilters = useHotelFilterStore((state) => state.setFilters) + const activeFilters = useHotelFilterStore((state) => state.activeFilters) - const methods = useForm>({ - defaultValues: searchParams - ?.get("filters") - ?.split(",") - .reduce((acc, curr) => ({ ...acc, [curr]: true }), {}), - }) - const { watch, handleSubmit, getValues, register } = methods + // Initialize the filters from the URL + useEffect(() => { + const filtersFromUrl = searchParams.get("filters") + if (filtersFromUrl) { + setFilters(filtersFromUrl.split(",")) + } else { + setFilters([]) + } + }, [searchParams, setFilters]) - const submitFilter = useCallback(() => { + // Update the URL when the filters changes + useEffect(() => { const newSearchParams = new URLSearchParams(searchParams) - const values = Object.entries(getValues()) - .filter(([_, value]) => !!value) - .map(([key, _]) => key) - .join(",") + const values = activeFilters.join(",") if (values === "") { newSearchParams.delete("filters") @@ -46,49 +51,51 @@ export default function HotelFilter({ filters }: HotelFiltersProps) { `${pathname}?${newSearchParams.toString()}` ) } - }, [getValues, pathname, searchParams]) - - useEffect(() => { - const subscription = watch(() => handleSubmit(submitFilter)()) - return () => subscription.unsubscribe() - }, [handleSubmit, watch, submitFilter]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeFilters]) if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) { return null } return ( -
    )} -
    - - -
    + ) diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts index 6183c5da4..ba40ccc3a 100644 --- a/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -8,9 +8,9 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { lng: hotel.hotelData.location.longitude, }, name: hotel.hotelData.name, - publicPrice: hotel.price?.regularAmount ?? null, - memberPrice: hotel.price?.memberAmount ?? null, - currency: hotel.price?.currency || null, + publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null, + memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null, + currency: hotel.price?.public?.localPrice.currency || null, images: [ hotel.hotelData.hotelContent.images, ...(hotel.hotelData.gallery?.heroImages ?? []), diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index c4a5a1eee..ab2d45d3e 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -12,6 +12,7 @@ import styles from "./hotelCardListing.module.css" import { type HotelCardListingProps, HotelCardListingTypeEnum, + type HotelData, } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" @@ -43,10 +44,15 @@ export default function HotelCardListing({ (a.hotelData.ratings?.tripAdvisor.rating ?? 0) ) case SortOrder.Price: + const getPricePerNight = (hotel: HotelData): number => { + return ( + hotel.price?.member?.localPrice?.pricePerNight ?? + hotel.price?.public?.localPrice?.pricePerNight ?? + 0 + ) + } return [...hotelData].sort( - (a, b) => - parseInt(a.price?.memberAmount ?? "0", 10) - - parseInt(b.price?.memberAmount ?? "0", 10) + (a, b) => getPricePerNight(a) - getPricePerNight(b) ) case SortOrder.Distance: default: diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index 9d6f24e00..af80e1f4d 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -49,7 +49,7 @@ export function filterDuplicateRoomTypesByLowestPrice( const previousLowest = roomMap.get(roomType) const currentRequestedPrice = Math.min( - Number(publicRequestedPrice.pricePerNight) ?? Infinity, + Number(publicRequestedPrice?.pricePerNight) ?? Infinity, Number(memberRequestedPrice?.pricePerNight) ?? Infinity ) const currentLocalPrice = Math.min( diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 02e7da88d..433a0ba33 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -348,7 +348,6 @@ "Terms and conditions": "Geschäftsbedingungen", "Thank you": "Danke", "Theatre": "Theater", - "There are no rooms available that match your request": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen", "There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2263cf43f..38a5da82b 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -378,7 +378,6 @@ "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Theatre": "Theatre", - "There are no rooms available that match your request": "There are no rooms available that match your request", "There are no rooms available that match your request.": "There are no rooms available that match your request.", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 59097c54a..218abed13 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -350,7 +350,6 @@ "Terms and conditions": "Käyttöehdot", "Thank you": "Kiitos", "Theatre": "Teatteri", - "There are no rooms available that match your request": "Pyyntöäsi vastaavia huoneita ei ole saatavilla", "There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index f5f131e8c..7cf8e985a 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -347,7 +347,6 @@ "Terms and conditions": "Vilkår og betingelser", "Thank you": "Takk", "Theatre": "Teater", - "There are no rooms available that match your request": "Det er ingen tilgjengelige rom som samsvarer med forespørselen din", "There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index bca60e315..7f935f88a 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -347,7 +347,6 @@ "Terms and conditions": "Allmänna villkor", "Thank you": "Tack", "Theatre": "Teater", - "There are no rooms available that match your request": "Det finns inga tillgängliga rum som matchar din förfrågan", "There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 407175703..4ab1aa5ea 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -82,7 +82,7 @@ export const getRoomAvailability = cache( roomStayStartDate, roomStayEndDate, children, - promotionCode, + bookingCode, rateCode, }: GetRoomsAvailabilityInput) { return serverClient().hotel.availability.rooms({ @@ -91,7 +91,7 @@ export const getRoomAvailability = cache( roomStayStartDate, roomStayEndDate, children, - promotionCode, + bookingCode, rateCode, }) } diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 04bb16b17..9bfecaf6a 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -8,9 +8,7 @@ export const getHotelsAvailabilityInputSchema = z.object({ roomStayEndDate: z.string(), adults: z.number(), children: z.string().optional(), - promotionCode: z.string().optional().default(""), - reservationProfileType: z.string().optional().default(""), - attachedProfileId: z.string().optional().default(""), + bookingCode: z.string().optional().default(""), }) export const getRoomsAvailabilityInputSchema = z.object({ @@ -19,9 +17,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ roomStayEndDate: z.string(), adults: z.number(), children: z.string().optional(), - promotionCode: z.string().optional(), - reservationProfileType: z.string().optional().default(""), - attachedProfileId: z.string().optional().default(""), + bookingCode: z.string().optional(), rateCode: z.string().optional(), }) @@ -31,9 +27,7 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({ roomStayEndDate: z.string(), adults: z.number(), children: z.string().optional(), - promotionCode: z.string().optional(), - reservationProfileType: z.string().optional().default(""), - attachedProfileId: z.string().optional().default(""), + bookingCode: z.string().optional(), rateCode: z.string(), roomTypeCode: z.string(), packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 9bc81225e..b68e87fc0 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -491,22 +491,6 @@ const occupancySchema = z.object({ children: z.array(childrenSchema), }) -const bestPricePerStaySchema = z.object({ - currency: z.string(), - // TODO: remove optional when API is ready - regularAmount: z.string().optional(), - // TODO: remove optional when API is ready - memberAmount: z.string().optional(), -}) - -const bestPricePerNightSchema = z.object({ - currency: z.string(), - // TODO: remove optional when API is ready - regularAmount: z.string().optional(), - // TODO: remove optional when API is ready - memberAmount: z.string().optional(), -}) - const linksSchema = z.object({ links: z.array( z.object({ @@ -516,30 +500,6 @@ const linksSchema = z.object({ ), }) -const hotelsAvailabilitySchema = z.object({ - data: z.array( - z.object({ - attributes: z.object({ - checkInDate: z.string(), - checkOutDate: z.string(), - occupancy: occupancySchema.optional(), - status: z.string(), - hotelId: z.number(), - ratePlanSet: z.string().optional(), - bestPricePerStay: bestPricePerStaySchema.optional(), - bestPricePerNight: bestPricePerNightSchema.optional(), - }), - relationships: linksSchema.optional(), - type: z.string().optional(), - }) - ), -}) - -export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema -export type HotelsAvailability = z.infer -export type HotelsAvailabilityPrices = - HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"] - export const priceSchema = z.object({ pricePerNight: z.coerce.number(), pricePerStay: z.coerce.number(), @@ -550,7 +510,7 @@ export const productTypePriceSchema = z.object({ rateCode: z.string(), rateType: z.string().optional(), localPrice: priceSchema, - requestedPrice: priceSchema, + requestedPrice: priceSchema.optional(), }) const productSchema = z.object({ @@ -560,6 +520,34 @@ const productSchema = z.object({ }), }) +const hotelsAvailabilitySchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + checkInDate: z.string(), + checkOutDate: z.string(), + occupancy: occupancySchema, + status: z.string(), + hotelId: z.number(), + productType: z + .object({ + public: productTypePriceSchema.optional(), + member: productTypePriceSchema.optional(), + }) + .optional(), + }), + relationships: linksSchema.optional(), + type: z.string().optional(), + }) + ), +}) + +export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema +export type HotelsAvailability = z.infer +export type ProductType = + HotelsAvailability["data"][number]["attributes"]["productType"] +export type ProductTypePrices = z.infer + const roomConfigurationSchema = z.object({ status: z.string(), roomTypeCode: z.string(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 28dd458d9..ea094de0f 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -369,9 +369,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, } = input const params: Record = { @@ -379,9 +377,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, ...(children && { children }), - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, language: apiLang, } hotelsAvailabilityCounter.add(1, { @@ -390,8 +386,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.hotelsAvailability start", @@ -414,8 +409,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -446,8 +440,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -466,8 +459,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.hotelsAvailability success", @@ -493,9 +485,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, rateCode, } = input @@ -504,9 +494,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, ...(children && { children }), - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, } roomsAvailabilityCounter.add(1, { @@ -515,8 +503,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.roomsAvailability start", @@ -540,8 +527,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -572,8 +558,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -592,8 +577,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.roomsAvailability success", @@ -620,9 +604,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, rateCode, roomTypeCode, packageCodes, @@ -633,9 +615,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, ...(children && { children }), - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, language: toApiLang(ctx.lang), } @@ -645,8 +625,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.selectedRoomAvailability start", @@ -670,8 +649,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "http_error", error: JSON.stringify({ status: apiResponseAvailability.status, @@ -702,8 +680,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -797,8 +774,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.selectedRoomAvailability success", diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 374f1dc89..b5f99cc38 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -25,7 +25,7 @@ const SESSION_STORAGE_KEY = "enterDetails" type TotalPrice = { local: { price: number; currency: string } - euro: { price: number; currency: string } + euro?: { price: number; currency: string } } export interface EnterDetailsState { diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 74ed4bda4..628fa3f8b 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -25,7 +25,7 @@ type Price = { export type RoomsData = { roomType: string localPrice: Price - euroPrice: Price + euroPrice: Price | undefined adults: number children?: Child[] cancellationText: string diff --git a/types/components/hotelReservation/selectHotel/availabilityInput.ts b/types/components/hotelReservation/selectHotel/availabilityInput.ts index 5b3a51b93..d8b7aad26 100644 --- a/types/components/hotelReservation/selectHotel/availabilityInput.ts +++ b/types/components/hotelReservation/selectHotel/availabilityInput.ts @@ -4,7 +4,5 @@ export type AvailabilityInput = { roomStayEndDate: string adults: number children?: string - promotionCode?: string - reservationProfileType?: string - attachedProfileId?: string + bookingCode?: string } diff --git a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts index 2464fad43..4144abf45 100644 --- a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts +++ b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts @@ -1,5 +1,6 @@ -import type { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" +import type { ProductType } from "@/server/routers/hotels/output" export type HotelPriceListProps = { - price: HotelsAvailabilityPrices + price: ProductType + hotelId: string } diff --git a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts index 9c56f9949..68a6174ed 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -1,4 +1,4 @@ -import { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" +import { ProductType } from "@/server/routers/hotels/output" import { Hotel } from "@/types/hotel" @@ -16,5 +16,5 @@ export type HotelCardListingProps = { export type HotelData = { hotelData: Hotel - price: HotelsAvailabilityPrices + price: ProductType } diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 233fc2105..810dba573 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -29,8 +29,8 @@ type ImageMetaData = z.infer export type HotelPin = { name: string coordinates: Coordinates - publicPrice: string | null - memberPrice: string | null + publicPrice: number | null + memberPrice: number | null currency: string | null images: { imageSizes: ImageSizes diff --git a/types/components/hotelReservation/selectHotel/priceCardProps.ts b/types/components/hotelReservation/selectHotel/priceCardProps.ts index d339b4a06..a56a67d0f 100644 --- a/types/components/hotelReservation/selectHotel/priceCardProps.ts +++ b/types/components/hotelReservation/selectHotel/priceCardProps.ts @@ -1,5 +1,6 @@ +import { ProductTypePrices } from "@/server/routers/hotels/output" + export type PriceCardProps = { - currency: string - memberAmount?: string | undefined - regularAmount?: string | undefined + productTypePrices: ProductTypePrices + isMemberPrice?: boolean } From 94f693c4f0e41372b6bb36d6778f5ce7a0c3f469 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Mon, 18 Nov 2024 09:13:23 +0100 Subject: [PATCH 019/101] feat: make steps of enter details flow dynamic depending on data --- .../[step]/@hotelHeader/[...paths]/page.tsx | 1 - .../(standard)/[step]/@hotelHeader/page.tsx | 25 --- .../(standard)/select-hotel/page.tsx | 2 +- .../{[step] => step}/@hotelHeader/loading.tsx | 0 .../step/@hotelHeader/page.module.css | 8 +- .../(standard)/step/@hotelHeader/page.tsx | 35 ++- .../{[step] => step}/@summary/page.module.css | 0 .../{[step] => step}/@summary/page.tsx | 2 +- .../(standard)/{[step] => step}/_preload.ts | 0 .../{[step] => step}/enterDetailsLayout.css | 0 .../(standard)/{[step] => step}/layout.tsx | 11 +- .../(standard)/{[step] => step}/page.tsx | 120 +++++----- .../payment-callback/[lang]/[status]/route.ts | 4 +- components/Forms/BookingWidget/index.tsx | 2 +- .../BedType/bedOptions.module.css | 3 +- .../EnterDetails/BedType/index.tsx | 20 +- .../Breakfast/breakfast.module.css | 1 - .../EnterDetails/Breakfast/index.tsx | 39 ++-- .../EnterDetails/Breakfast/schema.ts | 8 +- .../EnterDetails/Details/details.module.css | 1 - .../EnterDetails/Details/index.tsx | 42 ++-- .../HistoryStateManager/index.tsx | 6 +- .../EnterDetails/Payment/index.tsx | 16 +- .../EnterDetails/Provider/index.tsx | 29 --- .../EnterDetails/SectionAccordion/index.tsx | 29 +-- .../sectionAccordion.module.css | 7 +- .../EnterDetails/SelectedRoom/index.tsx | 9 +- .../Summary/BottomSheet/index.tsx | 6 +- .../EnterDetails/Summary/index.tsx | 192 ++++++++-------- .../HotelCard/HotelPriceList/index.tsx | 2 +- .../HotelReservation/HotelCard/index.tsx | 2 +- .../HotelCardDialog/index.tsx | 2 +- .../MobileMapButtonContainer/index.tsx | 2 +- .../SelectHotel/SelectHotelMap/index.tsx | 2 +- .../SelectRate/RoomSelection/utils.ts | 23 -- .../Form/ChoiceCard/_Card/index.tsx | 2 + components/TempDesignSystem/Select/index.tsx | 3 + constants/routes/hotelReservation.js | 126 ++++------- contexts/Details.ts | 5 + contexts/Steps.ts | 5 + hooks/useSetOverflowVisibleOnRA.ts | 11 + middlewares/bookingFlow.ts | 6 +- next.config.js | 5 + providers/DetailsProvider.tsx | 30 +++ providers/StepsProvider.tsx | 53 +++++ server/routers/hotels/schemas/packages.ts | 62 ++++++ stores/details.ts | 195 ++++++++++++++++ stores/enter-details.ts | 209 ------------------ stores/steps.ts | 159 +++++++++++++ .../enterDetails/breakfast.ts | 2 +- .../hotelReservation/enterDetails/step.ts | 7 +- .../hotelReservation/enterDetails/store.ts | 3 - .../hotelReservation/enterDetails/summary.ts | 6 + .../selectRate/sectionAccordion.ts | 2 +- types/contexts/details.ts | 3 + types/contexts/steps.ts | 3 + types/enums/breakfast.ts | 1 - types/enums/step.ts | 6 + types/providers/details.ts | 3 + types/providers/steps.ts | 10 + types/stores/details.ts | 40 ++++ types/stores/steps.ts | 10 + 62 files changed, 959 insertions(+), 659 deletions(-) delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/@hotelHeader/loading.tsx (100%) rename components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css => app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css (90%) rename components/HotelReservation/HotelSelectionHeader/index.tsx => app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx (63%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/@summary/page.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/@summary/page.tsx (99%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/_preload.ts (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/enterDetailsLayout.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/layout.tsx (73%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/page.tsx (59%) delete mode 100644 components/HotelReservation/EnterDetails/Provider/index.tsx create mode 100644 contexts/Details.ts create mode 100644 contexts/Steps.ts create mode 100644 hooks/useSetOverflowVisibleOnRA.ts create mode 100644 providers/DetailsProvider.tsx create mode 100644 providers/StepsProvider.tsx create mode 100644 server/routers/hotels/schemas/packages.ts create mode 100644 stores/details.ts delete mode 100644 stores/enter-details.ts create mode 100644 stores/steps.ts delete mode 100644 types/components/hotelReservation/enterDetails/store.ts create mode 100644 types/components/hotelReservation/enterDetails/summary.ts create mode 100644 types/contexts/details.ts create mode 100644 types/contexts/steps.ts create mode 100644 types/enums/step.ts create mode 100644 types/providers/details.ts create mode 100644 types/providers/steps.ts create mode 100644 types/stores/details.ts create mode 100644 types/stores/steps.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx deleted file mode 100644 index 03a82e5f5..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx deleted file mode 100644 index 75101475a..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { redirect } from "next/navigation" - -import { getHotelData } from "@/lib/trpc/memoizedRequests" - -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" - -import type { LangParams, PageArgs } from "@/types/params" - -export default async function HotelHeader({ - params, - searchParams, -}: PageArgs) { - const home = `/${params.lang}` - if (!searchParams.hotel) { - redirect(home) - } - const hotel = await getHotelData({ - hotelId: searchParams.hotel, - language: params.lang, - }) - if (!hotel?.data) { - redirect(home) - } - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 791773f94..a12639acf 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -86,7 +86,7 @@ export default async function SelectHotelPage({
    diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx diff --git a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css similarity index 90% rename from components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css index 9eefdfb33..82d6353ac 100644 --- a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css @@ -1,9 +1,9 @@ -.hotelSelectionHeader { +.header { background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x3) var(--Spacing-x2); } -.hotelSelectionHeaderWrapper { +.wrapper { display: flex; flex-direction: column; gap: var(--Spacing-x3); @@ -35,11 +35,11 @@ } @media (min-width: 768px) { - .hotelSelectionHeader { + .header { padding: var(--Spacing-x4) 0; } - .hotelSelectionHeaderWrapper { + .wrapper { flex-direction: row; gap: var(--Spacing-x6); margin: 0 auto; diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx similarity index 63% rename from components/HotelReservation/HotelSelectionHeader/index.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx index c0045dff5..83412f1d1 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx @@ -1,23 +1,38 @@ -"use client" -import { useIntl } from "react-intl" +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" -import styles from "./hotelSelectionHeader.module.css" +import styles from "./page.module.css" -import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader" +import type { LangParams, PageArgs } from "@/types/params" -export default function HotelSelectionHeader({ - hotel, -}: HotelSelectionHeaderProps) { - const intl = useIntl() +export default async function HotelHeader({ + params, + searchParams, +}: PageArgs) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotelData = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) + if (!hotelData?.data) { + redirect(home) + } + const intl = await getIntl() + const hotel = hotelData.data.attributes return ( -
    -
    +
    +
    {hotel.name} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx similarity index 99% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index d3228e7b7..da3554f50 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -61,7 +61,7 @@ export default async function SummaryPage({ if (!availability || !availability.selectedRoom) { console.error("No hotel or availability data", availability) // TODO: handle this case - redirect(selectRate[params.lang]) + redirect(selectRate(params.lang)) } const prices = diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx similarity index 73% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx index fbd462544..2bd8a5102 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx @@ -1,20 +1,19 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" -import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import { setLang } from "@/i18n/serverContext" +import DetailsProvider from "@/providers/DetailsProvider" import { preload } from "./_preload" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" export default async function StepLayout({ - summary, children, hotelHeader, params, + summary, }: React.PropsWithChildren< - LayoutArgs<LangParams & { step: StepEnum }> & { + LayoutArgs<LangParams> & { hotelHeader: React.ReactNode summary: React.ReactNode } @@ -25,7 +24,7 @@ export default async function StepLayout({ const user = await getProfileSafely() return ( - <EnterDetailsProvider step={params.step} isMember={!!user}> + <DetailsProvider isMember={!!user}> <main className="enter-details-layout__layout"> {hotelHeader} <div className={"enter-details-layout__container"}> @@ -35,6 +34,6 @@ export default async function StepLayout({ </aside> </div> </main> - </EnterDetailsProvider> + </DetailsProvider> ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx similarity index 59% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index ae04c61a6..648cdff93 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,6 +1,6 @@ import "./enterDetailsLayout.css" -import { notFound } from "next/navigation" +import { notFound, redirect, RedirectType } from "next/navigation" import { getBreakfastPackages, @@ -22,9 +22,10 @@ import { getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" +import StepsProvider from "@/providers/StepsProvider" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { StepEnum } from "@/types/enums/step" import type { LangParams, PageArgs } from "@/types/params" function isValidStep(step: string): step is StepEnum { @@ -32,11 +33,9 @@ function isValidStep(step: string): step is StepEnum { } export default async function StepPage({ - params, + params: { lang }, searchParams, -}: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) { - const { lang } = params - +}: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) { const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) const { @@ -88,7 +87,7 @@ export default async function StepPage({ const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() - if (!isValidStep(params.step) || !hotelData || !roomAvailability) { + if (!isValidStep(searchParams.step) || !hotelData || !roomAvailability) { return notFound() } @@ -113,54 +112,65 @@ export default async function StepPage({ } return ( - <section> - <HistoryStateManager /> - <SelectedRoom - hotelId={hotelId} - room={roomAvailability.selectedRoom} - rateDescription={roomAvailability.cancellationText} - /> - - {/* TODO: How to handle no beds found? */} - {roomAvailability.bedTypes ? ( - <SectionAccordion - header="Select bed" - step={StepEnum.selectBed} - label={intl.formatMessage({ id: "Request bedtype" })} - > - <BedType bedTypes={roomAvailability.bedTypes} /> - </SectionAccordion> - ) : null} - - <SectionAccordion - header={intl.formatMessage({ id: "Food options" })} - step={StepEnum.breakfast} - label={intl.formatMessage({ id: "Select breakfast options" })} - > - <Breakfast packages={breakfastPackages} /> - </SectionAccordion> - <SectionAccordion - header={intl.formatMessage({ id: "Details" })} - step={StepEnum.details} - label={intl.formatMessage({ id: "Enter your details" })} - > - <Details user={user} /> - </SectionAccordion> - <SectionAccordion - header={mustBeGuaranteed ? paymentGuarantee : payment} - step={StepEnum.payment} - label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} - > - <Payment - roomPrice={roomPrice} - otherPaymentOptions={ - hotelData.data.attributes.merchantInformationData - .alternatePaymentOptions - } - savedCreditCards={savedCreditCards} - mustBeGuaranteed={mustBeGuaranteed} + <StepsProvider + bedTypes={roomAvailability.bedTypes} + breakfastPackages={breakfastPackages} + isMember={!!user} + step={searchParams.step} + > + <section> + <HistoryStateManager /> + <SelectedRoom + hotelId={hotelId} + room={roomAvailability.selectedRoom} + rateDescription={roomAvailability.cancellationText} /> - </SectionAccordion> - </section> + + {/* TODO: How to handle no beds found? */} + {roomAvailability.bedTypes ? ( + <SectionAccordion + header={intl.formatMessage({ id: "Select bed" })} + step={StepEnum.selectBed} + label={intl.formatMessage({ id: "Request bedtype" })} + > + <BedType bedTypes={roomAvailability.bedTypes} /> + </SectionAccordion> + ) : null} + + {breakfastPackages?.length ? ( + <SectionAccordion + header={intl.formatMessage({ id: "Food options" })} + step={StepEnum.breakfast} + label={intl.formatMessage({ id: "Select breakfast options" })} + > + <Breakfast packages={breakfastPackages} /> + </SectionAccordion> + ) : null} + + <SectionAccordion + header={intl.formatMessage({ id: "Details" })} + step={StepEnum.details} + label={intl.formatMessage({ id: "Enter your details" })} + > + <Details user={user} /> + </SectionAccordion> + + <SectionAccordion + header={mustBeGuaranteed ? paymentGuarantee : payment} + step={StepEnum.payment} + label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} + > + <Payment + roomPrice={roomPrice} + otherPaymentOptions={ + hotelData.data.attributes.merchantInformationData + .alternatePaymentOptions + } + savedCreditCards={savedCreditCards} + mustBeGuaranteed={mustBeGuaranteed} + /> + </SectionAccordion> + </section> + </StepsProvider> ) } diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts index 624df1f52..5884d63f9 100644 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -26,7 +26,7 @@ export async function GET( const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER) if (status === "success" && confirmationNumber) { - const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`) + const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation(lang)}`) confirmationUrl.searchParams.set( BOOKING_CONFIRMATION_NUMBER, confirmationNumber @@ -36,7 +36,7 @@ export async function GET( return NextResponse.redirect(confirmationUrl) } - const returnUrl = new URL(`${publicURL}/${payment[lang]}`) + const returnUrl = new URL(`${publicURL}/${payment(lang)}`) returnUrl.search = queryParams.toString() if (confirmationNumber) { diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index b9ea569e8..b47ae74aa 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -35,7 +35,7 @@ export default function Form({ const locationData: Location = JSON.parse(decodeURIComponent(data.location)) const bookingFlowPage = - locationData.type == "cities" ? selectHotel[lang] : selectRate[lang] + locationData.type == "cities" ? selectHotel(lang) : selectRate(lang) const bookingWidgetParams = new URLSearchParams(data.date) if (locationData.type == "cities") diff --git a/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css b/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css index 81fd223b9..844ed4a6b 100644 --- a/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css +++ b/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css @@ -1,7 +1,6 @@ .form { display: grid; gap: var(--Spacing-x2); - grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - padding-bottom: var(--Spacing-x3); + grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); width: min(600px, 100%); } diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 1bf78fee5..eeb8237a0 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -4,7 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import { KingBedIcon } from "@/components/Icons" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" @@ -19,22 +20,18 @@ import type { } from "@/types/components/hotelReservation/enterDetails/bedType" export default function BedType({ bedTypes }: BedTypeProps) { - const bedType = useEnterDetailsStore((state) => state.userData.bedType) + const bedType = useDetailsStore((state) => state.data.bedType?.roomTypeCode) + const completeStep = useStepsStore((state) => state.completeStep) + const updateBedType = useDetailsStore((state) => state.actions.updateBedType) const methods = useForm<BedTypeFormSchema>({ - defaultValues: bedType?.roomTypeCode - ? { - bedType: bedType.roomTypeCode, - } - : undefined, + defaultValues: bedType ? { bedType } : undefined, criteriaMode: "all", mode: "all", resolver: zodResolver(bedTypeFormSchema), reValidateMode: "onChange", }) - const completeStep = useEnterDetailsStore((state) => state.completeStep) - const onSubmit = useCallback( (bedTypeRoomCode: BedTypeFormSchema) => { const matchingRoom = bedTypes.find( @@ -45,10 +42,11 @@ export default function BedType({ bedTypes }: BedTypeProps) { description: matchingRoom.description, roomTypeCode: matchingRoom.value, } - completeStep({ bedType }) + updateBedType(bedType) + completeStep() } }, - [completeStep, bedTypes] + [bedTypes, completeStep, updateBedType] ) useEffect(() => { diff --git a/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css b/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css index 81fd223b9..f24c6ba64 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css +++ b/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css @@ -2,6 +2,5 @@ display: grid; gap: var(--Spacing-x2); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - padding-bottom: var(--Spacing-x3); width: min(600px, 100%); } diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index 00d5ab4cc..fdaec3a84 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -5,7 +5,8 @@ import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" @@ -23,34 +24,37 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default function Breakfast({ packages }: BreakfastProps) { const intl = useIntl() - const breakfast = useEnterDetailsStore((state) => state.userData.breakfast) + const breakfast = useDetailsStore(({ data }) => + data.breakfast + ? data.breakfast.code + : data.breakfast === false + ? "false" + : data.breakfast + ) + const updateBreakfast = useDetailsStore( + (state) => state.actions.updateBreakfast + ) + const completeStep = useStepsStore((state) => state.completeStep) - let defaultValues = undefined - if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { - defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST } - } else if (breakfast?.code) { - defaultValues = { breakfast: breakfast.code } - } const methods = useForm<BreakfastFormSchema>({ - defaultValues, + defaultValues: breakfast ? { breakfast } : undefined, criteriaMode: "all", mode: "all", resolver: zodResolver(breakfastFormSchema), reValidateMode: "onChange", }) - const completeStep = useEnterDetailsStore((state) => state.completeStep) - const onSubmit = useCallback( (values: BreakfastFormSchema) => { const pkg = packages?.find((p) => p.code === values.breakfast) if (pkg) { - completeStep({ breakfast: pkg }) + updateBreakfast(pkg) } else { - completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST }) + updateBreakfast(false) } + completeStep() }, - [completeStep, packages] + [completeStep, packages, updateBreakfast] ) useEffect(() => { @@ -61,10 +65,6 @@ export default function Breakfast({ packages }: BreakfastProps) { return () => subscription.unsubscribe() }, [methods, onSubmit]) - if (!packages) { - return null - } - return ( <FormProvider {...methods}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> @@ -100,7 +100,6 @@ export default function Breakfast({ packages }: BreakfastProps) { /> ))} <RadioCard - id={BreakfastPackageEnum.NO_BREAKFAST} name="breakfast" subtitle={intl.formatMessage( { id: "{amount} {currency}" }, @@ -113,7 +112,7 @@ export default function Breakfast({ packages }: BreakfastProps) { id: "You can always change your mind later and add breakfast at the hotel.", })} title={intl.formatMessage({ id: "No breakfast" })} - value={BreakfastPackageEnum.NO_BREAKFAST} + value="false" /> </form> </FormProvider> diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts index 5f8c1f354..4766980cb 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/schema.ts +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -2,14 +2,10 @@ import { z } from "zod" import { breakfastPackageSchema } from "@/server/routers/hotels/output" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" - export const breakfastStoreSchema = z.object({ - breakfast: breakfastPackageSchema.or( - z.literal(BreakfastPackageEnum.NO_BREAKFAST) - ), + breakfast: breakfastPackageSchema.or(z.literal(false)), }) export const breakfastFormSchema = z.object({ - breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)), + breakfast: z.string().or(z.literal("false")), }) diff --git a/components/HotelReservation/EnterDetails/Details/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index 62a947e3e..f89dfa7cc 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,7 +1,6 @@ .form { display: grid; gap: var(--Spacing-x2); - padding: var(--Spacing-x3) 0px; } .container { diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 806776ce8..dd5959c31 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -1,9 +1,11 @@ "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 { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import Button from "@/components/TempDesignSystem/Button" import CountrySelect from "@/components/TempDesignSystem/Form/Country" @@ -24,19 +26,22 @@ import type { const formID = "enter-details" export default function Details({ user }: DetailsProps) { const intl = useIntl() - const initialData = useEnterDetailsStore((state) => ({ - countryCode: state.userData.countryCode, - email: state.userData.email, - firstName: state.userData.firstName, - lastName: state.userData.lastName, - phoneNumber: state.userData.phoneNumber, - join: state.userData.join, - dateOfBirth: state.userData.dateOfBirth, - zipCode: state.userData.zipCode, - termsAccepted: state.userData.termsAccepted, - membershipNo: state.userData.membershipNo, + const initialData = useDetailsStore((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, + membershipNo: state.data.membershipNo, })) + const updateDetails = useDetailsStore((state) => state.actions.updateDetails) + const completeStep = useStepsStore((state) => state.completeStep) + const methods = useForm<DetailsSchema>({ defaultValues: { countryCode: user?.address?.countryCode ?? initialData.countryCode, @@ -56,14 +61,20 @@ export default function Details({ user }: DetailsProps) { reValidateMode: "onChange", }) - const completeStep = useEnterDetailsStore((state) => state.completeStep) + const onSubmit = useCallback( + (values: DetailsSchema) => { + updateDetails(values) + completeStep() + }, + [completeStep, updateDetails] + ) return ( <FormProvider {...methods}> <form className={styles.form} id={formID} - onSubmit={methods.handleSubmit(completeStep)} + onSubmit={methods.handleSubmit(onSubmit)} > {user ? null : <Signup name="join" />} <Footnote @@ -107,7 +118,7 @@ export default function Details({ user }: DetailsProps) { readOnly={!!user} registerOptions={{ required: true }} /> - {user ? null : ( + {user || methods.watch("join") ? null : ( <Input className={styles.membershipNo} label={intl.formatMessage({ id: "Membership no" })} @@ -119,7 +130,6 @@ export default function Details({ user }: DetailsProps) { <footer className={styles.footer}> <Button disabled={!methods.formState.isValid} - form={formID} intent="secondary" size="small" theme="base" diff --git a/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx index 0ce1b1080..dc83a8072 100644 --- a/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx +++ b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx @@ -2,11 +2,11 @@ import { useCallback, useEffect } from "react" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useStepsStore } from "@/stores/steps" export default function HistoryStateManager() { - const setCurrentStep = useEnterDetailsStore((state) => state.setCurrentStep) - const currentStep = useEnterDetailsStore((state) => state.currentStep) + const setCurrentStep = useStepsStore((state) => state.setStep) + const currentStep = useStepsStore((state) => state.currentStep) const handleBackButton = useCallback( (event: PopStateEvent) => { diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index ff402bf2d..a912f74b9 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -18,7 +18,7 @@ import { } from "@/constants/currentWebHrefs" import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" @@ -40,7 +40,6 @@ import styles from "./payment.module.css" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" const maxRetries = 4 const retryInterval = 2000 @@ -61,12 +60,9 @@ export default function Payment({ const lang = useLang() const intl = useIntl() const queryParams = useSearchParams() - const { userData, roomData, setIsSubmittingDisabled } = useEnterDetailsStore( - (state) => ({ - userData: state.userData, - roomData: state.roomData, - setIsSubmittingDisabled: state.setIsSubmittingDisabled, - }) + const { booking, ...userData } = useDetailsStore((state) => state.data) + const setIsSubmittingDisabled = useDetailsStore( + (state) => state.actions.setIsSubmittingDisabled ) const { @@ -82,7 +78,7 @@ export default function Payment({ dateOfBirth, zipCode, } = userData - const { toDate, fromDate, rooms: rooms, hotel } = roomData + const { toDate, fromDate, rooms, hotel } = booking const [confirmationNumber, setConfirmationNumber] = useState<string>("") const [availablePaymentOptions, setAvailablePaymentOptions] = @@ -204,7 +200,7 @@ export default function Payment({ postalCode: zipCode, }, packages: { - breakfast: breakfast !== BreakfastPackageEnum.NO_BREAKFAST, + breakfast: !!(breakfast && breakfast.code), allergyFriendly: room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, petFriendly: diff --git a/components/HotelReservation/EnterDetails/Provider/index.tsx b/components/HotelReservation/EnterDetails/Provider/index.tsx deleted file mode 100644 index 82bfdbd82..000000000 --- a/components/HotelReservation/EnterDetails/Provider/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client" -import { useSearchParams } from "next/navigation" -import { PropsWithChildren, useRef } from "react" - -import { - EnterDetailsContext, - type EnterDetailsStore, - initEditDetailsState, -} from "@/stores/enter-details" - -import { EnterDetailsProviderProps } from "@/types/components/hotelReservation/enterDetails/store" - -export default function EnterDetailsProvider({ - step, - isMember, - children, -}: PropsWithChildren<EnterDetailsProviderProps>) { - const searchParams = useSearchParams() - const initialStore = useRef<EnterDetailsStore>() - if (!initialStore.current) { - initialStore.current = initEditDetailsState(step, searchParams, isMember) - } - - return ( - <EnterDetailsContext.Provider value={initialStore.current}> - {children} - </EnterDetailsContext.Provider> - ) -} diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index dee985295..ce548ae74 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from "react" import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import { CheckIcon, ChevronDownIcon } from "@/components/Icons" import Footnote from "@/components/TempDesignSystem/Text/Footnote" @@ -10,12 +11,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./sectionAccordion.module.css" -import { - StepEnum, - StepStoreKeys, -} from "@/types/components/hotelReservation/enterDetails/step" +import { StepStoreKeys } from "@/types/components/hotelReservation/enterDetails/step" import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import { StepEnum } from "@/types/enums/step" export default function SectionAccordion({ header, @@ -24,12 +22,12 @@ export default function SectionAccordion({ children, }: React.PropsWithChildren<SectionAccordionProps>) { const intl = useIntl() - const currentStep = useEnterDetailsStore((state) => state.currentStep) + const currentStep = useStepsStore((state) => state.currentStep) const [isComplete, setIsComplete] = useState(false) const [isOpen, setIsOpen] = useState(false) - const isValid = useEnterDetailsStore((state) => state.isValid[step]) - const navigate = useEnterDetailsStore((state) => state.navigate) - const stepData = useEnterDetailsStore((state) => state.userData) + const isValid = useDetailsStore((state) => state.isValid[step]) + const navigate = useStepsStore((state) => state.navigate) + const stepData = useDetailsStore((state) => state.data) const stepStoreKey = StepStoreKeys[step] const [title, setTitle] = useState(label) @@ -39,9 +37,12 @@ export default function SectionAccordion({ value && setTitle(value.description) } // If breakfast step, check if an option has been selected - if (step === StepEnum.breakfast && stepData.breakfast) { + if ( + step === StepEnum.breakfast && + (stepData.breakfast || stepData.breakfast === false) + ) { const value = stepData.breakfast - if (value === BreakfastPackageEnum.NO_BREAKFAST) { + if (value === false) { setTitle(intl.formatMessage({ id: "No breakfast" })) } else { setTitle(intl.formatMessage({ id: "Breakfast buffet" })) @@ -94,7 +95,9 @@ export default function SectionAccordion({ )} </button> </header> - <div className={styles.content}>{children}</div> + <div className={styles.content}> + <div className={styles.contentWrapper}>{children}</div> + </div> </div> </section> ) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index fc3de1764..ed91cb9e2 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -31,7 +31,6 @@ .main { display: grid; - gap: var(--Spacing-x3); width: 100%; border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-bottom: var(--Spacing-x3); @@ -80,6 +79,10 @@ overflow: hidden; } +.contentWrapper { + padding-top: var(--Spacing-x3); +} + @media screen and (min-width: 1367px) { .wrapper { gap: var(--Spacing-x3); @@ -98,4 +101,4 @@ content: ""; border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } -} +} \ No newline at end of file diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx index 64e9e0960..9d373f871 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx +++ b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx @@ -2,12 +2,13 @@ import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { selectRate } from "@/constants/routes/hotelReservation" import { CheckIcon, EditIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" import ToggleSidePeek from "./ToggleSidePeek" @@ -21,8 +22,7 @@ export default function SelectedRoom({ rateDescription, }: SelectedRoomProps) { const intl = useIntl() - - const selectRateUrl = useEnterDetailsStore((state) => state.selectRateUrl) + const lang = useLang() return ( <div className={styles.wrapper}> @@ -53,7 +53,8 @@ export default function SelectedRoom({ <Link className={styles.button} color="burgundy" - href={selectRateUrl} + href={selectRate(lang)} + keepSearchParams size="small" variant="icon" > diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx index ac7921aec..9f99a56c0 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx @@ -3,7 +3,7 @@ import { PropsWithChildren } from "react" import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -17,9 +17,9 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) { const intl = useIntl() const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } = - useEnterDetailsStore((state) => ({ + useDetailsStore((state) => ({ isSummaryOpen: state.isSummaryOpen, - toggleSummaryOpen: state.toggleSummaryOpen, + toggleSummaryOpen: state.actions.toggleSummaryOpen, totalPrice: state.totalPrice, isSubmittingDisabled: state.isSubmittingDisabled, })) diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 4b093f8ea..c447c7f75 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -5,7 +5,7 @@ import { ChevronDown } from "react-feather" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" -import { EnterDetailsState, useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" import { ArrowRightIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" @@ -18,45 +18,39 @@ import useLang from "@/hooks/useLang" import styles from "./summary.module.css" -import { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" -import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import type { SummaryProps } from "@/types/components/hotelReservation/enterDetails/summary" +import type { DetailsState } from "@/types/stores/details" -function storeSelector(state: EnterDetailsState) { +function storeSelector(state: DetailsState) { return { - fromDate: state.roomData.fromDate, - toDate: state.roomData.toDate, - bedType: state.userData.bedType, - breakfast: state.userData.breakfast, - toggleSummaryOpen: state.toggleSummaryOpen, - setTotalPrice: state.setTotalPrice, + fromDate: state.data.booking.fromDate, + toDate: state.data.booking.toDate, + bedType: state.data.bedType, + breakfast: state.data.breakfast, + toggleSummaryOpen: state.actions.toggleSummaryOpen, + setTotalPrice: state.actions.setTotalPrice, totalPrice: state.totalPrice, } } -export default function Summary({ - showMemberPrice, - room, -}: { - showMemberPrice: boolean - room: RoomsData -}) { +export default function Summary({ showMemberPrice, room }: SummaryProps) { const [chosenBed, setChosenBed] = useState<BedTypeSchema>() const [chosenBreakfast, setChosenBreakfast] = useState< - BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST + BreakfastPackage | false >() const intl = useIntl() const lang = useLang() const { - fromDate, - toDate, bedType, breakfast, + fromDate, setTotalPrice, - totalPrice, + toDate, toggleSummaryOpen, - } = useEnterDetailsStore(storeSelector) + totalPrice, + } = useDetailsStore(storeSelector) const diff = dt(toDate).diff(fromDate, "days") @@ -88,36 +82,39 @@ export default function Summary({ setChosenBed(bedType) setChosenBreakfast(breakfast) - if (breakfast && breakfast !== BreakfastPackageEnum.NO_BREAKFAST) { - setTotalPrice({ - local: { - price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice), - currency: room.localPrice.currency, - }, - euro: - room.euroPrice && roomsPriceEuro - ? { + if (breakfast || breakfast === false) { + setChosenBreakfast(breakfast) + if (breakfast === false) { + setTotalPrice({ + local: { + price: roomsPriceLocal, + currency: room.localPrice.currency, + }, + euro: + room.euroPrice && roomsPriceEuro + ? { + price: roomsPriceEuro, + currency: room.euroPrice.currency, + } + : undefined, + }) + } else { + setTotalPrice({ + local: { + price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice), + currency: room.localPrice.currency, + }, + euro: + room.euroPrice && roomsPriceEuro + ? { price: roomsPriceEuro + parseInt(breakfast.requestedPrice.totalPrice), currency: room.euroPrice.currency, } - : undefined, - }) - } else { - setTotalPrice({ - local: { - price: roomsPriceLocal, - currency: room.localPrice.currency, - }, - euro: - room.euroPrice && roomsPriceEuro - ? { - price: roomsPriceEuro, - currency: room.euroPrice.currency, - } - : undefined, - }) + : undefined, + }) + } } }, [ bedType, @@ -187,24 +184,24 @@ export default function Summary({ </div> {room.packages ? room.packages.map((roomPackage) => ( - <div className={styles.entry} key={roomPackage.code}> - <div> - <Body color="uiTextHighContrast"> - {roomPackage.description} - </Body> - </div> - - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: roomPackage.localPrice.price, - currency: roomPackage.localPrice.currency, - } - )} - </Caption> + <div className={styles.entry} key={roomPackage.code}> + <div> + <Body color="uiTextHighContrast"> + {roomPackage.description} + </Body> </div> - )) + + <Caption color="uiTextHighContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: roomPackage.localPrice.price, + currency: roomPackage.localPrice.currency, + } + )} + </Caption> + </div> + )) : null} {chosenBed ? ( <div className={styles.entry}> @@ -224,37 +221,36 @@ export default function Summary({ </div> ) : null} - {chosenBreakfast ? ( - chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? ( - <div className={styles.entry}> - <Body color="uiTextHighContrast"> - {intl.formatMessage({ id: "No breakfast" })} - </Body> - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "0", currency: room.localPrice.currency } - )} - </Caption> - </div> - ) : ( - <div className={styles.entry}> - <Body color="uiTextHighContrast"> - {intl.formatMessage({ id: "Breakfast buffet" })} - </Body> - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: chosenBreakfast.localPrice.totalPrice, - currency: chosenBreakfast.localPrice.currency, - } - )} - </Caption> - </div> - ) - ) : null} - </div> + {chosenBreakfast === false ? ( + <div className={styles.entry}> + <Body color="uiTextHighContrast"> + {intl.formatMessage({ id: "No breakfast" })} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: room.localPrice.currency } + )} + </Caption> + </div> + ) : chosenBreakfast?.code ? ( + <div className={styles.entry}> + <Body color="uiTextHighContrast"> + {intl.formatMessage({ id: "Breakfast buffet" })} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: chosenBreakfast.localPrice.totalPrice, + currency: chosenBreakfast.localPrice.currency, + } + )} + </Caption> + </div> + ) : null + } + </div > <Divider color="primaryLightSubtle" /> <div className={styles.total}> <div className={styles.entry}> @@ -295,6 +291,6 @@ export default function Summary({ </div> <Divider className={styles.bottomDivider} color="primaryLightSubtle" /> </div> - </section> + </section > ) } diff --git a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx index 1f55c2d8a..d98fdc260 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx +++ b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx @@ -39,7 +39,7 @@ export default function HotelPriceList({ className={styles.button} > <Link - href={`${selectRate[lang]}?hotel=${hotelId}`} + href={`${selectRate(lang)}?hotel=${hotelId}`} color="none" keepSearchParams > diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index feda19a05..808e9ac9f 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -93,7 +93,7 @@ export default function HotelCard({ </address> <Link className={styles.addressMobile} - href={`${selectHotelMap[lang]}?selectedHotel=${hotelData.name}`} + href={`${selectHotelMap(lang)}?selectedHotel=${hotelData.name}`} keepSearchParams > <Caption color="baseTextMediumContrast" type="underline"> diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index 16d1ce860..d444a1083 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -104,7 +104,7 @@ export default function HotelCardDialog({ <Button asChild theme="base" size="small" className={styles.button}> <Link - href={`${selectRate[lang]}?hotel=${data.operaId}`} + href={`${selectRate(lang)}?hotel=${data.operaId}`} color="none" keepSearchParams > diff --git a/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx b/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx index 558a3ef50..0b4881943 100644 --- a/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx +++ b/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx @@ -27,7 +27,7 @@ export default function MobileMapButtonContainer({ <div className={styles.buttonContainer}> <Button asChild variant="icon" intent="secondary" size="small"> <Link - href={`${selectHotelMap[lang]}`} + href={selectHotelMap(lang)} keepSearchParams color="burgundy" > diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 21b804306..215c7ae66 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -71,7 +71,7 @@ export default function SelectHotelMap({ } function handlePageRedirect() { - router.push(`${selectHotel[lang]}?${searchParams.toString()}`) + router.push(`${selectHotel(lang)}?${searchParams.toString()}`) } const closeButton = ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index aa6ef2810..43af470e3 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -53,26 +53,3 @@ export function getQueryParamsForEnterDetails( })), } } - -export function createSelectRateUrl(roomData: BookingData) { - const { hotel, fromDate, toDate } = roomData - const params = new URLSearchParams({ fromDate, toDate, hotel }) - - roomData.rooms.forEach((room, index) => { - params.set(`room[${index}].adults`, room.adults.toString()) - - if (room.children) { - room.children.forEach((child, childIndex) => { - params.set( - `room[${index}].child[${childIndex}].age`, - child.age.toString() - ) - params.set( - `room[${index}].child[${childIndex}].bed`, - child.bed.toString() - ) - }) - } - }) - return params -} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index 2a3faf57b..7d6ef8105 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -15,6 +15,7 @@ export default function Card({ iconHeight = 32, iconWidth = 32, declined = false, + defaultChecked, highlightSubtitle = false, id, list, @@ -45,6 +46,7 @@ export default function Card({ <input {...register(name)} aria-hidden + defaultChecked={defaultChecked} id={id || name} hidden type={type} diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index bce5132ab..816d8ddb9 100644 --- a/components/TempDesignSystem/Select/index.tsx +++ b/components/TempDesignSystem/Select/index.tsx @@ -12,6 +12,7 @@ import { import Label from "@/components/TempDesignSystem/Form/Label" import Body from "@/components/TempDesignSystem/Text/Body" +import useSetOverflowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA" import SelectChevron from "../Form/SelectChevron" @@ -39,6 +40,7 @@ export default function Select({ discreet = false, }: SelectProps) { const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined) + const setOverflowVisible = useSetOverflowVisibleOnRA() function setRef(node: SelectPortalContainerArgs) { if (node) { @@ -60,6 +62,7 @@ export default function Select({ onSelectionChange={handleOnSelect} placeholder={placeholder} selectedKey={value as Key} + onOpenChange={setOverflowVisible} > <Body asChild fontOnly> <Button className={styles.input} data-testid={name}> diff --git a/constants/routes/hotelReservation.js b/constants/routes/hotelReservation.js index 94a9cef18..c7882f159 100644 --- a/constants/routes/hotelReservation.js +++ b/constants/routes/hotelReservation.js @@ -1,97 +1,59 @@ -/** @type {import('@/types/routes').LangRoute} */ -export const hotelReservation = { - en: "/en/hotelreservation", - sv: "/sv/hotelreservation", - no: "/no/hotelreservation", - fi: "/fi/hotelreservation", - da: "/da/hotelreservation", - de: "/de/hotelreservation", +/** + * @typedef {import('@/constants/languages').Lang} Lang + */ + +/** + * @param {Lang} lang + */ +function base(lang) { + return `/${lang}/hotelreservation` } -export const selectHotel = { - en: `${hotelReservation.en}/select-hotel`, - sv: `${hotelReservation.sv}/select-hotel`, - no: `${hotelReservation.no}/select-hotel`, - fi: `${hotelReservation.fi}/select-hotel`, - da: `${hotelReservation.da}/select-hotel`, - de: `${hotelReservation.de}/select-hotel`, +/** + * @param {Lang} lang + */ +export function bookingConfirmation(lang) { + return `${base(lang)}/booking-confirmation` } -export const selectRate = { - en: `${hotelReservation.en}/select-rate`, - sv: `${hotelReservation.sv}/select-rate`, - no: `${hotelReservation.no}/select-rate`, - fi: `${hotelReservation.fi}/select-rate`, - da: `${hotelReservation.da}/select-rate`, - de: `${hotelReservation.de}/select-rate`, +/** + * @param {Lang} lang + */ +export function details(lang) { + return `${base(lang)}/details` } -// TODO: Translate paths -export const selectBed = { - en: `${hotelReservation.en}/select-bed`, - sv: `${hotelReservation.sv}/select-bed`, - no: `${hotelReservation.no}/select-bed`, - fi: `${hotelReservation.fi}/select-bed`, - da: `${hotelReservation.da}/select-bed`, - de: `${hotelReservation.de}/select-bed`, +/** + * @param {Lang} lang + */ +export function payment(lang) { + return `${base(lang)}/payment` } -// TODO: Translate paths -export const breakfast = { - en: `${hotelReservation.en}/breakfast`, - sv: `${hotelReservation.sv}/breakfast`, - no: `${hotelReservation.no}/breakfast`, - fi: `${hotelReservation.fi}/breakfast`, - da: `${hotelReservation.da}/breakfast`, - de: `${hotelReservation.de}/breakfast`, +/** + * @param {Lang} lang + */ +export function selectBed(lang) { + return `${base(lang)}/select-bed` } -// TODO: Translate paths -export const details = { - en: `${hotelReservation.en}/details`, - sv: `${hotelReservation.sv}/details`, - no: `${hotelReservation.no}/details`, - fi: `${hotelReservation.fi}/details`, - da: `${hotelReservation.da}/details`, - de: `${hotelReservation.de}/details`, +/** + * @param {Lang} lang + */ +export function selectHotel(lang) { + return `${base(lang)}/select-hotel` } -// TODO: Translate paths -export const payment = { - en: `${hotelReservation.en}/payment`, - sv: `${hotelReservation.sv}/payment`, - no: `${hotelReservation.no}/payment`, - fi: `${hotelReservation.fi}/payment`, - da: `${hotelReservation.da}/payment`, - de: `${hotelReservation.de}/payment`, +/** + * @param {Lang} lang + */ +export function selectHotelMap(lang) { + return `${base(lang)}/map` } -export const selectHotelMap = { - en: `${selectHotel.en}/map`, - sv: `${selectHotel.sv}/map`, - no: `${selectHotel.no}/map`, - fi: `${selectHotel.fi}/map`, - da: `${selectHotel.da}/map`, - de: `${selectHotel.de}/map`, +/** + * @param {Lang} lang + */ +export function selectRate(lang) { + return `${base(lang)}/select-rate` } - -/** @type {import('@/types/routes').LangRoute} */ -export const bookingConfirmation = { - en: `${hotelReservation.en}/booking-confirmation`, - sv: `${hotelReservation.sv}/booking-confirmation`, - no: `${hotelReservation.no}/booking-confirmation`, - fi: `${hotelReservation.fi}/booking-confirmation`, - da: `${hotelReservation.da}/booking-confirmation`, - de: `${hotelReservation.de}/booking-confirmation`, -} - -export const bookingFlow = [ - ...Object.values(selectHotel), - ...Object.values(selectBed), - ...Object.values(breakfast), - ...Object.values(details), - ...Object.values(payment), - ...Object.values(selectHotelMap), - ...Object.values(bookingConfirmation), - ...Object.values(selectRate), -] diff --git a/contexts/Details.ts b/contexts/Details.ts new file mode 100644 index 000000000..7fb3a010a --- /dev/null +++ b/contexts/Details.ts @@ -0,0 +1,5 @@ +import { createContext } from "react" + +import type { DetailsStore } from "@/types/contexts/details" + +export const DetailsContext = createContext<DetailsStore | null>(null) diff --git a/contexts/Steps.ts b/contexts/Steps.ts new file mode 100644 index 000000000..220365fbe --- /dev/null +++ b/contexts/Steps.ts @@ -0,0 +1,5 @@ +import { createContext } from "react" + +import type { StepsStore } from "@/types/contexts/steps" + +export const StepsContext = createContext<StepsStore | null>(null) diff --git a/hooks/useSetOverflowVisibleOnRA.ts b/hooks/useSetOverflowVisibleOnRA.ts new file mode 100644 index 000000000..e9031b477 --- /dev/null +++ b/hooks/useSetOverflowVisibleOnRA.ts @@ -0,0 +1,11 @@ +export default function useSetOverflowVisibleOnRA() { + function setOverflowVisible(isOpen: boolean) { + if (isOpen) { + document.body.style.overflow = "visible" + } else { + document.body.style.overflow = "" + } + } + + return setOverflowVisible +} diff --git a/middlewares/bookingFlow.ts b/middlewares/bookingFlow.ts index 098ca9108..20e646e5a 100644 --- a/middlewares/bookingFlow.ts +++ b/middlewares/bookingFlow.ts @@ -1,7 +1,5 @@ import { NextResponse } from "next/server" -import { bookingFlow } from "@/constants/routes/hotelReservation" - import { getDefaultRequestHeaders } from "./utils" import type { NextMiddleware } from "next/server" @@ -18,5 +16,7 @@ export const middleware: NextMiddleware = async (request) => { } export const matcher: MiddlewareMatcher = (request) => { - return bookingFlow.includes(request.nextUrl.pathname) + return !!request.nextUrl.pathname.match( + /^\/(da|de|en|fi|no|sv)\/(hotelreservation)/ + ) } diff --git a/next.config.js b/next.config.js index 29012f230..222f085ac 100644 --- a/next.config.js +++ b/next.config.js @@ -277,6 +277,11 @@ const nextConfig = { source: `${myPages.sv}/:path*`, destination: `/sv/my-pages/:path*`, }, + { + source: + "/:lang/hotelreservation/:step(breakfast|details|payment|select-bed)", + destination: "/:lang/hotelreservation/step?step=:step", + }, ], } }, diff --git a/providers/DetailsProvider.tsx b/providers/DetailsProvider.tsx new file mode 100644 index 000000000..328307ee7 --- /dev/null +++ b/providers/DetailsProvider.tsx @@ -0,0 +1,30 @@ +"use client" +import { useSearchParams } from "next/navigation" +import { useRef } from "react" + +import { createDetailsStore } from "@/stores/details" + +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { DetailsContext } from "@/contexts/Details" + +import type { DetailsStore } from "@/types/contexts/details" +import type { DetailsProviderProps } from "@/types/providers/details" + +export default function DetailsProvider({ + children, + isMember, +}: DetailsProviderProps) { + const storeRef = useRef<DetailsStore>() + const searchParams = useSearchParams() + + if (!storeRef.current) { + const booking = getQueryParamsForEnterDetails(searchParams) + storeRef.current = createDetailsStore({ booking }, isMember) + } + + return ( + <DetailsContext.Provider value={storeRef.current}> + {children} + </DetailsContext.Provider> + ) +} diff --git a/providers/StepsProvider.tsx b/providers/StepsProvider.tsx new file mode 100644 index 000000000..87594be02 --- /dev/null +++ b/providers/StepsProvider.tsx @@ -0,0 +1,53 @@ +"use client" +import { useRef } from "react" + +import { useDetailsStore } from "@/stores/details" +import { createStepsStore } from "@/stores/steps" + +import { StepsContext } from "@/contexts/Steps" + +import type { StepsStore } from "@/types/contexts/steps" +import type { StepsProviderProps } from "@/types/providers/steps" + +export default function StepsProvider({ + bedTypes, + breakfastPackages, + children, + isMember, + step, +}: StepsProviderProps) { + const storeRef = useRef<StepsStore>() + const updateBedType = useDetailsStore((state) => state.actions.updateBedType) + const updateBreakfast = useDetailsStore( + (state) => state.actions.updateBreakfast + ) + + if (!storeRef.current) { + const noBedChoices = bedTypes.length === 1 + const noBreakfast = !breakfastPackages?.length + + if (noBedChoices) { + updateBedType({ + description: bedTypes[0].description, + roomTypeCode: bedTypes[0].value, + }) + } + + if (noBreakfast) { + updateBreakfast(false) + } + + storeRef.current = createStepsStore( + step, + isMember, + noBedChoices, + noBreakfast + ) + } + + return ( + <StepsContext.Provider value={storeRef.current}> + {children} + </StepsContext.Provider> + ) +} diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts new file mode 100644 index 000000000..988cf1ac5 --- /dev/null +++ b/server/routers/hotels/schemas/packages.ts @@ -0,0 +1,62 @@ +import { z } from "zod" + +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { CurrencyEnum } from "@/types/enums/currency" + +export const getRoomPackagesInputSchema = z.object({ + hotelId: z.string(), + startDate: z.string(), + endDate: z.string(), + adults: z.number(), + children: z.number().optional().default(0), + packageCodes: z.array(z.string()).optional().default([]), +}) + +export const packagePriceSchema = z + .object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.string(), + totalPrice: z.string(), + }) + .optional() + .default({ + currency: CurrencyEnum.SEK, + price: "0", + totalPrice: "0", + }) // TODO: Remove optional and default when the API change has been deployed + +export const packagesSchema = z.object({ + code: z.nativeEnum(RoomPackageCodeEnum), + description: z.string(), + localPrice: packagePriceSchema, + requestedPrice: packagePriceSchema, + inventories: z.array( + z.object({ + date: z.string(), + total: z.number(), + available: z.number(), + }) + ), +}) + +export const getRoomPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: z.array(packagesSchema).default([]), + }), + relationships: z + .object({ + links: z.array( + z.object({ + url: z.string(), + type: z.string(), + }) + ), + }) + .optional(), + type: z.string(), + }), + }) + .transform((data) => data.data.attributes.packages) diff --git a/stores/details.ts b/stores/details.ts new file mode 100644 index 000000000..5d23248e5 --- /dev/null +++ b/stores/details.ts @@ -0,0 +1,195 @@ +import merge from "deepmerge" +import { produce } from "immer" +import { useContext } from "react" +import { create, useStore } from "zustand" +import { createJSONStorage, persist } from "zustand/middleware" + +import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" +import { DetailsContext } from "@/contexts/Details" + +import { StepEnum } from "@/types/enums/step" +import type { DetailsState, InitialState } from "@/types/stores/details" + +export const storageName = "details-storage" +export function createDetailsStore( + initialState: InitialState, + isMember: boolean +) { + if (typeof window !== "undefined") { + /** + * We need to initialize the store from sessionStorage ourselves + * since `persist` does it first after render and therefore + * we cannot use the data as `defaultValues` for our forms. + * RHF caches defaultValues on mount. + */ + const detailsStorageUnparsed = sessionStorage.getItem(storageName) + if (detailsStorageUnparsed) { + const detailsStorage: Record< + "state", + Pick<DetailsState, "data"> + > = JSON.parse(detailsStorageUnparsed) + initialState = merge(initialState, detailsStorage.state.data) + } + } + return create<DetailsState>()( + persist( + (set) => ({ + actions: { + setIsSubmittingDisabled(isSubmittingDisabled) { + return set( + produce((state: DetailsState) => { + state.isSubmittingDisabled = isSubmittingDisabled + }) + ) + }, + setTotalPrice(totalPrice) { + return set( + produce((state: DetailsState) => { + state.totalPrice = totalPrice + }) + ) + }, + toggleSummaryOpen() { + return set( + produce((state: DetailsState) => { + state.isSummaryOpen = !state.isSummaryOpen + }) + ) + }, + updateBedType(bedType) { + return set( + produce((state: DetailsState) => { + state.isValid["select-bed"] = true + state.data.bedType = bedType + }) + ) + }, + updateBreakfast(breakfast) { + return set( + produce((state: DetailsState) => { + state.isValid.breakfast = true + state.data.breakfast = breakfast + }) + ) + }, + updateDetails(data) { + return set( + produce((state: DetailsState) => { + state.isValid.details = true + + state.data.countryCode = data.countryCode + state.data.dateOfBirth = data.dateOfBirth + state.data.email = data.email + state.data.firstName = data.firstName + state.data.join = data.join + state.data.lastName = data.lastName + if (data.join) { + state.data.membershipNo = undefined + } else { + state.data.membershipNo = data.membershipNo + } + state.data.phoneNumber = data.phoneNumber + state.data.termsAccepted = data.termsAccepted + state.data.zipCode = data.zipCode + }) + ) + }, + updateValidity(property, isValid) { + return set( + produce((state: DetailsState) => { + state.isValid[property] = isValid + }) + ) + }, + }, + + data: merge( + { + bedType: undefined, + breakfast: undefined, + countryCode: "", + dateOfBirth: "", + email: "", + firstName: "", + join: false, + lastName: "", + membershipNo: "", + phoneNumber: "", + termsAccepted: false, + zipCode: "", + }, + initialState + ), + + isSubmittingDisabled: false, + isSummaryOpen: false, + isValid: { + [StepEnum.selectBed]: false, + [StepEnum.breakfast]: false, + [StepEnum.details]: false, + [StepEnum.payment]: false, + }, + + totalPrice: { + euro: { currency: "", price: 0 }, + local: { currency: "", price: 0 }, + }, + }), + { + name: storageName, + onRehydrateStorage() { + return function (state) { + if (state) { + const validatedBedType = bedTypeSchema.safeParse(state.data) + if (validatedBedType.success) { + state.actions.updateValidity(StepEnum.selectBed, true) + } else { + state.actions.updateValidity(StepEnum.selectBed, false) + } + + const validatedBreakfast = breakfastStoreSchema.safeParse( + state.data + ) + if (validatedBreakfast.success) { + state.actions.updateValidity(StepEnum.breakfast, true) + } else { + state.actions.updateValidity(StepEnum.breakfast, false) + } + + const detailsSchema = isMember + ? signedInDetailsSchema + : guestDetailsSchema + const validatedDetails = detailsSchema.safeParse(state.data) + if (validatedDetails.success) { + state.actions.updateValidity(StepEnum.details, true) + } else { + state.actions.updateValidity(StepEnum.details, false) + } + } + } + }, + partialize(state) { + return { + data: state.data, + } + }, + storage: createJSONStorage(() => sessionStorage), + } + ) + ) +} + +export function useDetailsStore<T>(selector: (store: DetailsState) => T) { + const store = useContext(DetailsContext) + + if (!store) { + throw new Error("useDetailsStore must be used within DetailsProvider") + } + + return useStore(store, selector) +} diff --git a/stores/enter-details.ts b/stores/enter-details.ts deleted file mode 100644 index b5f99cc38..000000000 --- a/stores/enter-details.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { produce } from "immer" -import { ReadonlyURLSearchParams } from "next/navigation" -import { createContext, useContext } from "react" -import { create, useStore } from "zustand" - -import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" -import { - guestDetailsSchema, - signedInDetailsSchema, -} from "@/components/HotelReservation/EnterDetails/Details/schema" -import { - createSelectRateUrl, - getQueryParamsForEnterDetails, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" - -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" -import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" -import { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" - -const SESSION_STORAGE_KEY = "enterDetails" - -type TotalPrice = { - local: { price: number; currency: string } - euro?: { price: number; currency: string } -} - -export interface EnterDetailsState { - userData: { - bedType: BedTypeSchema | undefined - breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined - } & DetailsSchema - roomData: BookingData - steps: StepEnum[] - selectRateUrl: string - currentStep: StepEnum - totalPrice: TotalPrice - isSubmittingDisabled: boolean - isSummaryOpen: boolean - isValid: Record<StepEnum, boolean> - completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void - navigate: ( - step: StepEnum, - updatedData?: Record< - string, - string | boolean | number | BreakfastPackage | BedTypeSchema - > - ) => void - setCurrentStep: (step: StepEnum) => void - toggleSummaryOpen: () => void - setTotalPrice: (totalPrice: TotalPrice) => void - setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void -} - -export function initEditDetailsState( - currentStep: StepEnum, - searchParams: ReadonlyURLSearchParams, - isMember: boolean -) { - const isBrowser = typeof window !== "undefined" - const sessionData = isBrowser - ? sessionStorage.getItem(SESSION_STORAGE_KEY) - : null - - let roomData: BookingData - let selectRateUrl: string - if (searchParams?.size) { - const data = getQueryParamsForEnterDetails(searchParams) - roomData = data - selectRateUrl = `select-rate?${createSelectRateUrl(data)}` - } - - const defaultUserData: EnterDetailsState["userData"] = { - bedType: undefined, - breakfast: undefined, - countryCode: "", - email: "", - firstName: "", - lastName: "", - phoneNumber: "", - join: false, - zipCode: "", - dateOfBirth: undefined, - termsAccepted: false, - membershipNo: "", - } - - let inputUserData = {} - if (sessionData) { - inputUserData = JSON.parse(sessionData) - } - - const validPaths = [StepEnum.selectBed] - - let initialData: EnterDetailsState["userData"] = defaultUserData - - const isValid = { - [StepEnum.selectBed]: false, - [StepEnum.breakfast]: false, - [StepEnum.details]: false, - [StepEnum.payment]: false, - } - - const validatedBedType = bedTypeSchema.safeParse(inputUserData) - if (validatedBedType.success) { - validPaths.push(StepEnum.breakfast) - initialData = { ...initialData, ...validatedBedType.data } - isValid[StepEnum.selectBed] = true - } - const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData) - if (validatedBreakfast.success) { - validPaths.push(StepEnum.details) - initialData = { ...initialData, ...validatedBreakfast.data } - isValid[StepEnum.breakfast] = true - } - const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema - const validatedDetails = detailsSchema.safeParse(inputUserData) - if (validatedDetails.success) { - validPaths.push(StepEnum.payment) - initialData = { ...initialData, ...validatedDetails.data } - isValid[StepEnum.details] = true - } - - if (!validPaths.includes(currentStep)) { - currentStep = validPaths.pop()! // We will always have at least one valid path - if (isBrowser) { - window.history.pushState( - { step: currentStep }, - "", - currentStep + window.location.search - ) - } - } - - return create<EnterDetailsState>()((set, get) => ({ - userData: initialData, - roomData, - selectRateUrl, - steps: Object.values(StepEnum), - totalPrice: { - local: { price: 0, currency: "" }, - euro: { price: 0, currency: "" }, - }, - isSummaryOpen: false, - isSubmittingDisabled: false, - setCurrentStep: (step) => set({ currentStep: step }), - navigate: (step, updatedData) => - set( - produce((state) => { - const sessionStorage = window.sessionStorage - - const previousDataString = sessionStorage.getItem(SESSION_STORAGE_KEY) - - const previousData = JSON.parse(previousDataString || "{}") - - sessionStorage.setItem( - SESSION_STORAGE_KEY, - JSON.stringify({ ...previousData, ...updatedData }) - ) - - state.currentStep = step - window.history.pushState({ step }, "", step + window.location.search) - }) - ), - currentStep, - isValid, - completeStep: (updatedData) => - set( - produce((state: EnterDetailsState) => { - state.isValid[state.currentStep] = true - - const nextStep = - state.steps[state.steps.indexOf(state.currentStep) + 1] - - state.userData = { - ...state.userData, - ...updatedData, - } - state.currentStep = nextStep - get().navigate(nextStep, updatedData) - }) - ), - toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }), - setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }), - setIsSubmittingDisabled: (isSubmittingDisabled) => - set({ isSubmittingDisabled }), - })) -} - -export type EnterDetailsStore = ReturnType<typeof initEditDetailsState> - -export const EnterDetailsContext = createContext<EnterDetailsStore | null>(null) - -export const useEnterDetailsStore = <T>( - selector: (store: EnterDetailsState) => T -): T => { - const enterDetailsContextStore = useContext(EnterDetailsContext) - - if (!enterDetailsContextStore) { - throw new Error( - `useEnterDetailsStore must be used within EnterDetailsContextProvider` - ) - } - - return useStore(enterDetailsContextStore, selector) -} diff --git a/stores/steps.ts b/stores/steps.ts new file mode 100644 index 000000000..f1e456af2 --- /dev/null +++ b/stores/steps.ts @@ -0,0 +1,159 @@ +"use client" +import merge from "deepmerge" +import { produce } from "immer" +import { useContext } from "react" +import { create, useStore } from "zustand" + +import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" +import { StepsContext } from "@/contexts/Steps" + +import { storageName as detailsStorageName } from "./details" + +import { StepEnum } from "@/types/enums/step" +import type { DetailsState } from "@/types/stores/details" +import type { StepState } from "@/types/stores/steps" + +function push(data: Record<string, string>, url: string) { + if (typeof window !== "undefined") { + window.history.pushState(data, "", url + window.location.search) + } +} + +export function createStepsStore( + currentStep: StepEnum, + isMember: boolean, + noBedChoices: boolean, + noBreakfast: boolean +) { + const isBrowser = typeof window !== "undefined" + const steps = [ + StepEnum.selectBed, + StepEnum.breakfast, + StepEnum.details, + StepEnum.payment, + ] + + /** + * TODO: + * - when included in rate, can packages still be received? + * - no hotels yet with breakfast included in the rate so + * impossible to build for atm. + * + * matching breakfast first so the steps array is altered + * before the bedTypes possible step altering + */ + if (noBreakfast) { + steps.splice(1, 1) + if (currentStep === StepEnum.breakfast) { + currentStep = steps[1] + push({ step: currentStep }, currentStep) + } + } + + if (noBedChoices) { + if (currentStep === StepEnum.selectBed) { + currentStep = steps[1] + push({ step: currentStep }, currentStep) + } + } + + const detailsStorageUnparsed = isBrowser + ? sessionStorage.getItem(detailsStorageName) + : null + if (detailsStorageUnparsed) { + const detailsStorage: Record< + "state", + Pick<DetailsState, "data"> + > = JSON.parse(detailsStorageUnparsed) + + const validPaths = [StepEnum.selectBed] + + const validatedBedType = bedTypeSchema.safeParse(detailsStorage.state.data) + if (validatedBedType.success) { + validPaths.push(steps[1]) + } + + const validatedBreakfast = breakfastStoreSchema.safeParse( + detailsStorage.state.data + ) + if (validatedBreakfast.success) { + validPaths.push(StepEnum.details) + } + + const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema + const validatedDetails = detailsSchema.safeParse(detailsStorage.state.data) + if (validatedDetails.success) { + validPaths.push(StepEnum.payment) + } + + if (!validPaths.includes(currentStep) && isBrowser) { + // We will always have at least one valid path + currentStep = validPaths.pop()! + push({ step: currentStep }, currentStep) + } + } + + const initalData = { + currentStep, + steps, + } + + return create<StepState>()((set) => + merge( + { + currentStep: StepEnum.selectBed, + steps: [], + + completeStep() { + return set( + produce((state: StepState) => { + const currentStepIndex = state.steps.indexOf(state.currentStep) + const nextStep = state.steps[currentStepIndex + 1] + state.currentStep = nextStep + window.history.pushState( + { step: nextStep }, + "", + nextStep + window.location.search + ) + }) + ) + }, + navigate(step: StepEnum) { + return set( + produce((state) => { + state.currentStep = step + window.history.pushState( + { step }, + "", + step + window.location.search + ) + }) + ) + }, + setStep(step: StepEnum) { + return set( + produce((state: StepState) => { + state.currentStep = step + }) + ) + }, + }, + initalData + ) + ) +} + +export function useStepsStore<T>(selector: (store: StepState) => T) { + const store = useContext(StepsContext) + + if (!store) { + throw new Error(`useStepsStore must be used within StepsProvider`) + } + + return useStore(store, selector) +} diff --git a/types/components/hotelReservation/enterDetails/breakfast.ts b/types/components/hotelReservation/enterDetails/breakfast.ts index 21ba37bd0..283d85028 100644 --- a/types/components/hotelReservation/enterDetails/breakfast.ts +++ b/types/components/hotelReservation/enterDetails/breakfast.ts @@ -17,5 +17,5 @@ export interface BreakfastPackage extends z.output<typeof breakfastPackageSchema> {} export interface BreakfastProps { - packages: BreakfastPackages | null + packages: BreakfastPackages } diff --git a/types/components/hotelReservation/enterDetails/step.ts b/types/components/hotelReservation/enterDetails/step.ts index 45de5a009..8c8c967ef 100644 --- a/types/components/hotelReservation/enterDetails/step.ts +++ b/types/components/hotelReservation/enterDetails/step.ts @@ -1,9 +1,4 @@ -export enum StepEnum { - selectBed = "select-bed", - breakfast = "breakfast", - details = "details", - payment = "payment", -} +import { StepEnum } from "@/types/enums/step" export const StepStoreKeys: Record<StepEnum, "bedType" | "breakfast" | null> = { "select-bed": "bedType", diff --git a/types/components/hotelReservation/enterDetails/store.ts b/types/components/hotelReservation/enterDetails/store.ts deleted file mode 100644 index 45dbd5f75..000000000 --- a/types/components/hotelReservation/enterDetails/store.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { StepEnum } from "./step" - -export type EnterDetailsProviderProps = { step: StepEnum; isMember: boolean } diff --git a/types/components/hotelReservation/enterDetails/summary.ts b/types/components/hotelReservation/enterDetails/summary.ts new file mode 100644 index 000000000..901113414 --- /dev/null +++ b/types/components/hotelReservation/enterDetails/summary.ts @@ -0,0 +1,6 @@ +import type { RoomsData } from "./bookingData" + +export interface SummaryProps { + showMemberPrice: boolean + room: RoomsData +} diff --git a/types/components/hotelReservation/selectRate/sectionAccordion.ts b/types/components/hotelReservation/selectRate/sectionAccordion.ts index 46b194db3..c50207f3a 100644 --- a/types/components/hotelReservation/selectRate/sectionAccordion.ts +++ b/types/components/hotelReservation/selectRate/sectionAccordion.ts @@ -1,4 +1,4 @@ -import { StepEnum } from "../enterDetails/step" +import { StepEnum } from "@/types/enums/step" export interface SectionAccordionProps { header: string diff --git a/types/contexts/details.ts b/types/contexts/details.ts new file mode 100644 index 000000000..ea6b65edd --- /dev/null +++ b/types/contexts/details.ts @@ -0,0 +1,3 @@ +import { createDetailsStore } from "@/stores/details" + +export type DetailsStore = ReturnType<typeof createDetailsStore> diff --git a/types/contexts/steps.ts b/types/contexts/steps.ts new file mode 100644 index 000000000..40c3cb55e --- /dev/null +++ b/types/contexts/steps.ts @@ -0,0 +1,3 @@ +import { createStepsStore } from "@/stores/steps" + +export type StepsStore = ReturnType<typeof createStepsStore> diff --git a/types/enums/breakfast.ts b/types/enums/breakfast.ts index 81ff51a2e..723326c37 100644 --- a/types/enums/breakfast.ts +++ b/types/enums/breakfast.ts @@ -1,5 +1,4 @@ export enum BreakfastPackageEnum { FREE_MEMBER_BREAKFAST = "BRF0", REGULAR_BREAKFAST = "BRF1", - NO_BREAKFAST = "NO_BREAKFAST", } diff --git a/types/enums/step.ts b/types/enums/step.ts new file mode 100644 index 000000000..e52d3c856 --- /dev/null +++ b/types/enums/step.ts @@ -0,0 +1,6 @@ +export enum StepEnum { + selectBed = "select-bed", + breakfast = "breakfast", + details = "details", + payment = "payment", +} diff --git a/types/providers/details.ts b/types/providers/details.ts new file mode 100644 index 000000000..c58effb2c --- /dev/null +++ b/types/providers/details.ts @@ -0,0 +1,3 @@ +export interface DetailsProviderProps extends React.PropsWithChildren { + isMember: boolean +} diff --git a/types/providers/steps.ts b/types/providers/steps.ts new file mode 100644 index 000000000..8c24fdc8f --- /dev/null +++ b/types/providers/steps.ts @@ -0,0 +1,10 @@ +import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" +import { StepEnum } from "@/types/enums/step" +import type { BreakfastPackage } from "../components/hotelReservation/enterDetails/breakfast" + +export interface StepsProviderProps extends React.PropsWithChildren { + bedTypes: BedTypeSelection[] + breakfastPackages: BreakfastPackage[] | null + isMember: boolean + step: StepEnum +} diff --git a/types/stores/details.ts b/types/stores/details.ts new file mode 100644 index 000000000..ef6d101dc --- /dev/null +++ b/types/stores/details.ts @@ -0,0 +1,40 @@ +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" +import { StepEnum } from "@/types/enums/step" + +export interface DetailsState { + actions: { + setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void + setTotalPrice: (totalPrice: TotalPrice) => void + toggleSummaryOpen: () => void, + updateBedType: (data: BedTypeSchema) => void + updateBreakfast: (data: BreakfastPackage | false) => void + updateDetails: (data: DetailsSchema) => void + updateValidity: (property: StepEnum, isValid: boolean) => void + } + data: DetailsSchema & { + bedType: BedTypeSchema | undefined + breakfast: BreakfastPackage | false | undefined + booking: BookingData + } + isSubmittingDisabled: boolean + isSummaryOpen: boolean + isValid: Record<StepEnum, boolean> + totalPrice: TotalPrice +} + +export interface InitialState extends Partial<DetailsState> { + booking: BookingData +} + +interface Price { + currency: string + price: number +} + +export interface TotalPrice { + euro: Price | undefined + local: Price +} \ No newline at end of file diff --git a/types/stores/steps.ts b/types/stores/steps.ts new file mode 100644 index 000000000..bfdafdae7 --- /dev/null +++ b/types/stores/steps.ts @@ -0,0 +1,10 @@ +import { StepEnum } from "@/types/enums/step" + +export interface StepState { + completeStep: () => void + navigate: (step: StepEnum) => void + setStep: (step: StepEnum) => void + + currentStep: StepEnum + steps: StepEnum[] +} From e7c7485ff869d43654bd1d615b06490d62faaeba Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:41:02 +0100 Subject: [PATCH 020/101] feat(SW-589) Updated after comments --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index e2829a1d8..8cf0b5c21 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -59,13 +59,11 @@ export default function RoomCard({ ?.generalTerms } - function getBreakfastInformation(rate: RateDefinition | undefined) { - return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) - ?.breakfastIncluded - } + const getBreakfastMessage = (rate: RateDefinition | undefined) => { + const breakfastInfo = rateDefinitions.find( + (def) => def.rateCode === rate?.rateCode + )?.breakfastIncluded - const breakfastMessage = (rate: RateDefinition | undefined) => { - const breakfastInfo = getBreakfastInformation(rate) switch (breakfastInfo) { case true: return intl.formatMessage({ id: "Breakfast is included." }) @@ -187,7 +185,7 @@ export default function RoomCard({ </div> <div className={styles.container}> <Caption color="uiTextHighContrast" type="bold"> - {breakfastMessage(rates.flexRate)} + {getBreakfastMessage(rates.flexRate)} </Caption> {roomConfiguration.status === "NotAvailable" ? ( <div className={styles.noRoomsContainer}> From d86e11ac8589537b331a3f9e503551dd895fbdba Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:43:02 +0100 Subject: [PATCH 021/101] feat(SW-589) reverted change on hotel card for alert (this will be removed) --- components/HotelReservation/HotelCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 2873bfaeb..4c478d275 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -136,7 +136,7 @@ export default function HotelCard({ {hotelData.specialAlerts.length > 0 && ( <div className={styles.specialAlerts}> {hotelData.specialAlerts.map((alert) => ( - <Alert key={alert.id} type={alert.type} text={alert.heading} /> + <Alert key={alert.id} type={alert.type} text={alert.text} /> ))} </div> )} From d56d2f84728d1ef742b978094354f48fb520e3f5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:54:18 +0100 Subject: [PATCH 022/101] feat(SW-589) updated getRateDefinitionForRate --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 8cf0b5c21..a81e4169e 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -54,15 +54,12 @@ export default function RoomCard({ : undefined } - function getPriceInformationForRate(rate: RateDefinition | undefined) { + function getRateDefinitionForRate(rate: RateDefinition | undefined) { return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) - ?.generalTerms } const getBreakfastMessage = (rate: RateDefinition | undefined) => { - const breakfastInfo = rateDefinitions.find( - (def) => def.rateCode === rate?.rateCode - )?.breakfastIncluded + const breakfastInfo = getRateDefinitionForRate(rate)?.breakfastIncluded switch (breakfastInfo) { case true: @@ -207,7 +204,7 @@ export default function RoomCard({ value={key.toLowerCase()} paymentTerm={key === "flexRate" ? payLater : payNow} product={findProductForRate(rate)} - priceInformation={getPriceInformationForRate(rate)} + priceInformation={getRateDefinitionForRate(rate)?.generalTerms} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} From 62442646f07f7c672f432d5dea3194d29a0f74d6 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:55:17 +0100 Subject: [PATCH 023/101] feat(SW-589): updated breakfastIncluded --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index a81e4169e..d69391122 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -59,9 +59,9 @@ export default function RoomCard({ } const getBreakfastMessage = (rate: RateDefinition | undefined) => { - const breakfastInfo = getRateDefinitionForRate(rate)?.breakfastIncluded + const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded - switch (breakfastInfo) { + switch (breakfastIncluded) { case true: return intl.formatMessage({ id: "Breakfast is included." }) case false: From beb776bac9f22fbfe41f6904bd345594cfe8c1d8 Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Mon, 18 Nov 2024 17:30:55 +0000 Subject: [PATCH 024/101] Merged in fix/remove-filter-to-show-all-hotels (pull request #925) Fix/remove filter to show all hotels * fix: remove filter to show all hotels on select-hotel-page * fix: add missing translations Approved-by: Pontus Dreij --- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + server/routers/hotels/output.ts | 2 +- server/routers/hotels/query.ts | 9 +++------ server/routers/hotels/schemas/room.ts | 4 ++-- 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 076544355..336f5eef9 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -356,6 +356,7 @@ "This room is not available": "Dette værelse er ikke tilgængeligt", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "For at få medlemsprisen <span>{amount} {currency}</span>, log ind eller tilmeld dig, når du udfylder bookingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", + "Total": "Total", "Total Points": "Samlet antal point", "Total price": "Samlet pris", "Total price (incl VAT)": "Samlet pris (inkl. moms)", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 433a0ba33..78d9fd5cb 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -354,6 +354,7 @@ "This room is not available": "Dieses Zimmer ist nicht verfügbar", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "Um den Mitgliederpreis von <span>{amount} {currency}</span> zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", + "Total": "Gesamt", "Total Points": "Gesamtpunktzahl", "Total price": "Gesamtpreis", "Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 38a5da82b..cb8a071d9 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -384,6 +384,7 @@ "This room is not available": "This room is not available", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + "Total": "Total", "Total Points": "Total Points", "Total cost": "Total cost", "Total price": "Total price", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 218abed13..48931f7f2 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -356,6 +356,7 @@ "This room is not available": "Tämä huone ei ole käytettävissä", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", + "Total": "Kokonais", "Total Points": "Kokonaispisteet", "Total price": "Kokonaishinta", "Total price (incl VAT)": "Kokonaishinta (sis. ALV)", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 7cf8e985a..313d0799d 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -353,6 +353,7 @@ "This room is not available": "Dette rommet er ikke tilgjengelig", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "For å få medlemsprisen <span>{amount} {currency}</span>, logg inn eller bli med når du fullfører bestillingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", + "Total": "Total", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", "Total price": "Totalpris", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 7f935f88a..878bce352 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -353,6 +353,7 @@ "This room is not available": "Detta rum är inte tillgängligt", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "För att få medlemsprisen <span>{amount} {currency}</span>, logga in eller bli medlem när du slutför bokningen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", + "Total": "Totalt", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", "Total price": "Totalpris", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index b68e87fc0..d4f3d89c9 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -105,7 +105,7 @@ const hotelContentSchema = z.object({ imageSizes: imageSizesSchema, }), texts: z.object({ - facilityInformation: z.string(), + facilityInformation: z.string().optional(), surroundingInformation: z.string(), descriptions: z.object({ short: z.string(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index ea094de0f..af9460b7c 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -468,12 +468,9 @@ export const hotelQueryRouter = router({ }) ) return { - availability: validateAvailabilityData.data.data - .filter( - (hotels) => - hotels.attributes.status === AvailabilityEnum.Available - ) - .flatMap((hotels) => hotels.attributes), + availability: validateAvailabilityData.data.data.flatMap( + (hotels) => hotels.attributes + ), } }), rooms: serviceProcedure diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index 5a1480097..8b9291c1f 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -11,8 +11,8 @@ const roomContentSchema = z.object({ ), texts: z.object({ descriptions: z.object({ - short: z.string(), - medium: z.string(), + short: z.string().optional(), + medium: z.string().optional(), }), }), }) From d67affd6778577e03c33497abf594c8fcd9ff32c Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 18 Nov 2024 10:54:35 +0100 Subject: [PATCH 025/101] fix: update booking service schemas --- .../(standard)/[step]/page.tsx | 8 ++--- .../BookingConfirmation/Details/index.tsx | 2 +- .../EnterDetails/Payment/index.tsx | 6 ++++ server/routers/booking/input.ts | 36 ++++++++++++++----- server/routers/booking/output.ts | 24 +++++++++++-- .../hotelReservation/selectRate/section.ts | 2 +- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 70aef0ada..ae04c61a6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -107,10 +107,10 @@ export default async function StepPage({ id: "Select payment method", }) - const roomPrice = - user && roomAvailability.memberRate - ? roomAvailability.memberRate?.localPrice.pricePerStay - : roomAvailability.publicRate!.localPrice.pricePerStay + const roomPrice = { + memberPrice: roomAvailability.memberRate?.localPrice.pricePerStay, + publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay, + } return ( <section> diff --git a/components/HotelReservation/BookingConfirmation/Details/index.tsx b/components/HotelReservation/BookingConfirmation/Details/index.tsx index 956ad8e45..5d23e55a8 100644 --- a/components/HotelReservation/BookingConfirmation/Details/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Details/index.tsx @@ -49,7 +49,7 @@ export default async function Details({ </li> <li className={styles.listItem}> <Body>{intl.formatMessage({ id: "Cancellation policy" })}</Body> - <Body>N/A</Body> + <Body>{booking.rateDefinition.cancellationText}</Body> </li> <li className={styles.listItem}> <Body>{intl.formatMessage({ id: "Rebooking" })}</Body> diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 303fc2d5c..84caf0967 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -77,6 +77,9 @@ export default function Payment({ breakfast, bedType, membershipNo, + join, + dateOfBirth, + zipCode, } = userData const { toDate, fromDate, rooms: rooms, hotel } = roomData @@ -181,6 +184,9 @@ export default function Payment({ phoneNumber, countryCode, membershipNumber: membershipNo, + becomeMember: join, + dateOfBirth, + postalCode: zipCode, }, packages: { breakfast: breakfast !== BreakfastPackageEnum.NO_BREAKFAST, diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts index 4c7d802ef..f838d201f 100644 --- a/server/routers/booking/input.ts +++ b/server/routers/booking/input.ts @@ -2,6 +2,15 @@ import { z } from "zod" import { ChildBedTypeEnum } from "@/constants/booking" +const signupSchema = z.discriminatedUnion("becomeMember", [ + z.object({ + dateOfBirth: z.string(), + postalCode: z.string(), + becomeMember: z.literal<boolean>(true), + }), + z.object({ becomeMember: z.literal<boolean>(false) }), +]) + const roomsSchema = z.array( z.object({ adults: z.number().int().nonnegative(), @@ -15,14 +24,17 @@ const roomsSchema = z.array( .default([]), rateCode: z.string(), roomTypeCode: z.coerce.string(), - guest: z.object({ - firstName: z.string(), - lastName: z.string(), - email: z.string().email(), - phoneNumber: z.string(), - countryCode: z.string(), - membershipNumber: z.string().optional(), - }), + guest: z.intersection( + z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + phoneNumber: z.string(), + countryCode: z.string(), + membershipNumber: z.string().optional(), + }), + signupSchema + ), smsConfirmationRequested: z.boolean(), packages: z.object({ breakfast: z.boolean(), @@ -30,7 +42,13 @@ const roomsSchema = z.array( petFriendly: z.boolean(), accessibility: z.boolean(), }), - roomPrice: z.number().or(z.string().transform((val) => Number(val))), + roomPrice: z.object({ + publicPrice: z.number().or(z.string().transform((val) => Number(val))), + memberPrice: z + .number() + .or(z.string().transform((val) => Number(val))) + .optional(), + }), }) ) diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 5fd34ac00..83a185e1e 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -15,7 +15,18 @@ export const createBookingSchema = z cancellationNumber: z.string().nullable(), reservationStatus: z.string(), paymentUrl: z.string().nullable(), - metadata: z.any(), // TODO: define metadata schema (not sure what it does) + metadata: z + .object({ + errorCode: z.number().optional(), + errorMessage: z.string().optional(), + priceChangedMetadata: z + .object({ + roomPrice: z.number().optional(), + totalPrice: z.number().optional(), + }) + .optional(), + }) + .nullable(), }), type: z.string(), id: z.string(), @@ -77,7 +88,16 @@ export const bookingConfirmationSchema = z guest: guestSchema, hotelId: z.string(), packages: z.array(packageSchema), - rateCode: z.string(), + rateDefinition: z.object({ + rateCode: z.string(), + title: z.string().nullable(), + breakfastIncluded: z.boolean(), + isMemberRate: z.boolean(), + generalTerms: z.array(z.string()).optional(), + cancellationRule: z.string().optional(), + cancellationText: z.string().optional(), + mustBeGuaranteed: z.boolean(), + }), reservationStatus: z.string(), roomPrice: z.number().int(), roomTypeCode: z.string(), diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 578819fb1..05d86ff6c 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -28,7 +28,7 @@ export interface BreakfastSelectionProps extends SectionProps { export interface DetailsProps extends SectionProps {} export interface PaymentProps { - roomPrice: number + roomPrice: { publicPrice: number; memberPrice: number | undefined } otherPaymentOptions: string[] savedCreditCards: CreditCard[] | null mustBeGuaranteed: boolean From 5206a40754756cfa16415b7dee9c2d4d6e6bfb96 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson <fredrik.thorsson@scandichotels.com> Date: Wed, 13 Nov 2024 16:49:36 +0100 Subject: [PATCH 026/101] feat(SW-880): add wellness and exercise sidepeek --- .../SidePeeks/WellnessAndExercise/index.tsx | 71 +++++++++++++++++++ .../wellnessAndExercise.module.css | 41 +++++++++++ components/ContentType/HotelPage/index.tsx | 10 +-- i18n/dictionaries/da.json | 5 ++ i18n/dictionaries/de.json | 5 ++ i18n/dictionaries/en.json | 5 ++ i18n/dictionaries/fi.json | 5 ++ i18n/dictionaries/no.json | 5 ++ i18n/dictionaries/sv.json | 5 ++ server/routers/hotels/query.ts | 1 + .../hotelPage/sidepeek/wellnessAndExercise.ts | 5 ++ 11 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css create mode 100644 types/components/hotelPage/sidepeek/wellnessAndExercise.ts diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx new file mode 100644 index 000000000..642664cee --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -0,0 +1,71 @@ +import { wellnessAndExercise } from "@/constants/routes/hotelPageParams" + +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + +import styles from "./wellnessAndExercise.module.css" + +import { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" + +export default async function WellnessAndExerciseSidePeek({ + healthFacilities, +}: WellnessAndExerciseSidePeekProps) { + const intl = await getIntl() + const lang = getLang() + + return ( + <SidePeek + contentKey={wellnessAndExercise[lang]} + title={intl.formatMessage({ id: "Wellness & Exercise" })} + > + <div className={styles.wrapper}> + {healthFacilities.map((facility) => ( + <div className={styles.content} key={facility.type}> + <Image + src={facility.content.images[0]?.imageSizes.large} + alt={facility.content.images[0]?.metaData.altText} + className={styles.image} + height={400} + width={200} + /> + <div className={styles.information}> + <Subtitle color="burgundy" asChild> + <Title level="h3">{facility.type} + +
    + + {intl.formatMessage({ id: " Opening Hours" })} + + + {facility.openingDetails.openingHours.ordinary.alwaysOpen + ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Mon-Fri" })} ${facility.openingDetails.openingHours.ordinary.openingTime}-${facility.openingDetails.openingHours.ordinary.closingTime}`} + + + {facility.openingDetails.openingHours.weekends.alwaysOpen + ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} + + Placeholder text +
    +
    +
    + ))} +
    +
    + +
    + + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css new file mode 100644 index 000000000..0c33da7cd --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css @@ -0,0 +1,41 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); + margin-bottom: calc( + var(--Spacing-x4) * 2 + 80px + ); /* Creates space between the wrapper and buttonContainer */ +} + +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.image { + width: 100%; + height: 270px; + object-fit: cover; + border-radius: var(--Corner-radius-Medium); +} + +.information { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.body { + margin-top: var(--Spacing-x1); +} + +.buttonContainer { + background-color: var(--Base-Background-Primary-Normal); + border-top: 1px solid var(--Base-Border-Subtle); + padding: var(--Spacing-x4) var(--Spacing-x2); + width: 100%; + position: absolute; + left: 0; + bottom: 0; +} diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 97969867b..eb9d1dd37 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -16,6 +16,7 @@ import MapCard from "./Map/MapCard" import MapWithCardWrapper from "./Map/MapWithCard" import MobileMapToggle from "./Map/MobileMapToggle" import StaticMap from "./Map/StaticMap" +import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise" import AmenitiesList from "./AmenitiesList" import Facilities from "./Facilities" import IntroSection from "./IntroSection" @@ -52,6 +53,7 @@ export default async function HotelPage() { facilities, faq, alerts, + healthFacilities, } = hotelData const topThreePois = pointsOfInterest.slice(0, 3) @@ -145,13 +147,7 @@ export default async function HotelPage() { {/* TODO */} Restaurant & Bar - - {/* TODO */} - Wellness & Exercise - + Date: Thu, 14 Nov 2024 11:35:02 +0100 Subject: [PATCH 027/101] feat(SW-880): add function for type mapping --- .../HotelPage/SidePeeks/Utils/getType.ts | 17 +++++++++++++++++ .../SidePeeks/WellnessAndExercise/index.tsx | 12 +++++++----- i18n/dictionaries/da.json | 4 ++++ i18n/dictionaries/de.json | 4 ++++ i18n/dictionaries/en.json | 4 ++++ i18n/dictionaries/fi.json | 4 ++++ i18n/dictionaries/no.json | 4 ++++ i18n/dictionaries/sv.json | 4 ++++ 8 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/Utils/getType.ts diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts new file mode 100644 index 000000000..33e39b220 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -0,0 +1,17 @@ +import { getIntl } from "@/i18n" + +export async function getType(type: string) { + const intl = await getIntl() + switch (type) { + case "OutdoorPool": + return intl.formatMessage({ id: "Outdoor pool" }) + case "Sauna": + return intl.formatMessage({ id: "Sauna" }) + case "Relax": + return intl.formatMessage({ id: "Relax" }) + case "Gym": + return intl.formatMessage({ id: "Gym" }) + default: + return type + } +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index 642664cee..ee9f01b85 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -10,6 +10,8 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import { getType } from "../Utils/getType" + import styles from "./wellnessAndExercise.module.css" import { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" @@ -36,19 +38,19 @@ export default async function WellnessAndExerciseSidePeek({ width={200} />
    - - {facility.type} + + {getType(facility.type)}
    - + {intl.formatMessage({ id: " Opening Hours" })} - + {facility.openingDetails.openingHours.ordinary.alwaysOpen ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Mon-Fri" })} ${facility.openingDetails.openingHours.ordinary.openingTime}-${facility.openingDetails.openingHours.ordinary.closingTime}`} - + {facility.openingDetails.openingHours.weekends.alwaysOpen ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 44e24d47d..232348983 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -143,6 +143,7 @@ "Guarantee booking with credit card": "Garantere booking med kreditkort", "Guest information": "Gæsteinformation", "Guests & Rooms": "Gæster & værelser", + "Gym": "Fitnesscenter", "Hi": "Hei", "Highest level": "Højeste niveau", "Hospital": "Hospital", @@ -238,6 +239,7 @@ "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", "Opening Hours": "Åbningstider", + "Outdoor pool": "Udendørs pool", "Overview": "Oversigt", "PETR": "Kæledyr", "Parking": "Parkering", @@ -278,6 +280,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Læs mere om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Slap af", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Request bedtype": "Anmod om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -290,6 +293,7 @@ "Rooms": "Værelser", "Rooms & Guests": "Værelser & gæster", "Sat-Sun": "Lør-Søn", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Gemme", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index a8e785126..d025df242 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -143,6 +143,7 @@ "Guarantee booking with credit card": "Buchung mit Kreditkarte garantieren", "Guest information": "Informationen für Gäste", "Guests & Rooms": "Gäste & Zimmer", + "Gym": "Fitnessstudio", "Hi": "Hallo", "Highest level": "Höchstes Level", "Hospital": "Krankenhaus", @@ -236,6 +237,7 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Opening Hours": "Öffnungszeiten", + "Outdoor pool": "Außenpool", "Overview": "Übersicht", "PETR": "Haustier", "Parking": "Parken", @@ -276,6 +278,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lesen Sie mehr über das Hotel", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Entspannen", "Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen", "Request bedtype": "Bettentyp anfragen", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -289,6 +292,7 @@ "Rooms": "Räume", "Rooms & Guests": "Zimmer & Gäste", "Sat-Sun": "Sa-So", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Speichern", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index ba17eaf15..c7d7336a5 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -155,6 +155,7 @@ "Guest": "Guest", "Guest information": "Guest information", "Guests & Rooms": "Guests & Rooms", + "Gym": "Gym", "Hi": "Hi", "Highest level": "Highest level", "Hospital": "Hospital", @@ -255,6 +256,7 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Opening Hours": "Opening Hours", + "Outdoor pool": "Outdoor pool", "Overview": "Overview", "PETR": "Pet", "Parking": "Parking", @@ -304,6 +306,7 @@ "Read more about wellness & exercise": "Read more about wellness & exercise", "Rebooking": "Rebooking", "Reference #{bookingNr}": "Reference #{bookingNr}", + "Relax": "Relax", "Remove card from member profile": "Remove card from member profile", "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -317,6 +320,7 @@ "Rooms": "Rooms", "Rooms & Guests": "Rooms & Guests", "Sat-Sun": "Sat-Sun", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Save", "Save card to profile": "Save card to profile", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 2210591e5..add731307 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -143,6 +143,7 @@ "Guarantee booking with credit card": "Varmista varaus luottokortilla", "Guest information": "Vieraan tiedot", "Guests & Rooms": "Vieraat & Huoneet", + "Gym": "Kuntosali", "Hi": "Hi", "Highest level": "Korkein taso", "Hospital": "Sairaala", @@ -238,6 +239,7 @@ "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", "Opening Hours": "Aukioloajat", + "Outdoor pool": "Ulkouima-allas", "Overview": "Yleiskatsaus", "PETR": "Lemmikki", "Parking": "Pysäköinti", @@ -278,6 +280,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Lue lisää hotellista", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Rentoutua", "Remove card from member profile": "Poista kortti jäsenprofiilista", "Request bedtype": "Pyydä sänkytyyppiä", "Restaurant": "{count, plural, one {#Ravintola} other {#Restaurants}}", @@ -291,6 +294,7 @@ "Rooms & Guests": "Huoneet & Vieraat", "Rooms & Guestss": "Huoneet & Vieraat", "Sat-Sun": "La-Su", + "Sauna": "Sauna", "Sauna and gym": "Sauna and gym", "Save": "Tallenna", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 5482b868a..4c6415e42 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -142,6 +142,7 @@ "Guarantee booking with credit card": "Garantere booking med kredittkort", "Guest information": "Informasjon til gjester", "Guests & Rooms": "Gjester & rom", + "Gym": "Treningsstudio", "Hi": "Hei", "Highest level": "Høyeste nivå", "Hospital": "Sykehus", @@ -236,6 +237,7 @@ "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", "Opening Hours": "Åpningstider", + "Outdoor pool": "Utendørs basseng", "Overview": "Oversikt", "PETR": "Kjæledyr", "Parking": "Parkering", @@ -276,6 +278,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Les mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Slappe av", "Remove card from member profile": "Fjern kortet fra medlemsprofilen", "Request bedtype": "Be om sengetype", "Restaurant": "{count, plural, one {#Restaurant} other {#Restaurants}}", @@ -288,6 +291,7 @@ "Rooms": "Rom", "Rooms & Guests": "Rom og gjester", "Sat-Sun": "Lør-Søn", + "Sauna": "Badstue", "Sauna and gym": "Sauna and gym", "Save": "Lagre", "Scandic Friends Mastercard": "Scandic Friends Mastercard", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 612e84812..47c8096ca 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -142,6 +142,7 @@ "Guarantee booking with credit card": "Garantera bokning med kreditkort", "Guest information": "Information till gästerna", "Guests & Rooms": "Gäster & rum", + "Gym": "Gym", "Hi": "Hej", "Highest level": "Högsta nivå", "Hospital": "Sjukhus", @@ -236,6 +237,7 @@ "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", "Opening Hours": "Öppettider", + "Outdoor pool": "Utomhuspool", "Overview": "Översikt", "PETR": "Husdjur", "Parking": "Parkering", @@ -276,6 +278,7 @@ "Read more & book a table": "Read more & book a table", "Read more about the hotel": "Läs mer om hotellet", "Read more about wellness & exercise": "Read more about wellness & exercise", + "Relax": "Koppla av", "Remove card from member profile": "Ta bort kortet från medlemsprofilen", "Request bedtype": "Request bedtype", "Restaurant": "{count, plural, one {#Restaurang} other {#Restauranger}}", @@ -288,6 +291,7 @@ "Rooms": "Rum", "Rooms & Guests": "Rum och gäster", "Sat-Sun": "Lör-Sön", + "Sauna": "Bastu", "Sauna and gym": "Sauna and gym", "Save": "Spara", "Scandic Friends Mastercard": "Scandic Friends Mastercard", From c7d285de74cca5bbb2cb1ef77f83d836fc6e2c86 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 14 Nov 2024 14:10:51 +0100 Subject: [PATCH 028/101] feat(SW-880): refactor getType --- .../HotelPage/SidePeeks/Utils/getType.ts | 15 +++++++++++---- .../SidePeeks/WellnessAndExercise/index.tsx | 4 +++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts index 33e39b220..47cbf00da 100644 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -2,15 +2,22 @@ import { getIntl } from "@/i18n" export async function getType(type: string) { const intl = await getIntl() + + /* TODO: Get full list of types */ + const outdoorPool = intl.formatMessage({ id: "Outdoor pool" }) + const sauna = intl.formatMessage({ id: "Sauna" }) + const relax = intl.formatMessage({ id: "Relax" }) + const gym = intl.formatMessage({ id: "Gym" }) + switch (type) { case "OutdoorPool": - return intl.formatMessage({ id: "Outdoor pool" }) + return outdoorPool case "Sauna": - return intl.formatMessage({ id: "Sauna" }) + return sauna case "Relax": - return intl.formatMessage({ id: "Relax" }) + return relax case "Gym": - return intl.formatMessage({ id: "Gym" }) + return gym default: return type } diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index ee9f01b85..6bd73d703 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -55,7 +55,9 @@ export default async function WellnessAndExerciseSidePeek({ ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} - Placeholder text + + {/* TODO: Determine what details should be displayed about the facility type */} +
    From edc65af74ead22a9fc26224e015aec1be9fc1cf4 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 14 Nov 2024 14:21:01 +0100 Subject: [PATCH 029/101] feat(SW-880): add import type --- components/ContentType/HotelPage/SidePeeks/Utils/getType.ts | 2 +- .../HotelPage/SidePeeks/WellnessAndExercise/index.tsx | 6 +++--- types/components/hotelPage/sidepeek/wellnessAndExercise.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts index 47cbf00da..9cab458c7 100644 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -1,6 +1,6 @@ import { getIntl } from "@/i18n" -export async function getType(type: string) { +export async function getFacilityType(type: string) { const intl = await getIntl() /* TODO: Get full list of types */ diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index 6bd73d703..dee9d73e1 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -10,11 +10,11 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { getType } from "../Utils/getType" +import { getFacilityType } from "../Utils/getType" import styles from "./wellnessAndExercise.module.css" -import { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" +import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" export default async function WellnessAndExerciseSidePeek({ healthFacilities, @@ -39,7 +39,7 @@ export default async function WellnessAndExerciseSidePeek({ />
    - {getType(facility.type)} + {getFacilityType(facility.type)}
    diff --git a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts index f64ccd003..a75499f2d 100644 --- a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts +++ b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts @@ -1,4 +1,4 @@ -import { Hotel } from "@/types/hotel" +import type { Hotel } from "@/types/hotel" export type WellnessAndExerciseSidePeekProps = { healthFacilities: Hotel["healthFacilities"] From c1152109c38f9b593660d3fbf27eb0a1ca119f74 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Thu, 14 Nov 2024 22:38:53 +0100 Subject: [PATCH 030/101] feat(SW-880): add translations --- .../ContentType/HotelPage/SidePeeks/Utils/getType.ts | 7 ++++++- i18n/dictionaries/da.json | 2 ++ i18n/dictionaries/de.json | 2 ++ i18n/dictionaries/en.json | 2 ++ i18n/dictionaries/fi.json | 2 ++ i18n/dictionaries/no.json | 2 ++ i18n/dictionaries/sv.json | 2 ++ 7 files changed, 18 insertions(+), 1 deletion(-) diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts index 9cab458c7..1a6f87a63 100644 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts @@ -3,21 +3,26 @@ import { getIntl } from "@/i18n" export async function getFacilityType(type: string) { const intl = await getIntl() - /* TODO: Get full list of types */ const outdoorPool = intl.formatMessage({ id: "Outdoor pool" }) + const indoorPool = intl.formatMessage({ id: "Indoor pool" }) const sauna = intl.formatMessage({ id: "Sauna" }) const relax = intl.formatMessage({ id: "Relax" }) const gym = intl.formatMessage({ id: "Gym" }) + const jacuzzi = intl.formatMessage({ id: "Jacuzzi" }) switch (type) { case "OutdoorPool": return outdoorPool + case "IndoorPool": + return indoorPool case "Sauna": return sauna case "Relax": return relax case "Gym": return gym + case "Jacuzzi": + return jacuzzi default: return type } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 232348983..14c46b7be 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -160,7 +160,9 @@ "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", "Included": "Inkluderet", + "Indoor pool": "Indendørs pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", + "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Tilmeld dig Scandic Friends", "Join at no cost": "Tilmeld dig uden omkostninger", "Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index d025df242..403f0db22 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -160,7 +160,9 @@ "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", "Included": "Iinklusive", + "Indoor pool": "Innenpool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", + "Jacuzzi": "Whirlpool", "Join Scandic Friends": "Treten Sie Scandic Friends bei", "Join at no cost": "Kostenlos beitreten", "Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index c7d7336a5..2bb99e5f2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -172,7 +172,9 @@ "In crib": "In crib", "In extra bed": "In extra bed", "Included": "Included", + "Indoor pool": "Indoor pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", + "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Join Scandic Friends", "Join at no cost": "Join at no cost", "Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index add731307..bfaa69ec6 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -160,7 +160,9 @@ "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", "Included": "Sisälly hintaan", + "Indoor pool": "Sisäuima-allas", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", + "Jacuzzi": "Poreallas", "Join Scandic Friends": "Liity jäseneksi", "Join at no cost": "Liity maksutta", "Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 4c6415e42..b593f1c83 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -158,7 +158,9 @@ "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", "Included": "Inkludert", + "Indoor pool": "Innendørs basseng", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", + "Jacuzzi": "Boblebad", "Join Scandic Friends": "Bli med i Scandic Friends", "Join at no cost": "Bli med uten kostnad", "Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 47c8096ca..05c775020 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -158,7 +158,9 @@ "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", "Included": "Inkluderad", + "Indoor pool": "Inomhuspool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", + "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Gå med i Scandic Friends", "Join at no cost": "Gå med utan kostnad", "Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.", From ce6914a7e4f1154205aeb2d81278d9f8b3394e31 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Fri, 15 Nov 2024 14:21:52 +0100 Subject: [PATCH 031/101] feat(SW-880): update facility type rendering --- .../HotelPage/SidePeeks/Utils/getType.ts | 29 ------------- .../SidePeeks/WellnessAndExercise/index.tsx | 42 ++++++++++--------- i18n/dictionaries/da.json | 4 +- i18n/dictionaries/de.json | 4 +- i18n/dictionaries/en.json | 4 +- i18n/dictionaries/fi.json | 4 +- i18n/dictionaries/no.json | 4 +- i18n/dictionaries/sv.json | 4 +- .../hotelPage/sidepeek/wellnessAndExercise.ts | 1 + 9 files changed, 35 insertions(+), 61 deletions(-) delete mode 100644 components/ContentType/HotelPage/SidePeeks/Utils/getType.ts diff --git a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts b/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts deleted file mode 100644 index 1a6f87a63..000000000 --- a/components/ContentType/HotelPage/SidePeeks/Utils/getType.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getIntl } from "@/i18n" - -export async function getFacilityType(type: string) { - const intl = await getIntl() - - const outdoorPool = intl.formatMessage({ id: "Outdoor pool" }) - const indoorPool = intl.formatMessage({ id: "Indoor pool" }) - const sauna = intl.formatMessage({ id: "Sauna" }) - const relax = intl.formatMessage({ id: "Relax" }) - const gym = intl.formatMessage({ id: "Gym" }) - const jacuzzi = intl.formatMessage({ id: "Jacuzzi" }) - - switch (type) { - case "OutdoorPool": - return outdoorPool - case "IndoorPool": - return indoorPool - case "Sauna": - return sauna - case "Relax": - return relax - case "Gym": - return gym - case "Jacuzzi": - return jacuzzi - default: - return type - } -} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index dee9d73e1..5314e3f18 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -10,14 +10,13 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { getFacilityType } from "../Utils/getType" - import styles from "./wellnessAndExercise.module.css" import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" export default async function WellnessAndExerciseSidePeek({ healthFacilities, + buttonUrl, }: WellnessAndExerciseSidePeekProps) { const intl = await getIntl() const lang = getLang() @@ -30,16 +29,20 @@ export default async function WellnessAndExerciseSidePeek({
    {healthFacilities.map((facility) => (
    - {facility.content.images[0]?.metaData.altText} + {facility.content.images[0]?.imageSizes.medium && ( + {facility.content.images[0].metaData.altText + )}
    - {getFacilityType(facility.type)} + + {intl.formatMessage({ id: `${facility.type}` })} +
    @@ -55,21 +58,20 @@ export default async function WellnessAndExerciseSidePeek({ ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} - - {/* TODO: Determine what details should be displayed about the facility type */} -
    ))}
    -
    - -
    + {buttonUrl && ( +
    + +
    + )} ) } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 14c46b7be..c453589ea 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -160,7 +160,7 @@ "In crib": "i tremmeseng", "In extra bed": "i ekstra seng", "Included": "Inkluderet", - "Indoor pool": "Indendørs pool", + "IndoorPool": "Indendørs pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.", "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Tilmeld dig Scandic Friends", @@ -241,7 +241,7 @@ "Open menu": "Åbn menuen", "Open my pages menu": "Åbn mine sider menuen", "Opening Hours": "Åbningstider", - "Outdoor pool": "Udendørs pool", + "OutdoorPool": "Udendørs pool", "Overview": "Oversigt", "PETR": "Kæledyr", "Parking": "Parkering", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 403f0db22..04d750494 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -160,7 +160,7 @@ "In crib": "im Kinderbett", "In extra bed": "im zusätzlichen Bett", "Included": "Iinklusive", - "Indoor pool": "Innenpool", + "IndoorPool": "Innenpool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.", "Jacuzzi": "Whirlpool", "Join Scandic Friends": "Treten Sie Scandic Friends bei", @@ -239,7 +239,7 @@ "Open menu": "Menü öffnen", "Open my pages menu": "Meine Seiten Menü öffnen", "Opening Hours": "Öffnungszeiten", - "Outdoor pool": "Außenpool", + "OutdoorPool": "Außenpool", "Overview": "Übersicht", "PETR": "Haustier", "Parking": "Parken", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2bb99e5f2..09cf02ec9 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -172,7 +172,7 @@ "In crib": "In crib", "In extra bed": "In extra bed", "Included": "Included", - "Indoor pool": "Indoor pool", + "IndoorPool": "Indoor pool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.", "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Join Scandic Friends", @@ -258,7 +258,7 @@ "Open menu": "Open menu", "Open my pages menu": "Open my pages menu", "Opening Hours": "Opening Hours", - "Outdoor pool": "Outdoor pool", + "OutdoorPool": "Outdoor pool", "Overview": "Overview", "PETR": "Pet", "Parking": "Parking", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index bfaa69ec6..64903ea74 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -160,7 +160,7 @@ "In crib": "Pinnasängyssä", "In extra bed": "Oma vuodepaikka", "Included": "Sisälly hintaan", - "Indoor pool": "Sisäuima-allas", + "IndoorPool": "Sisäuima-allas", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.", "Jacuzzi": "Poreallas", "Join Scandic Friends": "Liity jäseneksi", @@ -241,7 +241,7 @@ "Open menu": "Avaa valikko", "Open my pages menu": "Avaa omat sivut -valikko", "Opening Hours": "Aukioloajat", - "Outdoor pool": "Ulkouima-allas", + "OutdoorPool": "Ulkouima-allas", "Overview": "Yleiskatsaus", "PETR": "Lemmikki", "Parking": "Pysäköinti", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index b593f1c83..ef15b0823 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -158,7 +158,7 @@ "In crib": "i sprinkelseng", "In extra bed": "i ekstraseng", "Included": "Inkludert", - "Indoor pool": "Innendørs basseng", + "IndoorPool": "Innendørs basseng", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.", "Jacuzzi": "Boblebad", "Join Scandic Friends": "Bli med i Scandic Friends", @@ -239,7 +239,7 @@ "Open menu": "Åpne menyen", "Open my pages menu": "Åpne mine sider menyen", "Opening Hours": "Åpningstider", - "Outdoor pool": "Utendørs basseng", + "OutdoorPool": "Utendørs basseng", "Overview": "Oversikt", "PETR": "Kjæledyr", "Parking": "Parkering", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 05c775020..8be596fc1 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -158,7 +158,7 @@ "In crib": "I spjälsäng", "In extra bed": "Egen sängplats", "Included": "Inkluderad", - "Indoor pool": "Inomhuspool", + "IndoorPool": "Inomhuspool", "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.", "Jacuzzi": "Jacuzzi", "Join Scandic Friends": "Gå med i Scandic Friends", @@ -239,7 +239,7 @@ "Open menu": "Öppna menyn", "Open my pages menu": "Öppna mina sidor menyn", "Opening Hours": "Öppettider", - "Outdoor pool": "Utomhuspool", + "OutdoorPool": "Utomhuspool", "Overview": "Översikt", "PETR": "Husdjur", "Parking": "Parkering", diff --git a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts index a75499f2d..828f3ee8b 100644 --- a/types/components/hotelPage/sidepeek/wellnessAndExercise.ts +++ b/types/components/hotelPage/sidepeek/wellnessAndExercise.ts @@ -2,4 +2,5 @@ import type { Hotel } from "@/types/hotel" export type WellnessAndExerciseSidePeekProps = { healthFacilities: Hotel["healthFacilities"] + buttonUrl?: string } From 3f73699e73a3ad8af3340b277ed34f631710d6b3 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 18 Nov 2024 10:24:45 +0100 Subject: [PATCH 032/101] feat(SW-880): create facility card component --- .../FacilityCard/facilityCard.module.css | 22 ++++++++ .../FacilityCard/index.tsx | 54 +++++++++++++++++++ .../SidePeeks/WellnessAndExercise/index.tsx | 47 ++++------------ .../wellnessAndExercise.module.css | 23 -------- components/ContentType/HotelPage/index.tsx | 5 +- .../hotelPage/sidepeek/facilityCard.ts | 19 +++++++ 6 files changed, 109 insertions(+), 61 deletions(-) create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx create mode 100644 types/components/hotelPage/sidepeek/facilityCard.ts diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css new file mode 100644 index 000000000..5abac9f32 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css @@ -0,0 +1,22 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.image { + width: 100%; + height: 270px; + object-fit: cover; + border-radius: var(--Corner-radius-Medium); +} + +.information { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.body { + margin-top: var(--Spacing-x1); +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx new file mode 100644 index 000000000..ba3925baf --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx @@ -0,0 +1,54 @@ +import Image from "@/components/Image" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./facilityCard.module.css" + +import type { FacilityCardProps } from "@/types/components/hotelPage/sidepeek/facilityCard" + +export default async function FacilityCard({ + imgUrl, + imgAltText, + facilityType, + ordinaryOpeningTimes, + weekendOpeningTimes, +}: FacilityCardProps) { + const intl = await getIntl() + return ( +
    + {imgUrl && ( + {imgAltText + )} +
    + + + {intl.formatMessage({ id: `${facilityType}` })} + + +
    + + {intl.formatMessage({ id: " Opening Hours" })} + + + {ordinaryOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`} + + + {weekendOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`} + +
    +
    +
    + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index 5314e3f18..d7f381d9b 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -1,15 +1,13 @@ import { wellnessAndExercise } from "@/constants/routes/hotelPageParams" -import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" import SidePeek from "@/components/TempDesignSystem/SidePeek" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" +import FacilityCard from "./FacilityCard" + import styles from "./wellnessAndExercise.module.css" import type { WellnessAndExerciseSidePeekProps } from "@/types/components/hotelPage/sidepeek/wellnessAndExercise" @@ -28,39 +26,14 @@ export default async function WellnessAndExerciseSidePeek({ >
    {healthFacilities.map((facility) => ( -
    - {facility.content.images[0]?.imageSizes.medium && ( - {facility.content.images[0].metaData.altText - )} -
    - - - {intl.formatMessage({ id: `${facility.type}` })} - - -
    - - {intl.formatMessage({ id: " Opening Hours" })} - - - {facility.openingDetails.openingHours.ordinary.alwaysOpen - ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Mon-Fri" })} ${facility.openingDetails.openingHours.ordinary.openingTime}-${facility.openingDetails.openingHours.ordinary.closingTime}`} - - - {facility.openingDetails.openingHours.weekends.alwaysOpen - ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Sat-Sun" })} ${facility.openingDetails.openingHours.weekends.openingTime}-${facility.openingDetails.openingHours.weekends.closingTime}`} - -
    -
    -
    + ))}
    {buttonUrl && ( diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css index 0c33da7cd..11a410f13 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/wellnessAndExercise.module.css @@ -7,29 +7,6 @@ ); /* Creates space between the wrapper and buttonContainer */ } -.content { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.image { - width: 100%; - height: 270px; - object-fit: cover; - border-radius: var(--Corner-radius-Medium); -} - -.information { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); -} - -.body { - margin-top: var(--Spacing-x1); -} - .buttonContainer { background-color: var(--Base-Background-Primary-Normal); border-top: 1px solid var(--Base-Border-Subtle); diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index eb9d1dd37..a565ea117 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -147,7 +147,10 @@ export default async function HotelPage() { {/* TODO */} Restaurant & Bar - + Date: Mon, 18 Nov 2024 11:23:34 +0100 Subject: [PATCH 033/101] feat(SW-880): refactor facility component --- .../facility.module.css} | 2 +- .../WellnessAndExercise/Facility/index.tsx | 64 +++++++++++++++++++ .../FacilityCard/index.tsx | 54 ---------------- .../SidePeeks/WellnessAndExercise/index.tsx | 11 +--- .../components/hotelPage/sidepeek/facility.ts | 5 ++ .../hotelPage/sidepeek/facilityCard.ts | 19 ------ 6 files changed, 72 insertions(+), 83 deletions(-) rename components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/{FacilityCard/facilityCard.module.css => Facility/facility.module.css} (95%) create mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx delete mode 100644 components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx create mode 100644 types/components/hotelPage/sidepeek/facility.ts delete mode 100644 types/components/hotelPage/sidepeek/facilityCard.ts diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/facility.module.css similarity index 95% rename from components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css rename to components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/facility.module.css index 5abac9f32..22aea5bce 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/facilityCard.module.css +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/facility.module.css @@ -17,6 +17,6 @@ gap: var(--Spacing-x-one-and-half); } -.body { +.openingHours { margin-top: var(--Spacing-x1); } diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx new file mode 100644 index 000000000..474ccb901 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx @@ -0,0 +1,64 @@ +import Image from "@/components/Image" +import Body from "@/components/TempDesignSystem/Text/Body" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./facility.module.css" + +import { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility" + +export default async function Facility({ data }: FacilityProps) { + const intl = await getIntl() + const imgUrl = data.content.images[0]?.imageSizes.medium + const imgAltText = data.content.images[0]?.metaData.altText + const facilityType = data.type + const ordinaryOpeningTimes = { + alwaysOpen: data.openingDetails.openingHours.ordinary.alwaysOpen, + openingTime: data.openingDetails.openingHours.ordinary.openingTime, + closingTime: data.openingDetails.openingHours.ordinary.closingTime, + } + const weekendOpeningTimes = { + alwaysOpen: data.openingDetails.openingHours.weekends.alwaysOpen, + openingTime: data.openingDetails.openingHours.weekends.openingTime, + closingTime: data.openingDetails.openingHours.weekends.closingTime, + } + + return ( +
    + {imgUrl && ( + {imgAltText + )} +
    + + + {intl.formatMessage({ id: `${facilityType}` })} + + +
    + + {intl.formatMessage({ id: " Opening Hours" })} + +
    + + {ordinaryOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`} + + + {weekendOpeningTimes.alwaysOpen + ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` + : `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`} + +
    +
    +
    +
    + ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx deleted file mode 100644 index ba3925baf..000000000 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/FacilityCard/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import Image from "@/components/Image" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" - -import styles from "./facilityCard.module.css" - -import type { FacilityCardProps } from "@/types/components/hotelPage/sidepeek/facilityCard" - -export default async function FacilityCard({ - imgUrl, - imgAltText, - facilityType, - ordinaryOpeningTimes, - weekendOpeningTimes, -}: FacilityCardProps) { - const intl = await getIntl() - return ( -
    - {imgUrl && ( - {imgAltText - )} -
    - - - {intl.formatMessage({ id: `${facilityType}` })} - - -
    - - {intl.formatMessage({ id: " Opening Hours" })} - - - {ordinaryOpeningTimes.alwaysOpen - ? `${intl.formatMessage({ id: "Mon-Fri" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Mon-Fri" })} ${ordinaryOpeningTimes.openingTime}-${ordinaryOpeningTimes.closingTime}`} - - - {weekendOpeningTimes.alwaysOpen - ? `${intl.formatMessage({ id: "Sat-Sun" })} ${intl.formatMessage({ id: "Always open" })}` - : `${intl.formatMessage({ id: "Sat-Sun" })} ${weekendOpeningTimes.openingTime}-${weekendOpeningTimes.closingTime}`} - -
    -
    -
    - ) -} diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx index d7f381d9b..97ac09a91 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/index.tsx @@ -6,7 +6,7 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import FacilityCard from "./FacilityCard" +import Facility from "./Facility" import styles from "./wellnessAndExercise.module.css" @@ -26,14 +26,7 @@ export default async function WellnessAndExerciseSidePeek({ >
    {healthFacilities.map((facility) => ( - + ))}
    {buttonUrl && ( diff --git a/types/components/hotelPage/sidepeek/facility.ts b/types/components/hotelPage/sidepeek/facility.ts new file mode 100644 index 000000000..6cb6f0796 --- /dev/null +++ b/types/components/hotelPage/sidepeek/facility.ts @@ -0,0 +1,5 @@ +import type { Hotel } from "@/types/hotel" + +export type FacilityProps = { + data: Hotel["healthFacilities"][number] +} diff --git a/types/components/hotelPage/sidepeek/facilityCard.ts b/types/components/hotelPage/sidepeek/facilityCard.ts deleted file mode 100644 index fbf6e9942..000000000 --- a/types/components/hotelPage/sidepeek/facilityCard.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type FacilityCardProps = { - imgUrl: string - imgAltText: string - facilityType: string - ordinaryOpeningTimes: { - alwaysOpen: boolean - isClosed: boolean - openingTime?: string - closingTime?: string - sortOrder?: number - } - weekendOpeningTimes: { - alwaysOpen: boolean - isClosed: boolean - openingTime?: string - closingTime?: string - sortOrder?: number - } -} From 3c181959c2637d2a555a78428fa58ff27f1a0ea3 Mon Sep 17 00:00:00 2001 From: Fredrik Thorsson Date: Mon, 18 Nov 2024 12:13:23 +0100 Subject: [PATCH 034/101] feat(SW-880): refactor variables --- .../WellnessAndExercise/Facility/index.tsx | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx index 474ccb901..e00ed5964 100644 --- a/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/WellnessAndExercise/Facility/index.tsx @@ -10,26 +10,16 @@ import { FacilityProps } from "@/types/components/hotelPage/sidepeek/facility" export default async function Facility({ data }: FacilityProps) { const intl = await getIntl() - const imgUrl = data.content.images[0]?.imageSizes.medium - const imgAltText = data.content.images[0]?.metaData.altText - const facilityType = data.type - const ordinaryOpeningTimes = { - alwaysOpen: data.openingDetails.openingHours.ordinary.alwaysOpen, - openingTime: data.openingDetails.openingHours.ordinary.openingTime, - closingTime: data.openingDetails.openingHours.ordinary.closingTime, - } - const weekendOpeningTimes = { - alwaysOpen: data.openingDetails.openingHours.weekends.alwaysOpen, - openingTime: data.openingDetails.openingHours.weekends.openingTime, - closingTime: data.openingDetails.openingHours.weekends.closingTime, - } + const image = data.content.images[0] + const ordinaryOpeningTimes = data.openingDetails.openingHours.ordinary + const weekendOpeningTimes = data.openingDetails.openingHours.weekends return (
    - {imgUrl && ( + {image.imageSizes.medium && ( {imgAltText - - {intl.formatMessage({ id: `${facilityType}` })} - + {intl.formatMessage({ id: `${data.type}` })}
    From 5fb70866eac87b52fedb532ea16d8587d43da349 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Mon, 18 Nov 2024 14:02:32 +0000 Subject: [PATCH 035/101] Merged in feat/SW-342-filtering-and-sorting-mobile (pull request #919) Feat/SW-342 filtering and sorting mobile * feat(SW-342): add sort and filter on mobile * Use zustand for state management * Add count and translations * Clear filters * Small fixes * Fixes Approved-by: Pontus Dreij --- .../(standard)/select-hotel/page.module.css | 22 +++- .../(standard)/select-hotel/page.tsx | 8 +- .../HotelCardListing/index.tsx | 19 ++- .../filterAndSortModal.module.css | 99 ++++++++++++++++ .../SelectHotel/FilterAndSortModal/index.tsx | 87 ++++++++++++++ .../FilterCheckbox/filterCheckbox.module.css | 29 +++++ .../HotelFilter/FilterCheckbox/index.tsx | 35 ++++++ .../HotelFilter/hotelFilter.module.css | 7 -- .../SelectHotel/HotelFilter/index.tsx | 111 ++++++++++-------- .../HotelSorter/hotelSorter.module.css | 9 -- .../SelectHotel/HotelSorter/index.tsx | 25 ++-- .../MobileMapButtonContainer/index.tsx | 31 ++--- .../mobileMapButtonContainer.module.css | 4 +- i18n/dictionaries/da.json | 3 + i18n/dictionaries/de.json | 3 + i18n/dictionaries/en.json | 3 + i18n/dictionaries/fi.json | 3 + i18n/dictionaries/no.json | 3 + i18n/dictionaries/sv.json | 3 + stores/hotel-filters.ts | 27 +++++ .../selectHotel/filterAndSortModal.ts | 5 + .../selectHotel/filterCheckbox.ts | 6 + .../selectHotel/hotelFilters.ts | 1 + .../selectHotel/hotelSorter.ts | 4 + 24 files changed, 434 insertions(+), 113 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css create mode 100644 components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx create mode 100644 components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css create mode 100644 components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx delete mode 100644 components/HotelReservation/SelectHotel/HotelSorter/hotelSorter.module.css create mode 100644 stores/hotel-filters.ts create mode 100644 types/components/hotelReservation/selectHotel/filterAndSortModal.ts create mode 100644 types/components/hotelReservation/selectHotel/filterCheckbox.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index 8bf36ee38..e42544196 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -20,10 +20,13 @@ gap: var(--Spacing-x1); } +.sorter { + display: none; +} + .sideBar { display: flex; flex-direction: column; - max-width: 340px; } .link { @@ -47,6 +50,10 @@ gap: var(--Spacing-x3); } +.filter { + display: none; +} + @media (min-width: 768px) { .main { padding: var(--Spacing-x5); @@ -58,6 +65,11 @@ var(--Spacing-x5); } + .sorter { + display: block; + width: 339px; + } + .title { margin: 0 auto; display: flex; @@ -65,6 +77,14 @@ align-items: center; justify-content: space-between; } + + .sideBar { + max-width: 340px; + } + .filter { + display: block; + } + .link { display: flex; padding-bottom: var(--Spacing-x6); diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 0493c9d70..791773f94 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -74,9 +74,11 @@ export default async function SelectHotelPage({ {city.name} {hotels.length} hotels
    - +
    + +
    - +
    @@ -118,7 +120,7 @@ export default async function SelectHotelPage({ />
    )} - +
    {!hotels.length && ( diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 4ba65ed9c..c4a5a1eee 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -2,7 +2,7 @@ import { useSearchParams } from "next/navigation" import { useMemo } from "react" -import Title from "@/components/TempDesignSystem/Text/Title" +import { useHotelFilterStore } from "@/stores/hotel-filters" import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" @@ -22,6 +22,8 @@ export default function HotelCardListing({ onHotelCardHover, }: HotelCardListingProps) { const searchParams = useSearchParams() + const activeFilters = useHotelFilterStore((state) => state.activeFilters) + const setResultCount = useHotelFilterStore((state) => state.setResultCount) const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, @@ -57,17 +59,22 @@ export default function HotelCardListing({ }, [hotelData, sortBy]) const hotels = useMemo(() => { - const appliedFilters = searchParams.get("filters")?.split(",") - if (!appliedFilters || appliedFilters.length === 0) return sortedHotels + if (activeFilters.length === 0) { + setResultCount(sortedHotels.length) + return sortedHotels + } - return sortedHotels.filter((hotel) => - appliedFilters.every((appliedFilterId) => + const filteredHotels = sortedHotels.filter((hotel) => + activeFilters.every((appliedFilterId) => hotel.hotelData.detailedFacilities.some( (facility) => facility.id.toString() === appliedFilterId ) ) ) - }, [searchParams, sortedHotels]) + + setResultCount(filteredHotels.length) + return filteredHotels + }, [activeFilters, sortedHotels, setResultCount]) return (
    diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css new file mode 100644 index 000000000..6768d2c56 --- /dev/null +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css @@ -0,0 +1,99 @@ +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} +@keyframes modal-slide-up { + from { + bottom: -100%; + } + + to { + bottom: 0; + } +} + +.overlay { + align-items: center; + background: rgba(0, 0, 0, 0.5); + display: flex; + height: var(--visual-viewport-height); + justify-content: center; + left: 0; + position: fixed; + top: 0; + width: 100vw; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + + &[data-exiting] { + animation: modal-fade 150ms reverse ease-in; + } +} + +.modal { + position: absolute; + left: 0; + bottom: 0; + height: calc(100dvh - 20px); + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); + width: 100%; + + &[data-entering] { + animation: modal-slide-up 200ms; + } + + &[data-existing] { + animation: modal-slide-up 200ms reverse; + } +} + +.content { + flex-direction: column; + gap: var(--Spacing-x3); + display: flex; + height: 100%; +} + +.sorter { + padding: var(--Spacing-x2); + flex: 0 0 auto; +} + +.filters { + padding: var(--Spacing-x2); + flex: 1 1 auto; + overflow-y: auto; +} + +.header { + text-align: right; + padding: var(--Spacing-x-one-and-half); + flex: 0 0 auto; +} + +.close { + background: none; + border: none; + cursor: pointer; + justify-self: flex-end; + padding: 0; +} + +.footer { + display: flex; + flex-direction: column-reverse; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); + flex: 0 0 auto; + border-top: 1px solid var(--Base-Border-Subtle); +} diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx new file mode 100644 index 000000000..be1a1bc9c --- /dev/null +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -0,0 +1,87 @@ +"use client" + +import { + Dialog as AriaDialog, + DialogTrigger, + Modal, + ModalOverlay, +} from "react-aria-components" +import { useIntl } from "react-intl" + +import { useHotelFilterStore } from "@/stores/hotel-filters" + +import { CloseLargeIcon, FilterIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" + +import HotelFilter from "../HotelFilter" +import HotelSorter from "../HotelSorter" + +import styles from "./filterAndSortModal.module.css" + +import type { FilterAndSortModalProps } from "@/types/components/hotelReservation/selectHotel/filterAndSortModal" + +export default function FilterAndSortModal({ + filters, +}: FilterAndSortModalProps) { + const intl = useIntl() + const resultCount = useHotelFilterStore((state) => state.resultCount) + const setFilters = useHotelFilterStore((state) => state.setFilters) + + return ( + <> + + + + + + {({ close }) => ( + <> +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    + + )} +
    +
    +
    +
    + + ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css new file mode 100644 index 000000000..4b3b94787 --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/filterCheckbox.module.css @@ -0,0 +1,29 @@ +.container { + display: flex; + flex-direction: column; + color: var(--text-color); +} + +.container[data-selected] .checkbox { + border: none; + background: var(--UI-Input-Controls-Fill-Selected); +} + +.checkboxContainer { + display: flex; + align-items: center; + gap: var(--Spacing-x-one-and-half); +} + +.checkbox { + width: 24px; + height: 24px; + min-width: 24px; + border: 1px solid var(--UI-Input-Controls-Border-Normal); + border-radius: 4px; + transition: all 200ms; + display: flex; + align-items: center; + justify-content: center; + forced-color-adjust: none; +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx new file mode 100644 index 000000000..6767f666b --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelFilter/FilterCheckbox/index.tsx @@ -0,0 +1,35 @@ +"use client" + +import { Checkbox as AriaCheckbox } from "react-aria-components" + +import CheckIcon from "@/components/Icons/Check" + +import styles from "./filterCheckbox.module.css" + +import type { FilterCheckboxProps } from "@/types/components/hotelReservation/selectHotel/filterCheckbox" + +export default function FilterCheckbox({ + isSelected, + name, + id, + onChange, +}: FilterCheckboxProps) { + return ( + onChange(id)} + > + {({ isSelected }) => ( + <> + + + {isSelected && } + + {name} + + + )} + + ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css index c81b31cbd..8a4fcebff 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css +++ b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css @@ -1,6 +1,5 @@ .container { min-width: 272px; - display: none; } .container form { @@ -39,9 +38,3 @@ height: 1.25rem; margin: 0; } - -@media (min-width: 768px) { - .container { - display: block; - } -} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index a3b68b28e..c428894a3 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -1,37 +1,42 @@ "use client" import { usePathname, useSearchParams } from "next/navigation" -import { useCallback, useEffect } from "react" -import { FormProvider, useForm } from "react-hook-form" +import { useEffect } from "react" import { useIntl } from "react-intl" -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import { useHotelFilterStore } from "@/stores/hotel-filters" + import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Title from "@/components/TempDesignSystem/Text/Title" +import FilterCheckbox from "./FilterCheckbox" + import styles from "./hotelFilter.module.css" import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -export default function HotelFilter({ filters }: HotelFiltersProps) { +export default function HotelFilter({ className, filters }: HotelFiltersProps) { const intl = useIntl() const searchParams = useSearchParams() const pathname = usePathname() + const toggleFilter = useHotelFilterStore((state) => state.toggleFilter) + const setFilters = useHotelFilterStore((state) => state.setFilters) + const activeFilters = useHotelFilterStore((state) => state.activeFilters) - const methods = useForm>({ - defaultValues: searchParams - ?.get("filters") - ?.split(",") - .reduce((acc, curr) => ({ ...acc, [curr]: true }), {}), - }) - const { watch, handleSubmit, getValues, register } = methods + // Initialize the filters from the URL + useEffect(() => { + const filtersFromUrl = searchParams.get("filters") + if (filtersFromUrl) { + setFilters(filtersFromUrl.split(",")) + } else { + setFilters([]) + } + }, [searchParams, setFilters]) - const submitFilter = useCallback(() => { + // Update the URL when the filters changes + useEffect(() => { const newSearchParams = new URLSearchParams(searchParams) - const values = Object.entries(getValues()) - .filter(([_, value]) => !!value) - .map(([key, _]) => key) - .join(",") + const values = activeFilters.join(",") if (values === "") { newSearchParams.delete("filters") @@ -46,49 +51,51 @@ export default function HotelFilter({ filters }: HotelFiltersProps) { `${pathname}?${newSearchParams.toString()}` ) } - }, [getValues, pathname, searchParams]) - - useEffect(() => { - const subscription = watch(() => handleSubmit(submitFilter)()) - return () => subscription.unsubscribe() - }, [handleSubmit, watch, submitFilter]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeFilters]) if (!filters.facilityFilters.length && !filters.surroundingsFilters.length) { return null } return ( -
    diff --git a/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx b/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx index 942f9fafe..3a5613ade 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx +++ b/components/HotelReservation/HotelCard/HotelPriceList/HotelPriceCard/index.tsx @@ -1,5 +1,6 @@ import { useIntl } from "react-intl" +import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -9,15 +10,14 @@ import styles from "../hotelPriceList.module.css" import type { PriceCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps" export default function HotelPriceCard({ - currency, - memberAmount, - regularAmount, + productTypePrices, + isMemberPrice = false, }: PriceCardProps) { const intl = useIntl() return (
    - {memberAmount && ( + {isMemberPrice && (
    @@ -30,7 +30,7 @@ export default function HotelPriceCard({
    {intl.formatMessage({ id: "From" })} @@ -39,15 +39,15 @@ export default function HotelPriceCard({
    - {memberAmount ? memberAmount : regularAmount} + {productTypePrices.localPrice.pricePerNight} - {currency} + {productTypePrices.localPrice.currency} /{intl.formatMessage({ id: "night" })} @@ -55,17 +55,40 @@ export default function HotelPriceCard({
    - {/* TODO add correct local price when API change */} -
    -
    - - {intl.formatMessage({ id: "Approx." })} - -
    -
    - - EUR -
    -
    + {productTypePrices?.requestedPrice && ( +
    +
    + + {intl.formatMessage({ id: "Approx." })} + +
    +
    + + {productTypePrices.requestedPrice.pricePerNight}{" "} + {productTypePrices.requestedPrice.currency} + +
    +
    + )} + {productTypePrices.localPrice.pricePerStay !== + productTypePrices.localPrice.pricePerNight && ( + <> + +
    +
    + + {intl.formatMessage({ id: "Total" })} + +
    +
    + + {productTypePrices.localPrice.pricePerStay}{" "} + {productTypePrices.localPrice.currency} + +
    +
    + + )}
    ) } diff --git a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css index fe28eef93..bd81f1170 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css +++ b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css @@ -11,6 +11,17 @@ gap: var(--Spacing-x1); } +.prices { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); + max-width: 260px; +} + +.divider { + margin: var(--Spacing-x-half) 0; +} + .priceRow { display: flex; justify-content: space-between; diff --git a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx index 4167e044a..1f55c2d8a 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx +++ b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx @@ -1,6 +1,12 @@ +import { useParams } from "next/dist/client/components/navigation" import { useIntl } from "react-intl" +import { Lang } from "@/constants/languages" +import { selectRate } from "@/constants/routes/hotelReservation" + import { ErrorCircleIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import HotelPriceCard from "./HotelPriceCard" @@ -9,34 +15,52 @@ import styles from "./hotelPriceList.module.css" import { HotelPriceListProps } from "@/types/components/hotelReservation/selectHotel/hotePriceListProps" -export default function HotelPriceList({ price }: HotelPriceListProps) { +export default function HotelPriceList({ + price, + hotelId, +}: HotelPriceListProps) { const intl = useIntl() + const params = useParams() + const lang = params.lang as Lang return ( - <> +
    {price ? ( <> - - + {price.public && } + {price.member && ( + + )} + ) : (
    - +
    + +
    {intl.formatMessage({ - id: "There are no rooms available that match your request", + id: "There are no rooms available that match your request.", })}
    )} - +
    ) } diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index b20a7e30d..bfa87ea3a 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -70,13 +70,6 @@ gap: var(--Spacing-x-half); } -.prices { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); - width: 100%; -} - .detailsButton { border-bottom: none; } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index f996578c7..feda19a05 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -141,25 +141,7 @@ export default function HotelCard({
    )} -
    - - -
    + ) diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts index 6183c5da4..ba40ccc3a 100644 --- a/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -8,9 +8,9 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { lng: hotel.hotelData.location.longitude, }, name: hotel.hotelData.name, - publicPrice: hotel.price?.regularAmount ?? null, - memberPrice: hotel.price?.memberAmount ?? null, - currency: hotel.price?.currency || null, + publicPrice: hotel.price?.public?.localPrice.pricePerNight ?? null, + memberPrice: hotel.price?.member?.localPrice.pricePerNight ?? null, + currency: hotel.price?.public?.localPrice.currency || null, images: [ hotel.hotelData.hotelContent.images, ...(hotel.hotelData.gallery?.heroImages ?? []), diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index c4a5a1eee..ab2d45d3e 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -12,6 +12,7 @@ import styles from "./hotelCardListing.module.css" import { type HotelCardListingProps, HotelCardListingTypeEnum, + type HotelData, } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter" @@ -43,10 +44,15 @@ export default function HotelCardListing({ (a.hotelData.ratings?.tripAdvisor.rating ?? 0) ) case SortOrder.Price: + const getPricePerNight = (hotel: HotelData): number => { + return ( + hotel.price?.member?.localPrice?.pricePerNight ?? + hotel.price?.public?.localPrice?.pricePerNight ?? + 0 + ) + } return [...hotelData].sort( - (a, b) => - parseInt(a.price?.memberAmount ?? "0", 10) - - parseInt(b.price?.memberAmount ?? "0", 10) + (a, b) => getPricePerNight(a) - getPricePerNight(b) ) case SortOrder.Distance: default: diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index 9d6f24e00..af80e1f4d 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -49,7 +49,7 @@ export function filterDuplicateRoomTypesByLowestPrice( const previousLowest = roomMap.get(roomType) const currentRequestedPrice = Math.min( - Number(publicRequestedPrice.pricePerNight) ?? Infinity, + Number(publicRequestedPrice?.pricePerNight) ?? Infinity, Number(memberRequestedPrice?.pricePerNight) ?? Infinity ) const currentLocalPrice = Math.min( diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 02e7da88d..433a0ba33 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -348,7 +348,6 @@ "Terms and conditions": "Geschäftsbedingungen", "Thank you": "Danke", "Theatre": "Theater", - "There are no rooms available that match your request": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen", "There are no rooms available that match your request.": "Es sind keine Zimmer verfügbar, die Ihrer Anfrage entsprechen.", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 2263cf43f..38a5da82b 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -378,7 +378,6 @@ "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Theatre": "Theatre", - "There are no rooms available that match your request": "There are no rooms available that match your request", "There are no rooms available that match your request.": "There are no rooms available that match your request.", "There are no transactions to display": "There are no transactions to display", "Things nearby HOTEL_NAME": "Things nearby {hotelName}", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 59097c54a..218abed13 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -350,7 +350,6 @@ "Terms and conditions": "Käyttöehdot", "Thank you": "Kiitos", "Theatre": "Teatteri", - "There are no rooms available that match your request": "Pyyntöäsi vastaavia huoneita ei ole saatavilla", "There are no rooms available that match your request.": "Ei huoneita saatavilla, jotka vastaavat pyyntöäsi.", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index f5f131e8c..7cf8e985a 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -347,7 +347,6 @@ "Terms and conditions": "Vilkår og betingelser", "Thank you": "Takk", "Theatre": "Teater", - "There are no rooms available that match your request": "Det er ingen tilgjengelige rom som samsvarer med forespørselen din", "There are no rooms available that match your request.": "Det er ingen rom tilgjengelige som matcher din forespørsel.", "There are no transactions to display": "Det er ingen transaksjoner å vise", "Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index bca60e315..7f935f88a 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -347,7 +347,6 @@ "Terms and conditions": "Allmänna villkor", "Thank you": "Tack", "Theatre": "Teater", - "There are no rooms available that match your request": "Det finns inga tillgängliga rum som matchar din förfrågan", "There are no rooms available that match your request.": "Det finns inga rum tillgängliga som matchar din begäran.", "There are no transactions to display": "Det finns inga transaktioner att visa", "Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}", diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 407175703..4ab1aa5ea 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -82,7 +82,7 @@ export const getRoomAvailability = cache( roomStayStartDate, roomStayEndDate, children, - promotionCode, + bookingCode, rateCode, }: GetRoomsAvailabilityInput) { return serverClient().hotel.availability.rooms({ @@ -91,7 +91,7 @@ export const getRoomAvailability = cache( roomStayStartDate, roomStayEndDate, children, - promotionCode, + bookingCode, rateCode, }) } diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 04bb16b17..9bfecaf6a 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -8,9 +8,7 @@ export const getHotelsAvailabilityInputSchema = z.object({ roomStayEndDate: z.string(), adults: z.number(), children: z.string().optional(), - promotionCode: z.string().optional().default(""), - reservationProfileType: z.string().optional().default(""), - attachedProfileId: z.string().optional().default(""), + bookingCode: z.string().optional().default(""), }) export const getRoomsAvailabilityInputSchema = z.object({ @@ -19,9 +17,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ roomStayEndDate: z.string(), adults: z.number(), children: z.string().optional(), - promotionCode: z.string().optional(), - reservationProfileType: z.string().optional().default(""), - attachedProfileId: z.string().optional().default(""), + bookingCode: z.string().optional(), rateCode: z.string().optional(), }) @@ -31,9 +27,7 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({ roomStayEndDate: z.string(), adults: z.number(), children: z.string().optional(), - promotionCode: z.string().optional(), - reservationProfileType: z.string().optional().default(""), - attachedProfileId: z.string().optional().default(""), + bookingCode: z.string().optional(), rateCode: z.string(), roomTypeCode: z.string(), packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 9bc81225e..b68e87fc0 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -491,22 +491,6 @@ const occupancySchema = z.object({ children: z.array(childrenSchema), }) -const bestPricePerStaySchema = z.object({ - currency: z.string(), - // TODO: remove optional when API is ready - regularAmount: z.string().optional(), - // TODO: remove optional when API is ready - memberAmount: z.string().optional(), -}) - -const bestPricePerNightSchema = z.object({ - currency: z.string(), - // TODO: remove optional when API is ready - regularAmount: z.string().optional(), - // TODO: remove optional when API is ready - memberAmount: z.string().optional(), -}) - const linksSchema = z.object({ links: z.array( z.object({ @@ -516,30 +500,6 @@ const linksSchema = z.object({ ), }) -const hotelsAvailabilitySchema = z.object({ - data: z.array( - z.object({ - attributes: z.object({ - checkInDate: z.string(), - checkOutDate: z.string(), - occupancy: occupancySchema.optional(), - status: z.string(), - hotelId: z.number(), - ratePlanSet: z.string().optional(), - bestPricePerStay: bestPricePerStaySchema.optional(), - bestPricePerNight: bestPricePerNightSchema.optional(), - }), - relationships: linksSchema.optional(), - type: z.string().optional(), - }) - ), -}) - -export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema -export type HotelsAvailability = z.infer -export type HotelsAvailabilityPrices = - HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"] - export const priceSchema = z.object({ pricePerNight: z.coerce.number(), pricePerStay: z.coerce.number(), @@ -550,7 +510,7 @@ export const productTypePriceSchema = z.object({ rateCode: z.string(), rateType: z.string().optional(), localPrice: priceSchema, - requestedPrice: priceSchema, + requestedPrice: priceSchema.optional(), }) const productSchema = z.object({ @@ -560,6 +520,34 @@ const productSchema = z.object({ }), }) +const hotelsAvailabilitySchema = z.object({ + data: z.array( + z.object({ + attributes: z.object({ + checkInDate: z.string(), + checkOutDate: z.string(), + occupancy: occupancySchema, + status: z.string(), + hotelId: z.number(), + productType: z + .object({ + public: productTypePriceSchema.optional(), + member: productTypePriceSchema.optional(), + }) + .optional(), + }), + relationships: linksSchema.optional(), + type: z.string().optional(), + }) + ), +}) + +export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema +export type HotelsAvailability = z.infer +export type ProductType = + HotelsAvailability["data"][number]["attributes"]["productType"] +export type ProductTypePrices = z.infer + const roomConfigurationSchema = z.object({ status: z.string(), roomTypeCode: z.string(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 28dd458d9..ea094de0f 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -369,9 +369,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, } = input const params: Record = { @@ -379,9 +377,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, ...(children && { children }), - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, language: apiLang, } hotelsAvailabilityCounter.add(1, { @@ -390,8 +386,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.hotelsAvailability start", @@ -414,8 +409,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -446,8 +440,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -466,8 +459,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.hotelsAvailability success", @@ -493,9 +485,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, rateCode, } = input @@ -504,9 +494,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, ...(children && { children }), - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, } roomsAvailabilityCounter.add(1, { @@ -515,8 +503,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.roomsAvailability start", @@ -540,8 +527,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, @@ -572,8 +558,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -592,8 +577,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.roomsAvailability success", @@ -620,9 +604,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, rateCode, roomTypeCode, packageCodes, @@ -633,9 +615,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, ...(children && { children }), - promotionCode, - reservationProfileType, - attachedProfileId, + bookingCode, language: toApiLang(ctx.lang), } @@ -645,8 +625,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.selectedRoomAvailability start", @@ -670,8 +649,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "http_error", error: JSON.stringify({ status: apiResponseAvailability.status, @@ -702,8 +680,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, error_type: "validation_error", error: JSON.stringify(validateAvailabilityData.error), }) @@ -797,8 +774,7 @@ export const hotelQueryRouter = router({ roomStayEndDate, adults, children, - promotionCode, - reservationProfileType, + bookingCode, }) console.info( "api.hotels.selectedRoomAvailability success", diff --git a/stores/enter-details.ts b/stores/enter-details.ts index 374f1dc89..b5f99cc38 100644 --- a/stores/enter-details.ts +++ b/stores/enter-details.ts @@ -25,7 +25,7 @@ const SESSION_STORAGE_KEY = "enterDetails" type TotalPrice = { local: { price: number; currency: string } - euro: { price: number; currency: string } + euro?: { price: number; currency: string } } export interface EnterDetailsState { diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 74ed4bda4..628fa3f8b 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -25,7 +25,7 @@ type Price = { export type RoomsData = { roomType: string localPrice: Price - euroPrice: Price + euroPrice: Price | undefined adults: number children?: Child[] cancellationText: string diff --git a/types/components/hotelReservation/selectHotel/availabilityInput.ts b/types/components/hotelReservation/selectHotel/availabilityInput.ts index 5b3a51b93..d8b7aad26 100644 --- a/types/components/hotelReservation/selectHotel/availabilityInput.ts +++ b/types/components/hotelReservation/selectHotel/availabilityInput.ts @@ -4,7 +4,5 @@ export type AvailabilityInput = { roomStayEndDate: string adults: number children?: string - promotionCode?: string - reservationProfileType?: string - attachedProfileId?: string + bookingCode?: string } diff --git a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts index 2464fad43..4144abf45 100644 --- a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts +++ b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts @@ -1,5 +1,6 @@ -import type { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" +import type { ProductType } from "@/server/routers/hotels/output" export type HotelPriceListProps = { - price: HotelsAvailabilityPrices + price: ProductType + hotelId: string } diff --git a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts index 9c56f9949..68a6174ed 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -1,4 +1,4 @@ -import { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" +import { ProductType } from "@/server/routers/hotels/output" import { Hotel } from "@/types/hotel" @@ -16,5 +16,5 @@ export type HotelCardListingProps = { export type HotelData = { hotelData: Hotel - price: HotelsAvailabilityPrices + price: ProductType } diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 233fc2105..810dba573 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -29,8 +29,8 @@ type ImageMetaData = z.infer export type HotelPin = { name: string coordinates: Coordinates - publicPrice: string | null - memberPrice: string | null + publicPrice: number | null + memberPrice: number | null currency: string | null images: { imageSizes: ImageSizes diff --git a/types/components/hotelReservation/selectHotel/priceCardProps.ts b/types/components/hotelReservation/selectHotel/priceCardProps.ts index d339b4a06..a56a67d0f 100644 --- a/types/components/hotelReservation/selectHotel/priceCardProps.ts +++ b/types/components/hotelReservation/selectHotel/priceCardProps.ts @@ -1,5 +1,6 @@ +import { ProductTypePrices } from "@/server/routers/hotels/output" + export type PriceCardProps = { - currency: string - memberAmount?: string | undefined - regularAmount?: string | undefined + productTypePrices: ProductTypePrices + isMemberPrice?: boolean } From c6fc500d9e52a474c32dfd1e79bbca5c585043b0 Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Mon, 18 Nov 2024 09:13:23 +0100 Subject: [PATCH 039/101] feat: make steps of enter details flow dynamic depending on data --- .../[step]/@hotelHeader/[...paths]/page.tsx | 1 - .../(standard)/[step]/@hotelHeader/page.tsx | 25 --- .../(standard)/select-hotel/page.tsx | 2 +- .../{[step] => step}/@hotelHeader/loading.tsx | 0 .../step/@hotelHeader/page.module.css | 8 +- .../(standard)/step/@hotelHeader/page.tsx | 35 ++- .../{[step] => step}/@summary/page.module.css | 0 .../{[step] => step}/@summary/page.tsx | 2 +- .../(standard)/{[step] => step}/_preload.ts | 0 .../{[step] => step}/enterDetailsLayout.css | 0 .../(standard)/{[step] => step}/layout.tsx | 11 +- .../(standard)/{[step] => step}/page.tsx | 120 +++++----- .../payment-callback/[lang]/[status]/route.ts | 4 +- components/Forms/BookingWidget/index.tsx | 2 +- .../BedType/bedOptions.module.css | 3 +- .../EnterDetails/BedType/index.tsx | 20 +- .../Breakfast/breakfast.module.css | 1 - .../EnterDetails/Breakfast/index.tsx | 39 ++-- .../EnterDetails/Breakfast/schema.ts | 8 +- .../EnterDetails/Details/details.module.css | 1 - .../EnterDetails/Details/index.tsx | 42 ++-- .../HistoryStateManager/index.tsx | 6 +- .../EnterDetails/Payment/index.tsx | 16 +- .../EnterDetails/Provider/index.tsx | 29 --- .../EnterDetails/SectionAccordion/index.tsx | 29 +-- .../sectionAccordion.module.css | 7 +- .../EnterDetails/SelectedRoom/index.tsx | 9 +- .../Summary/BottomSheet/index.tsx | 6 +- .../EnterDetails/Summary/index.tsx | 192 ++++++++-------- .../HotelCard/HotelPriceList/index.tsx | 2 +- .../HotelReservation/HotelCard/index.tsx | 2 +- .../HotelCardDialog/index.tsx | 2 +- .../MobileMapButtonContainer/index.tsx | 2 +- .../SelectHotel/SelectHotelMap/index.tsx | 2 +- .../SelectRate/RoomSelection/utils.ts | 23 -- .../Form/ChoiceCard/_Card/index.tsx | 2 + components/TempDesignSystem/Select/index.tsx | 3 + constants/routes/hotelReservation.js | 126 ++++------- contexts/Details.ts | 5 + contexts/Steps.ts | 5 + hooks/useSetOverflowVisibleOnRA.ts | 11 + middlewares/bookingFlow.ts | 6 +- next.config.js | 5 + providers/DetailsProvider.tsx | 30 +++ providers/StepsProvider.tsx | 53 +++++ server/routers/hotels/schemas/packages.ts | 62 ++++++ stores/details.ts | 195 ++++++++++++++++ stores/enter-details.ts | 209 ------------------ stores/steps.ts | 159 +++++++++++++ .../enterDetails/breakfast.ts | 2 +- .../hotelReservation/enterDetails/step.ts | 7 +- .../hotelReservation/enterDetails/store.ts | 3 - .../hotelReservation/enterDetails/summary.ts | 6 + .../selectRate/sectionAccordion.ts | 2 +- types/contexts/details.ts | 3 + types/contexts/steps.ts | 3 + types/enums/breakfast.ts | 1 - types/enums/step.ts | 6 + types/providers/details.ts | 3 + types/providers/steps.ts | 10 + types/stores/details.ts | 40 ++++ types/stores/steps.ts | 10 + 62 files changed, 959 insertions(+), 659 deletions(-) delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/@hotelHeader/loading.tsx (100%) rename components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css => app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css (90%) rename components/HotelReservation/HotelSelectionHeader/index.tsx => app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx (63%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/@summary/page.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/@summary/page.tsx (99%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/_preload.ts (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/enterDetailsLayout.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/layout.tsx (73%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/{[step] => step}/page.tsx (59%) delete mode 100644 components/HotelReservation/EnterDetails/Provider/index.tsx create mode 100644 contexts/Details.ts create mode 100644 contexts/Steps.ts create mode 100644 hooks/useSetOverflowVisibleOnRA.ts create mode 100644 providers/DetailsProvider.tsx create mode 100644 providers/StepsProvider.tsx create mode 100644 server/routers/hotels/schemas/packages.ts create mode 100644 stores/details.ts delete mode 100644 stores/enter-details.ts create mode 100644 stores/steps.ts delete mode 100644 types/components/hotelReservation/enterDetails/store.ts create mode 100644 types/components/hotelReservation/enterDetails/summary.ts create mode 100644 types/contexts/details.ts create mode 100644 types/contexts/steps.ts create mode 100644 types/enums/step.ts create mode 100644 types/providers/details.ts create mode 100644 types/providers/steps.ts create mode 100644 types/stores/details.ts create mode 100644 types/stores/steps.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx deleted file mode 100644 index 03a82e5f5..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/[...paths]/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../page" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx deleted file mode 100644 index 75101475a..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { redirect } from "next/navigation" - -import { getHotelData } from "@/lib/trpc/memoizedRequests" - -import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader" - -import type { LangParams, PageArgs } from "@/types/params" - -export default async function HotelHeader({ - params, - searchParams, -}: PageArgs) { - const home = `/${params.lang}` - if (!searchParams.hotel) { - redirect(home) - } - const hotel = await getHotelData({ - hotelId: searchParams.hotel, - language: params.lang, - }) - if (!hotel?.data) { - redirect(home) - } - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 791773f94..a12639acf 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -86,7 +86,7 @@ export default async function SelectHotelPage({
    diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@hotelHeader/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx diff --git a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css similarity index 90% rename from components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css index 9eefdfb33..82d6353ac 100644 --- a/components/HotelReservation/HotelSelectionHeader/hotelSelectionHeader.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css @@ -1,9 +1,9 @@ -.hotelSelectionHeader { +.header { background-color: var(--Base-Surface-Subtle-Normal); padding: var(--Spacing-x3) var(--Spacing-x2); } -.hotelSelectionHeaderWrapper { +.wrapper { display: flex; flex-direction: column; gap: var(--Spacing-x3); @@ -35,11 +35,11 @@ } @media (min-width: 768px) { - .hotelSelectionHeader { + .header { padding: var(--Spacing-x4) 0; } - .hotelSelectionHeaderWrapper { + .wrapper { flex-direction: row; gap: var(--Spacing-x6); margin: 0 auto; diff --git a/components/HotelReservation/HotelSelectionHeader/index.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx similarity index 63% rename from components/HotelReservation/HotelSelectionHeader/index.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx index c0045dff5..83412f1d1 100644 --- a/components/HotelReservation/HotelSelectionHeader/index.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx @@ -1,23 +1,38 @@ -"use client" -import { useIntl } from "react-intl" +import { redirect } from "next/navigation" + +import { getHotelData } from "@/lib/trpc/memoizedRequests" import Divider from "@/components/TempDesignSystem/Divider" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" -import styles from "./hotelSelectionHeader.module.css" +import styles from "./page.module.css" -import { HotelSelectionHeaderProps } from "@/types/components/hotelReservation/selectRate/hotelSelectionHeader" +import type { LangParams, PageArgs } from "@/types/params" -export default function HotelSelectionHeader({ - hotel, -}: HotelSelectionHeaderProps) { - const intl = useIntl() +export default async function HotelHeader({ + params, + searchParams, +}: PageArgs) { + const home = `/${params.lang}` + if (!searchParams.hotel) { + redirect(home) + } + const hotelData = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) + if (!hotelData?.data) { + redirect(home) + } + const intl = await getIntl() + const hotel = hotelData.data.attributes return ( -
    -
    +
    +
    {hotel.name} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx similarity index 99% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index d3228e7b7..da3554f50 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -61,7 +61,7 @@ export default async function SummaryPage({ if (!availability || !availability.selectedRoom) { console.error("No hotel or availability data", availability) // TODO: handle this case - redirect(selectRate[params.lang]) + redirect(selectRate(params.lang)) } const prices = diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/_preload.ts rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/enterDetailsLayout.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx similarity index 73% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx index fbd462544..2bd8a5102 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx @@ -1,20 +1,19 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" -import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import { setLang } from "@/i18n/serverContext" +import DetailsProvider from "@/providers/DetailsProvider" import { preload } from "./_preload" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" export default async function StepLayout({ - summary, children, hotelHeader, params, + summary, }: React.PropsWithChildren< - LayoutArgs<LangParams & { step: StepEnum }> & { + LayoutArgs<LangParams> & { hotelHeader: React.ReactNode summary: React.ReactNode } @@ -25,7 +24,7 @@ export default async function StepLayout({ const user = await getProfileSafely() return ( - <EnterDetailsProvider step={params.step} isMember={!!user}> + <DetailsProvider isMember={!!user}> <main className="enter-details-layout__layout"> {hotelHeader} <div className={"enter-details-layout__container"}> @@ -35,6 +34,6 @@ export default async function StepLayout({ </aside> </div> </main> - </EnterDetailsProvider> + </DetailsProvider> ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx similarity index 59% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index ae04c61a6..648cdff93 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,6 +1,6 @@ import "./enterDetailsLayout.css" -import { notFound } from "next/navigation" +import { notFound, redirect, RedirectType } from "next/navigation" import { getBreakfastPackages, @@ -22,9 +22,10 @@ import { getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" +import StepsProvider from "@/providers/StepsProvider" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { StepEnum } from "@/types/enums/step" import type { LangParams, PageArgs } from "@/types/params" function isValidStep(step: string): step is StepEnum { @@ -32,11 +33,9 @@ function isValidStep(step: string): step is StepEnum { } export default async function StepPage({ - params, + params: { lang }, searchParams, -}: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) { - const { lang } = params - +}: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) { const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) const { @@ -88,7 +87,7 @@ export default async function StepPage({ const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() - if (!isValidStep(params.step) || !hotelData || !roomAvailability) { + if (!isValidStep(searchParams.step) || !hotelData || !roomAvailability) { return notFound() } @@ -113,54 +112,65 @@ export default async function StepPage({ } return ( - <section> - <HistoryStateManager /> - <SelectedRoom - hotelId={hotelId} - room={roomAvailability.selectedRoom} - rateDescription={roomAvailability.cancellationText} - /> - - {/* TODO: How to handle no beds found? */} - {roomAvailability.bedTypes ? ( - <SectionAccordion - header="Select bed" - step={StepEnum.selectBed} - label={intl.formatMessage({ id: "Request bedtype" })} - > - <BedType bedTypes={roomAvailability.bedTypes} /> - </SectionAccordion> - ) : null} - - <SectionAccordion - header={intl.formatMessage({ id: "Food options" })} - step={StepEnum.breakfast} - label={intl.formatMessage({ id: "Select breakfast options" })} - > - <Breakfast packages={breakfastPackages} /> - </SectionAccordion> - <SectionAccordion - header={intl.formatMessage({ id: "Details" })} - step={StepEnum.details} - label={intl.formatMessage({ id: "Enter your details" })} - > - <Details user={user} /> - </SectionAccordion> - <SectionAccordion - header={mustBeGuaranteed ? paymentGuarantee : payment} - step={StepEnum.payment} - label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} - > - <Payment - roomPrice={roomPrice} - otherPaymentOptions={ - hotelData.data.attributes.merchantInformationData - .alternatePaymentOptions - } - savedCreditCards={savedCreditCards} - mustBeGuaranteed={mustBeGuaranteed} + <StepsProvider + bedTypes={roomAvailability.bedTypes} + breakfastPackages={breakfastPackages} + isMember={!!user} + step={searchParams.step} + > + <section> + <HistoryStateManager /> + <SelectedRoom + hotelId={hotelId} + room={roomAvailability.selectedRoom} + rateDescription={roomAvailability.cancellationText} /> - </SectionAccordion> - </section> + + {/* TODO: How to handle no beds found? */} + {roomAvailability.bedTypes ? ( + <SectionAccordion + header={intl.formatMessage({ id: "Select bed" })} + step={StepEnum.selectBed} + label={intl.formatMessage({ id: "Request bedtype" })} + > + <BedType bedTypes={roomAvailability.bedTypes} /> + </SectionAccordion> + ) : null} + + {breakfastPackages?.length ? ( + <SectionAccordion + header={intl.formatMessage({ id: "Food options" })} + step={StepEnum.breakfast} + label={intl.formatMessage({ id: "Select breakfast options" })} + > + <Breakfast packages={breakfastPackages} /> + </SectionAccordion> + ) : null} + + <SectionAccordion + header={intl.formatMessage({ id: "Details" })} + step={StepEnum.details} + label={intl.formatMessage({ id: "Enter your details" })} + > + <Details user={user} /> + </SectionAccordion> + + <SectionAccordion + header={mustBeGuaranteed ? paymentGuarantee : payment} + step={StepEnum.payment} + label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} + > + <Payment + roomPrice={roomPrice} + otherPaymentOptions={ + hotelData.data.attributes.merchantInformationData + .alternatePaymentOptions + } + savedCreditCards={savedCreditCards} + mustBeGuaranteed={mustBeGuaranteed} + /> + </SectionAccordion> + </section> + </StepsProvider> ) } diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts index 624df1f52..5884d63f9 100644 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -26,7 +26,7 @@ export async function GET( const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER) if (status === "success" && confirmationNumber) { - const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`) + const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation(lang)}`) confirmationUrl.searchParams.set( BOOKING_CONFIRMATION_NUMBER, confirmationNumber @@ -36,7 +36,7 @@ export async function GET( return NextResponse.redirect(confirmationUrl) } - const returnUrl = new URL(`${publicURL}/${payment[lang]}`) + const returnUrl = new URL(`${publicURL}/${payment(lang)}`) returnUrl.search = queryParams.toString() if (confirmationNumber) { diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index b9ea569e8..b47ae74aa 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -35,7 +35,7 @@ export default function Form({ const locationData: Location = JSON.parse(decodeURIComponent(data.location)) const bookingFlowPage = - locationData.type == "cities" ? selectHotel[lang] : selectRate[lang] + locationData.type == "cities" ? selectHotel(lang) : selectRate(lang) const bookingWidgetParams = new URLSearchParams(data.date) if (locationData.type == "cities") diff --git a/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css b/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css index 81fd223b9..844ed4a6b 100644 --- a/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css +++ b/components/HotelReservation/EnterDetails/BedType/bedOptions.module.css @@ -1,7 +1,6 @@ .form { display: grid; gap: var(--Spacing-x2); - grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - padding-bottom: var(--Spacing-x3); + grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); width: min(600px, 100%); } diff --git a/components/HotelReservation/EnterDetails/BedType/index.tsx b/components/HotelReservation/EnterDetails/BedType/index.tsx index 1bf78fee5..eeb8237a0 100644 --- a/components/HotelReservation/EnterDetails/BedType/index.tsx +++ b/components/HotelReservation/EnterDetails/BedType/index.tsx @@ -4,7 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod" import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import { KingBedIcon } from "@/components/Icons" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" @@ -19,22 +20,18 @@ import type { } from "@/types/components/hotelReservation/enterDetails/bedType" export default function BedType({ bedTypes }: BedTypeProps) { - const bedType = useEnterDetailsStore((state) => state.userData.bedType) + const bedType = useDetailsStore((state) => state.data.bedType?.roomTypeCode) + const completeStep = useStepsStore((state) => state.completeStep) + const updateBedType = useDetailsStore((state) => state.actions.updateBedType) const methods = useForm<BedTypeFormSchema>({ - defaultValues: bedType?.roomTypeCode - ? { - bedType: bedType.roomTypeCode, - } - : undefined, + defaultValues: bedType ? { bedType } : undefined, criteriaMode: "all", mode: "all", resolver: zodResolver(bedTypeFormSchema), reValidateMode: "onChange", }) - const completeStep = useEnterDetailsStore((state) => state.completeStep) - const onSubmit = useCallback( (bedTypeRoomCode: BedTypeFormSchema) => { const matchingRoom = bedTypes.find( @@ -45,10 +42,11 @@ export default function BedType({ bedTypes }: BedTypeProps) { description: matchingRoom.description, roomTypeCode: matchingRoom.value, } - completeStep({ bedType }) + updateBedType(bedType) + completeStep() } }, - [completeStep, bedTypes] + [bedTypes, completeStep, updateBedType] ) useEffect(() => { diff --git a/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css b/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css index 81fd223b9..f24c6ba64 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css +++ b/components/HotelReservation/EnterDetails/Breakfast/breakfast.module.css @@ -2,6 +2,5 @@ display: grid; gap: var(--Spacing-x2); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - padding-bottom: var(--Spacing-x3); width: min(600px, 100%); } diff --git a/components/HotelReservation/EnterDetails/Breakfast/index.tsx b/components/HotelReservation/EnterDetails/Breakfast/index.tsx index 00d5ab4cc..fdaec3a84 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/index.tsx +++ b/components/HotelReservation/EnterDetails/Breakfast/index.tsx @@ -5,7 +5,8 @@ import { useCallback, useEffect } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import { Highlight } from "@/components/TempDesignSystem/Form/ChoiceCard/_Card" import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio" @@ -23,34 +24,37 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast" export default function Breakfast({ packages }: BreakfastProps) { const intl = useIntl() - const breakfast = useEnterDetailsStore((state) => state.userData.breakfast) + const breakfast = useDetailsStore(({ data }) => + data.breakfast + ? data.breakfast.code + : data.breakfast === false + ? "false" + : data.breakfast + ) + const updateBreakfast = useDetailsStore( + (state) => state.actions.updateBreakfast + ) + const completeStep = useStepsStore((state) => state.completeStep) - let defaultValues = undefined - if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { - defaultValues = { breakfast: BreakfastPackageEnum.NO_BREAKFAST } - } else if (breakfast?.code) { - defaultValues = { breakfast: breakfast.code } - } const methods = useForm<BreakfastFormSchema>({ - defaultValues, + defaultValues: breakfast ? { breakfast } : undefined, criteriaMode: "all", mode: "all", resolver: zodResolver(breakfastFormSchema), reValidateMode: "onChange", }) - const completeStep = useEnterDetailsStore((state) => state.completeStep) - const onSubmit = useCallback( (values: BreakfastFormSchema) => { const pkg = packages?.find((p) => p.code === values.breakfast) if (pkg) { - completeStep({ breakfast: pkg }) + updateBreakfast(pkg) } else { - completeStep({ breakfast: BreakfastPackageEnum.NO_BREAKFAST }) + updateBreakfast(false) } + completeStep() }, - [completeStep, packages] + [completeStep, packages, updateBreakfast] ) useEffect(() => { @@ -61,10 +65,6 @@ export default function Breakfast({ packages }: BreakfastProps) { return () => subscription.unsubscribe() }, [methods, onSubmit]) - if (!packages) { - return null - } - return ( <FormProvider {...methods}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> @@ -100,7 +100,6 @@ export default function Breakfast({ packages }: BreakfastProps) { /> ))} <RadioCard - id={BreakfastPackageEnum.NO_BREAKFAST} name="breakfast" subtitle={intl.formatMessage( { id: "{amount} {currency}" }, @@ -113,7 +112,7 @@ export default function Breakfast({ packages }: BreakfastProps) { id: "You can always change your mind later and add breakfast at the hotel.", })} title={intl.formatMessage({ id: "No breakfast" })} - value={BreakfastPackageEnum.NO_BREAKFAST} + value="false" /> </form> </FormProvider> diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts index 5f8c1f354..4766980cb 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/schema.ts +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -2,14 +2,10 @@ import { z } from "zod" import { breakfastPackageSchema } from "@/server/routers/hotels/output" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" - export const breakfastStoreSchema = z.object({ - breakfast: breakfastPackageSchema.or( - z.literal(BreakfastPackageEnum.NO_BREAKFAST) - ), + breakfast: breakfastPackageSchema.or(z.literal(false)), }) export const breakfastFormSchema = z.object({ - breakfast: z.string().or(z.literal(BreakfastPackageEnum.NO_BREAKFAST)), + breakfast: z.string().or(z.literal("false")), }) diff --git a/components/HotelReservation/EnterDetails/Details/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index 62a947e3e..f89dfa7cc 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,7 +1,6 @@ .form { display: grid; gap: var(--Spacing-x2); - padding: var(--Spacing-x3) 0px; } .container { diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 806776ce8..dd5959c31 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -1,9 +1,11 @@ "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 { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import Button from "@/components/TempDesignSystem/Button" import CountrySelect from "@/components/TempDesignSystem/Form/Country" @@ -24,19 +26,22 @@ import type { const formID = "enter-details" export default function Details({ user }: DetailsProps) { const intl = useIntl() - const initialData = useEnterDetailsStore((state) => ({ - countryCode: state.userData.countryCode, - email: state.userData.email, - firstName: state.userData.firstName, - lastName: state.userData.lastName, - phoneNumber: state.userData.phoneNumber, - join: state.userData.join, - dateOfBirth: state.userData.dateOfBirth, - zipCode: state.userData.zipCode, - termsAccepted: state.userData.termsAccepted, - membershipNo: state.userData.membershipNo, + const initialData = useDetailsStore((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, + membershipNo: state.data.membershipNo, })) + const updateDetails = useDetailsStore((state) => state.actions.updateDetails) + const completeStep = useStepsStore((state) => state.completeStep) + const methods = useForm<DetailsSchema>({ defaultValues: { countryCode: user?.address?.countryCode ?? initialData.countryCode, @@ -56,14 +61,20 @@ export default function Details({ user }: DetailsProps) { reValidateMode: "onChange", }) - const completeStep = useEnterDetailsStore((state) => state.completeStep) + const onSubmit = useCallback( + (values: DetailsSchema) => { + updateDetails(values) + completeStep() + }, + [completeStep, updateDetails] + ) return ( <FormProvider {...methods}> <form className={styles.form} id={formID} - onSubmit={methods.handleSubmit(completeStep)} + onSubmit={methods.handleSubmit(onSubmit)} > {user ? null : <Signup name="join" />} <Footnote @@ -107,7 +118,7 @@ export default function Details({ user }: DetailsProps) { readOnly={!!user} registerOptions={{ required: true }} /> - {user ? null : ( + {user || methods.watch("join") ? null : ( <Input className={styles.membershipNo} label={intl.formatMessage({ id: "Membership no" })} @@ -119,7 +130,6 @@ export default function Details({ user }: DetailsProps) { <footer className={styles.footer}> <Button disabled={!methods.formState.isValid} - form={formID} intent="secondary" size="small" theme="base" diff --git a/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx index 0ce1b1080..dc83a8072 100644 --- a/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx +++ b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx @@ -2,11 +2,11 @@ import { useCallback, useEffect } from "react" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useStepsStore } from "@/stores/steps" export default function HistoryStateManager() { - const setCurrentStep = useEnterDetailsStore((state) => state.setCurrentStep) - const currentStep = useEnterDetailsStore((state) => state.currentStep) + const setCurrentStep = useStepsStore((state) => state.setStep) + const currentStep = useStepsStore((state) => state.currentStep) const handleBackButton = useCallback( (event: PopStateEvent) => { diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index ff402bf2d..a912f74b9 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -18,7 +18,7 @@ import { } from "@/constants/currentWebHrefs" import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" @@ -40,7 +40,6 @@ import styles from "./payment.module.css" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" const maxRetries = 4 const retryInterval = 2000 @@ -61,12 +60,9 @@ export default function Payment({ const lang = useLang() const intl = useIntl() const queryParams = useSearchParams() - const { userData, roomData, setIsSubmittingDisabled } = useEnterDetailsStore( - (state) => ({ - userData: state.userData, - roomData: state.roomData, - setIsSubmittingDisabled: state.setIsSubmittingDisabled, - }) + const { booking, ...userData } = useDetailsStore((state) => state.data) + const setIsSubmittingDisabled = useDetailsStore( + (state) => state.actions.setIsSubmittingDisabled ) const { @@ -82,7 +78,7 @@ export default function Payment({ dateOfBirth, zipCode, } = userData - const { toDate, fromDate, rooms: rooms, hotel } = roomData + const { toDate, fromDate, rooms, hotel } = booking const [confirmationNumber, setConfirmationNumber] = useState<string>("") const [availablePaymentOptions, setAvailablePaymentOptions] = @@ -204,7 +200,7 @@ export default function Payment({ postalCode: zipCode, }, packages: { - breakfast: breakfast !== BreakfastPackageEnum.NO_BREAKFAST, + breakfast: !!(breakfast && breakfast.code), allergyFriendly: room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, petFriendly: diff --git a/components/HotelReservation/EnterDetails/Provider/index.tsx b/components/HotelReservation/EnterDetails/Provider/index.tsx deleted file mode 100644 index 82bfdbd82..000000000 --- a/components/HotelReservation/EnterDetails/Provider/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client" -import { useSearchParams } from "next/navigation" -import { PropsWithChildren, useRef } from "react" - -import { - EnterDetailsContext, - type EnterDetailsStore, - initEditDetailsState, -} from "@/stores/enter-details" - -import { EnterDetailsProviderProps } from "@/types/components/hotelReservation/enterDetails/store" - -export default function EnterDetailsProvider({ - step, - isMember, - children, -}: PropsWithChildren<EnterDetailsProviderProps>) { - const searchParams = useSearchParams() - const initialStore = useRef<EnterDetailsStore>() - if (!initialStore.current) { - initialStore.current = initEditDetailsState(step, searchParams, isMember) - } - - return ( - <EnterDetailsContext.Provider value={initialStore.current}> - {children} - </EnterDetailsContext.Provider> - ) -} diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index dee985295..ce548ae74 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from "react" import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" +import { useStepsStore } from "@/stores/steps" import { CheckIcon, ChevronDownIcon } from "@/components/Icons" import Footnote from "@/components/TempDesignSystem/Text/Footnote" @@ -10,12 +11,9 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./sectionAccordion.module.css" -import { - StepEnum, - StepStoreKeys, -} from "@/types/components/hotelReservation/enterDetails/step" +import { StepStoreKeys } from "@/types/components/hotelReservation/enterDetails/step" import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import { StepEnum } from "@/types/enums/step" export default function SectionAccordion({ header, @@ -24,12 +22,12 @@ export default function SectionAccordion({ children, }: React.PropsWithChildren<SectionAccordionProps>) { const intl = useIntl() - const currentStep = useEnterDetailsStore((state) => state.currentStep) + const currentStep = useStepsStore((state) => state.currentStep) const [isComplete, setIsComplete] = useState(false) const [isOpen, setIsOpen] = useState(false) - const isValid = useEnterDetailsStore((state) => state.isValid[step]) - const navigate = useEnterDetailsStore((state) => state.navigate) - const stepData = useEnterDetailsStore((state) => state.userData) + const isValid = useDetailsStore((state) => state.isValid[step]) + const navigate = useStepsStore((state) => state.navigate) + const stepData = useDetailsStore((state) => state.data) const stepStoreKey = StepStoreKeys[step] const [title, setTitle] = useState(label) @@ -39,9 +37,12 @@ export default function SectionAccordion({ value && setTitle(value.description) } // If breakfast step, check if an option has been selected - if (step === StepEnum.breakfast && stepData.breakfast) { + if ( + step === StepEnum.breakfast && + (stepData.breakfast || stepData.breakfast === false) + ) { const value = stepData.breakfast - if (value === BreakfastPackageEnum.NO_BREAKFAST) { + if (value === false) { setTitle(intl.formatMessage({ id: "No breakfast" })) } else { setTitle(intl.formatMessage({ id: "Breakfast buffet" })) @@ -94,7 +95,9 @@ export default function SectionAccordion({ )} </button> </header> - <div className={styles.content}>{children}</div> + <div className={styles.content}> + <div className={styles.contentWrapper}>{children}</div> + </div> </div> </section> ) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index fc3de1764..ed91cb9e2 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -31,7 +31,6 @@ .main { display: grid; - gap: var(--Spacing-x3); width: 100%; border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); padding-bottom: var(--Spacing-x3); @@ -80,6 +79,10 @@ overflow: hidden; } +.contentWrapper { + padding-top: var(--Spacing-x3); +} + @media screen and (min-width: 1367px) { .wrapper { gap: var(--Spacing-x3); @@ -98,4 +101,4 @@ content: ""; border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } -} +} \ No newline at end of file diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx index 64e9e0960..9d373f871 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx +++ b/components/HotelReservation/EnterDetails/SelectedRoom/index.tsx @@ -2,12 +2,13 @@ import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { selectRate } from "@/constants/routes/hotelReservation" import { CheckIcon, EditIcon } from "@/components/Icons" import Link from "@/components/TempDesignSystem/Link" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" import ToggleSidePeek from "./ToggleSidePeek" @@ -21,8 +22,7 @@ export default function SelectedRoom({ rateDescription, }: SelectedRoomProps) { const intl = useIntl() - - const selectRateUrl = useEnterDetailsStore((state) => state.selectRateUrl) + const lang = useLang() return ( <div className={styles.wrapper}> @@ -53,7 +53,8 @@ export default function SelectedRoom({ <Link className={styles.button} color="burgundy" - href={selectRateUrl} + href={selectRate(lang)} + keepSearchParams size="small" variant="icon" > diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx index ac7921aec..9f99a56c0 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx @@ -3,7 +3,7 @@ import { PropsWithChildren } from "react" import { useIntl } from "react-intl" -import { useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -17,9 +17,9 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) { const intl = useIntl() const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } = - useEnterDetailsStore((state) => ({ + useDetailsStore((state) => ({ isSummaryOpen: state.isSummaryOpen, - toggleSummaryOpen: state.toggleSummaryOpen, + toggleSummaryOpen: state.actions.toggleSummaryOpen, totalPrice: state.totalPrice, isSubmittingDisabled: state.isSubmittingDisabled, })) diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 4b093f8ea..c447c7f75 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -5,7 +5,7 @@ import { ChevronDown } from "react-feather" import { useIntl } from "react-intl" import { dt } from "@/lib/dt" -import { EnterDetailsState, useEnterDetailsStore } from "@/stores/enter-details" +import { useDetailsStore } from "@/stores/details" import { ArrowRightIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" @@ -18,45 +18,39 @@ import useLang from "@/hooks/useLang" import styles from "./summary.module.css" -import { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData" -import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import type { SummaryProps } from "@/types/components/hotelReservation/enterDetails/summary" +import type { DetailsState } from "@/types/stores/details" -function storeSelector(state: EnterDetailsState) { +function storeSelector(state: DetailsState) { return { - fromDate: state.roomData.fromDate, - toDate: state.roomData.toDate, - bedType: state.userData.bedType, - breakfast: state.userData.breakfast, - toggleSummaryOpen: state.toggleSummaryOpen, - setTotalPrice: state.setTotalPrice, + fromDate: state.data.booking.fromDate, + toDate: state.data.booking.toDate, + bedType: state.data.bedType, + breakfast: state.data.breakfast, + toggleSummaryOpen: state.actions.toggleSummaryOpen, + setTotalPrice: state.actions.setTotalPrice, totalPrice: state.totalPrice, } } -export default function Summary({ - showMemberPrice, - room, -}: { - showMemberPrice: boolean - room: RoomsData -}) { +export default function Summary({ showMemberPrice, room }: SummaryProps) { const [chosenBed, setChosenBed] = useState<BedTypeSchema>() const [chosenBreakfast, setChosenBreakfast] = useState< - BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST + BreakfastPackage | false >() const intl = useIntl() const lang = useLang() const { - fromDate, - toDate, bedType, breakfast, + fromDate, setTotalPrice, - totalPrice, + toDate, toggleSummaryOpen, - } = useEnterDetailsStore(storeSelector) + totalPrice, + } = useDetailsStore(storeSelector) const diff = dt(toDate).diff(fromDate, "days") @@ -88,36 +82,39 @@ export default function Summary({ setChosenBed(bedType) setChosenBreakfast(breakfast) - if (breakfast && breakfast !== BreakfastPackageEnum.NO_BREAKFAST) { - setTotalPrice({ - local: { - price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice), - currency: room.localPrice.currency, - }, - euro: - room.euroPrice && roomsPriceEuro - ? { + if (breakfast || breakfast === false) { + setChosenBreakfast(breakfast) + if (breakfast === false) { + setTotalPrice({ + local: { + price: roomsPriceLocal, + currency: room.localPrice.currency, + }, + euro: + room.euroPrice && roomsPriceEuro + ? { + price: roomsPriceEuro, + currency: room.euroPrice.currency, + } + : undefined, + }) + } else { + setTotalPrice({ + local: { + price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice), + currency: room.localPrice.currency, + }, + euro: + room.euroPrice && roomsPriceEuro + ? { price: roomsPriceEuro + parseInt(breakfast.requestedPrice.totalPrice), currency: room.euroPrice.currency, } - : undefined, - }) - } else { - setTotalPrice({ - local: { - price: roomsPriceLocal, - currency: room.localPrice.currency, - }, - euro: - room.euroPrice && roomsPriceEuro - ? { - price: roomsPriceEuro, - currency: room.euroPrice.currency, - } - : undefined, - }) + : undefined, + }) + } } }, [ bedType, @@ -187,24 +184,24 @@ export default function Summary({ </div> {room.packages ? room.packages.map((roomPackage) => ( - <div className={styles.entry} key={roomPackage.code}> - <div> - <Body color="uiTextHighContrast"> - {roomPackage.description} - </Body> - </div> - - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: roomPackage.localPrice.price, - currency: roomPackage.localPrice.currency, - } - )} - </Caption> + <div className={styles.entry} key={roomPackage.code}> + <div> + <Body color="uiTextHighContrast"> + {roomPackage.description} + </Body> </div> - )) + + <Caption color="uiTextHighContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: roomPackage.localPrice.price, + currency: roomPackage.localPrice.currency, + } + )} + </Caption> + </div> + )) : null} {chosenBed ? ( <div className={styles.entry}> @@ -224,37 +221,36 @@ export default function Summary({ </div> ) : null} - {chosenBreakfast ? ( - chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? ( - <div className={styles.entry}> - <Body color="uiTextHighContrast"> - {intl.formatMessage({ id: "No breakfast" })} - </Body> - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "0", currency: room.localPrice.currency } - )} - </Caption> - </div> - ) : ( - <div className={styles.entry}> - <Body color="uiTextHighContrast"> - {intl.formatMessage({ id: "Breakfast buffet" })} - </Body> - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: chosenBreakfast.localPrice.totalPrice, - currency: chosenBreakfast.localPrice.currency, - } - )} - </Caption> - </div> - ) - ) : null} - </div> + {chosenBreakfast === false ? ( + <div className={styles.entry}> + <Body color="uiTextHighContrast"> + {intl.formatMessage({ id: "No breakfast" })} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: room.localPrice.currency } + )} + </Caption> + </div> + ) : chosenBreakfast?.code ? ( + <div className={styles.entry}> + <Body color="uiTextHighContrast"> + {intl.formatMessage({ id: "Breakfast buffet" })} + </Body> + <Caption color="uiTextMediumContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: chosenBreakfast.localPrice.totalPrice, + currency: chosenBreakfast.localPrice.currency, + } + )} + </Caption> + </div> + ) : null + } + </div > <Divider color="primaryLightSubtle" /> <div className={styles.total}> <div className={styles.entry}> @@ -295,6 +291,6 @@ export default function Summary({ </div> <Divider className={styles.bottomDivider} color="primaryLightSubtle" /> </div> - </section> + </section > ) } diff --git a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx index 1f55c2d8a..d98fdc260 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/index.tsx +++ b/components/HotelReservation/HotelCard/HotelPriceList/index.tsx @@ -39,7 +39,7 @@ export default function HotelPriceList({ className={styles.button} > <Link - href={`${selectRate[lang]}?hotel=${hotelId}`} + href={`${selectRate(lang)}?hotel=${hotelId}`} color="none" keepSearchParams > diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index feda19a05..808e9ac9f 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -93,7 +93,7 @@ export default function HotelCard({ </address> <Link className={styles.addressMobile} - href={`${selectHotelMap[lang]}?selectedHotel=${hotelData.name}`} + href={`${selectHotelMap(lang)}?selectedHotel=${hotelData.name}`} keepSearchParams > <Caption color="baseTextMediumContrast" type="underline"> diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index 16d1ce860..d444a1083 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -104,7 +104,7 @@ export default function HotelCardDialog({ <Button asChild theme="base" size="small" className={styles.button}> <Link - href={`${selectRate[lang]}?hotel=${data.operaId}`} + href={`${selectRate(lang)}?hotel=${data.operaId}`} color="none" keepSearchParams > diff --git a/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx b/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx index 558a3ef50..0b4881943 100644 --- a/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx +++ b/components/HotelReservation/SelectHotel/MobileMapButtonContainer/index.tsx @@ -27,7 +27,7 @@ export default function MobileMapButtonContainer({ <div className={styles.buttonContainer}> <Button asChild variant="icon" intent="secondary" size="small"> <Link - href={`${selectHotelMap[lang]}`} + href={selectHotelMap(lang)} keepSearchParams color="burgundy" > diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 21b804306..215c7ae66 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -71,7 +71,7 @@ export default function SelectHotelMap({ } function handlePageRedirect() { - router.push(`${selectHotel[lang]}?${searchParams.toString()}`) + router.push(`${selectHotel(lang)}?${searchParams.toString()}`) } const closeButton = ( diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index aa6ef2810..43af470e3 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -53,26 +53,3 @@ export function getQueryParamsForEnterDetails( })), } } - -export function createSelectRateUrl(roomData: BookingData) { - const { hotel, fromDate, toDate } = roomData - const params = new URLSearchParams({ fromDate, toDate, hotel }) - - roomData.rooms.forEach((room, index) => { - params.set(`room[${index}].adults`, room.adults.toString()) - - if (room.children) { - room.children.forEach((child, childIndex) => { - params.set( - `room[${index}].child[${childIndex}].age`, - child.age.toString() - ) - params.set( - `room[${index}].child[${childIndex}].bed`, - child.bed.toString() - ) - }) - } - }) - return params -} diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index 2a3faf57b..7d6ef8105 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -15,6 +15,7 @@ export default function Card({ iconHeight = 32, iconWidth = 32, declined = false, + defaultChecked, highlightSubtitle = false, id, list, @@ -45,6 +46,7 @@ export default function Card({ <input {...register(name)} aria-hidden + defaultChecked={defaultChecked} id={id || name} hidden type={type} diff --git a/components/TempDesignSystem/Select/index.tsx b/components/TempDesignSystem/Select/index.tsx index bce5132ab..816d8ddb9 100644 --- a/components/TempDesignSystem/Select/index.tsx +++ b/components/TempDesignSystem/Select/index.tsx @@ -12,6 +12,7 @@ import { import Label from "@/components/TempDesignSystem/Form/Label" import Body from "@/components/TempDesignSystem/Text/Body" +import useSetOverflowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA" import SelectChevron from "../Form/SelectChevron" @@ -39,6 +40,7 @@ export default function Select({ discreet = false, }: SelectProps) { const [rootDiv, setRootDiv] = useState<SelectPortalContainer>(undefined) + const setOverflowVisible = useSetOverflowVisibleOnRA() function setRef(node: SelectPortalContainerArgs) { if (node) { @@ -60,6 +62,7 @@ export default function Select({ onSelectionChange={handleOnSelect} placeholder={placeholder} selectedKey={value as Key} + onOpenChange={setOverflowVisible} > <Body asChild fontOnly> <Button className={styles.input} data-testid={name}> diff --git a/constants/routes/hotelReservation.js b/constants/routes/hotelReservation.js index 94a9cef18..c7882f159 100644 --- a/constants/routes/hotelReservation.js +++ b/constants/routes/hotelReservation.js @@ -1,97 +1,59 @@ -/** @type {import('@/types/routes').LangRoute} */ -export const hotelReservation = { - en: "/en/hotelreservation", - sv: "/sv/hotelreservation", - no: "/no/hotelreservation", - fi: "/fi/hotelreservation", - da: "/da/hotelreservation", - de: "/de/hotelreservation", +/** + * @typedef {import('@/constants/languages').Lang} Lang + */ + +/** + * @param {Lang} lang + */ +function base(lang) { + return `/${lang}/hotelreservation` } -export const selectHotel = { - en: `${hotelReservation.en}/select-hotel`, - sv: `${hotelReservation.sv}/select-hotel`, - no: `${hotelReservation.no}/select-hotel`, - fi: `${hotelReservation.fi}/select-hotel`, - da: `${hotelReservation.da}/select-hotel`, - de: `${hotelReservation.de}/select-hotel`, +/** + * @param {Lang} lang + */ +export function bookingConfirmation(lang) { + return `${base(lang)}/booking-confirmation` } -export const selectRate = { - en: `${hotelReservation.en}/select-rate`, - sv: `${hotelReservation.sv}/select-rate`, - no: `${hotelReservation.no}/select-rate`, - fi: `${hotelReservation.fi}/select-rate`, - da: `${hotelReservation.da}/select-rate`, - de: `${hotelReservation.de}/select-rate`, +/** + * @param {Lang} lang + */ +export function details(lang) { + return `${base(lang)}/details` } -// TODO: Translate paths -export const selectBed = { - en: `${hotelReservation.en}/select-bed`, - sv: `${hotelReservation.sv}/select-bed`, - no: `${hotelReservation.no}/select-bed`, - fi: `${hotelReservation.fi}/select-bed`, - da: `${hotelReservation.da}/select-bed`, - de: `${hotelReservation.de}/select-bed`, +/** + * @param {Lang} lang + */ +export function payment(lang) { + return `${base(lang)}/payment` } -// TODO: Translate paths -export const breakfast = { - en: `${hotelReservation.en}/breakfast`, - sv: `${hotelReservation.sv}/breakfast`, - no: `${hotelReservation.no}/breakfast`, - fi: `${hotelReservation.fi}/breakfast`, - da: `${hotelReservation.da}/breakfast`, - de: `${hotelReservation.de}/breakfast`, +/** + * @param {Lang} lang + */ +export function selectBed(lang) { + return `${base(lang)}/select-bed` } -// TODO: Translate paths -export const details = { - en: `${hotelReservation.en}/details`, - sv: `${hotelReservation.sv}/details`, - no: `${hotelReservation.no}/details`, - fi: `${hotelReservation.fi}/details`, - da: `${hotelReservation.da}/details`, - de: `${hotelReservation.de}/details`, +/** + * @param {Lang} lang + */ +export function selectHotel(lang) { + return `${base(lang)}/select-hotel` } -// TODO: Translate paths -export const payment = { - en: `${hotelReservation.en}/payment`, - sv: `${hotelReservation.sv}/payment`, - no: `${hotelReservation.no}/payment`, - fi: `${hotelReservation.fi}/payment`, - da: `${hotelReservation.da}/payment`, - de: `${hotelReservation.de}/payment`, +/** + * @param {Lang} lang + */ +export function selectHotelMap(lang) { + return `${base(lang)}/map` } -export const selectHotelMap = { - en: `${selectHotel.en}/map`, - sv: `${selectHotel.sv}/map`, - no: `${selectHotel.no}/map`, - fi: `${selectHotel.fi}/map`, - da: `${selectHotel.da}/map`, - de: `${selectHotel.de}/map`, +/** + * @param {Lang} lang + */ +export function selectRate(lang) { + return `${base(lang)}/select-rate` } - -/** @type {import('@/types/routes').LangRoute} */ -export const bookingConfirmation = { - en: `${hotelReservation.en}/booking-confirmation`, - sv: `${hotelReservation.sv}/booking-confirmation`, - no: `${hotelReservation.no}/booking-confirmation`, - fi: `${hotelReservation.fi}/booking-confirmation`, - da: `${hotelReservation.da}/booking-confirmation`, - de: `${hotelReservation.de}/booking-confirmation`, -} - -export const bookingFlow = [ - ...Object.values(selectHotel), - ...Object.values(selectBed), - ...Object.values(breakfast), - ...Object.values(details), - ...Object.values(payment), - ...Object.values(selectHotelMap), - ...Object.values(bookingConfirmation), - ...Object.values(selectRate), -] diff --git a/contexts/Details.ts b/contexts/Details.ts new file mode 100644 index 000000000..7fb3a010a --- /dev/null +++ b/contexts/Details.ts @@ -0,0 +1,5 @@ +import { createContext } from "react" + +import type { DetailsStore } from "@/types/contexts/details" + +export const DetailsContext = createContext<DetailsStore | null>(null) diff --git a/contexts/Steps.ts b/contexts/Steps.ts new file mode 100644 index 000000000..220365fbe --- /dev/null +++ b/contexts/Steps.ts @@ -0,0 +1,5 @@ +import { createContext } from "react" + +import type { StepsStore } from "@/types/contexts/steps" + +export const StepsContext = createContext<StepsStore | null>(null) diff --git a/hooks/useSetOverflowVisibleOnRA.ts b/hooks/useSetOverflowVisibleOnRA.ts new file mode 100644 index 000000000..e9031b477 --- /dev/null +++ b/hooks/useSetOverflowVisibleOnRA.ts @@ -0,0 +1,11 @@ +export default function useSetOverflowVisibleOnRA() { + function setOverflowVisible(isOpen: boolean) { + if (isOpen) { + document.body.style.overflow = "visible" + } else { + document.body.style.overflow = "" + } + } + + return setOverflowVisible +} diff --git a/middlewares/bookingFlow.ts b/middlewares/bookingFlow.ts index 098ca9108..20e646e5a 100644 --- a/middlewares/bookingFlow.ts +++ b/middlewares/bookingFlow.ts @@ -1,7 +1,5 @@ import { NextResponse } from "next/server" -import { bookingFlow } from "@/constants/routes/hotelReservation" - import { getDefaultRequestHeaders } from "./utils" import type { NextMiddleware } from "next/server" @@ -18,5 +16,7 @@ export const middleware: NextMiddleware = async (request) => { } export const matcher: MiddlewareMatcher = (request) => { - return bookingFlow.includes(request.nextUrl.pathname) + return !!request.nextUrl.pathname.match( + /^\/(da|de|en|fi|no|sv)\/(hotelreservation)/ + ) } diff --git a/next.config.js b/next.config.js index 29012f230..222f085ac 100644 --- a/next.config.js +++ b/next.config.js @@ -277,6 +277,11 @@ const nextConfig = { source: `${myPages.sv}/:path*`, destination: `/sv/my-pages/:path*`, }, + { + source: + "/:lang/hotelreservation/:step(breakfast|details|payment|select-bed)", + destination: "/:lang/hotelreservation/step?step=:step", + }, ], } }, diff --git a/providers/DetailsProvider.tsx b/providers/DetailsProvider.tsx new file mode 100644 index 000000000..328307ee7 --- /dev/null +++ b/providers/DetailsProvider.tsx @@ -0,0 +1,30 @@ +"use client" +import { useSearchParams } from "next/navigation" +import { useRef } from "react" + +import { createDetailsStore } from "@/stores/details" + +import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" +import { DetailsContext } from "@/contexts/Details" + +import type { DetailsStore } from "@/types/contexts/details" +import type { DetailsProviderProps } from "@/types/providers/details" + +export default function DetailsProvider({ + children, + isMember, +}: DetailsProviderProps) { + const storeRef = useRef<DetailsStore>() + const searchParams = useSearchParams() + + if (!storeRef.current) { + const booking = getQueryParamsForEnterDetails(searchParams) + storeRef.current = createDetailsStore({ booking }, isMember) + } + + return ( + <DetailsContext.Provider value={storeRef.current}> + {children} + </DetailsContext.Provider> + ) +} diff --git a/providers/StepsProvider.tsx b/providers/StepsProvider.tsx new file mode 100644 index 000000000..87594be02 --- /dev/null +++ b/providers/StepsProvider.tsx @@ -0,0 +1,53 @@ +"use client" +import { useRef } from "react" + +import { useDetailsStore } from "@/stores/details" +import { createStepsStore } from "@/stores/steps" + +import { StepsContext } from "@/contexts/Steps" + +import type { StepsStore } from "@/types/contexts/steps" +import type { StepsProviderProps } from "@/types/providers/steps" + +export default function StepsProvider({ + bedTypes, + breakfastPackages, + children, + isMember, + step, +}: StepsProviderProps) { + const storeRef = useRef<StepsStore>() + const updateBedType = useDetailsStore((state) => state.actions.updateBedType) + const updateBreakfast = useDetailsStore( + (state) => state.actions.updateBreakfast + ) + + if (!storeRef.current) { + const noBedChoices = bedTypes.length === 1 + const noBreakfast = !breakfastPackages?.length + + if (noBedChoices) { + updateBedType({ + description: bedTypes[0].description, + roomTypeCode: bedTypes[0].value, + }) + } + + if (noBreakfast) { + updateBreakfast(false) + } + + storeRef.current = createStepsStore( + step, + isMember, + noBedChoices, + noBreakfast + ) + } + + return ( + <StepsContext.Provider value={storeRef.current}> + {children} + </StepsContext.Provider> + ) +} diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts new file mode 100644 index 000000000..988cf1ac5 --- /dev/null +++ b/server/routers/hotels/schemas/packages.ts @@ -0,0 +1,62 @@ +import { z } from "zod" + +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { CurrencyEnum } from "@/types/enums/currency" + +export const getRoomPackagesInputSchema = z.object({ + hotelId: z.string(), + startDate: z.string(), + endDate: z.string(), + adults: z.number(), + children: z.number().optional().default(0), + packageCodes: z.array(z.string()).optional().default([]), +}) + +export const packagePriceSchema = z + .object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.string(), + totalPrice: z.string(), + }) + .optional() + .default({ + currency: CurrencyEnum.SEK, + price: "0", + totalPrice: "0", + }) // TODO: Remove optional and default when the API change has been deployed + +export const packagesSchema = z.object({ + code: z.nativeEnum(RoomPackageCodeEnum), + description: z.string(), + localPrice: packagePriceSchema, + requestedPrice: packagePriceSchema, + inventories: z.array( + z.object({ + date: z.string(), + total: z.number(), + available: z.number(), + }) + ), +}) + +export const getRoomPackagesSchema = z + .object({ + data: z.object({ + attributes: z.object({ + hotelId: z.number(), + packages: z.array(packagesSchema).default([]), + }), + relationships: z + .object({ + links: z.array( + z.object({ + url: z.string(), + type: z.string(), + }) + ), + }) + .optional(), + type: z.string(), + }), + }) + .transform((data) => data.data.attributes.packages) diff --git a/stores/details.ts b/stores/details.ts new file mode 100644 index 000000000..5d23248e5 --- /dev/null +++ b/stores/details.ts @@ -0,0 +1,195 @@ +import merge from "deepmerge" +import { produce } from "immer" +import { useContext } from "react" +import { create, useStore } from "zustand" +import { createJSONStorage, persist } from "zustand/middleware" + +import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" +import { DetailsContext } from "@/contexts/Details" + +import { StepEnum } from "@/types/enums/step" +import type { DetailsState, InitialState } from "@/types/stores/details" + +export const storageName = "details-storage" +export function createDetailsStore( + initialState: InitialState, + isMember: boolean +) { + if (typeof window !== "undefined") { + /** + * We need to initialize the store from sessionStorage ourselves + * since `persist` does it first after render and therefore + * we cannot use the data as `defaultValues` for our forms. + * RHF caches defaultValues on mount. + */ + const detailsStorageUnparsed = sessionStorage.getItem(storageName) + if (detailsStorageUnparsed) { + const detailsStorage: Record< + "state", + Pick<DetailsState, "data"> + > = JSON.parse(detailsStorageUnparsed) + initialState = merge(initialState, detailsStorage.state.data) + } + } + return create<DetailsState>()( + persist( + (set) => ({ + actions: { + setIsSubmittingDisabled(isSubmittingDisabled) { + return set( + produce((state: DetailsState) => { + state.isSubmittingDisabled = isSubmittingDisabled + }) + ) + }, + setTotalPrice(totalPrice) { + return set( + produce((state: DetailsState) => { + state.totalPrice = totalPrice + }) + ) + }, + toggleSummaryOpen() { + return set( + produce((state: DetailsState) => { + state.isSummaryOpen = !state.isSummaryOpen + }) + ) + }, + updateBedType(bedType) { + return set( + produce((state: DetailsState) => { + state.isValid["select-bed"] = true + state.data.bedType = bedType + }) + ) + }, + updateBreakfast(breakfast) { + return set( + produce((state: DetailsState) => { + state.isValid.breakfast = true + state.data.breakfast = breakfast + }) + ) + }, + updateDetails(data) { + return set( + produce((state: DetailsState) => { + state.isValid.details = true + + state.data.countryCode = data.countryCode + state.data.dateOfBirth = data.dateOfBirth + state.data.email = data.email + state.data.firstName = data.firstName + state.data.join = data.join + state.data.lastName = data.lastName + if (data.join) { + state.data.membershipNo = undefined + } else { + state.data.membershipNo = data.membershipNo + } + state.data.phoneNumber = data.phoneNumber + state.data.termsAccepted = data.termsAccepted + state.data.zipCode = data.zipCode + }) + ) + }, + updateValidity(property, isValid) { + return set( + produce((state: DetailsState) => { + state.isValid[property] = isValid + }) + ) + }, + }, + + data: merge( + { + bedType: undefined, + breakfast: undefined, + countryCode: "", + dateOfBirth: "", + email: "", + firstName: "", + join: false, + lastName: "", + membershipNo: "", + phoneNumber: "", + termsAccepted: false, + zipCode: "", + }, + initialState + ), + + isSubmittingDisabled: false, + isSummaryOpen: false, + isValid: { + [StepEnum.selectBed]: false, + [StepEnum.breakfast]: false, + [StepEnum.details]: false, + [StepEnum.payment]: false, + }, + + totalPrice: { + euro: { currency: "", price: 0 }, + local: { currency: "", price: 0 }, + }, + }), + { + name: storageName, + onRehydrateStorage() { + return function (state) { + if (state) { + const validatedBedType = bedTypeSchema.safeParse(state.data) + if (validatedBedType.success) { + state.actions.updateValidity(StepEnum.selectBed, true) + } else { + state.actions.updateValidity(StepEnum.selectBed, false) + } + + const validatedBreakfast = breakfastStoreSchema.safeParse( + state.data + ) + if (validatedBreakfast.success) { + state.actions.updateValidity(StepEnum.breakfast, true) + } else { + state.actions.updateValidity(StepEnum.breakfast, false) + } + + const detailsSchema = isMember + ? signedInDetailsSchema + : guestDetailsSchema + const validatedDetails = detailsSchema.safeParse(state.data) + if (validatedDetails.success) { + state.actions.updateValidity(StepEnum.details, true) + } else { + state.actions.updateValidity(StepEnum.details, false) + } + } + } + }, + partialize(state) { + return { + data: state.data, + } + }, + storage: createJSONStorage(() => sessionStorage), + } + ) + ) +} + +export function useDetailsStore<T>(selector: (store: DetailsState) => T) { + const store = useContext(DetailsContext) + + if (!store) { + throw new Error("useDetailsStore must be used within DetailsProvider") + } + + return useStore(store, selector) +} diff --git a/stores/enter-details.ts b/stores/enter-details.ts deleted file mode 100644 index b5f99cc38..000000000 --- a/stores/enter-details.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { produce } from "immer" -import { ReadonlyURLSearchParams } from "next/navigation" -import { createContext, useContext } from "react" -import { create, useStore } from "zustand" - -import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" -import { - guestDetailsSchema, - signedInDetailsSchema, -} from "@/components/HotelReservation/EnterDetails/Details/schema" -import { - createSelectRateUrl, - getQueryParamsForEnterDetails, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" - -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" -import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" -import { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" -import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step" -import { BreakfastPackageEnum } from "@/types/enums/breakfast" - -const SESSION_STORAGE_KEY = "enterDetails" - -type TotalPrice = { - local: { price: number; currency: string } - euro?: { price: number; currency: string } -} - -export interface EnterDetailsState { - userData: { - bedType: BedTypeSchema | undefined - breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined - } & DetailsSchema - roomData: BookingData - steps: StepEnum[] - selectRateUrl: string - currentStep: StepEnum - totalPrice: TotalPrice - isSubmittingDisabled: boolean - isSummaryOpen: boolean - isValid: Record<StepEnum, boolean> - completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void - navigate: ( - step: StepEnum, - updatedData?: Record< - string, - string | boolean | number | BreakfastPackage | BedTypeSchema - > - ) => void - setCurrentStep: (step: StepEnum) => void - toggleSummaryOpen: () => void - setTotalPrice: (totalPrice: TotalPrice) => void - setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void -} - -export function initEditDetailsState( - currentStep: StepEnum, - searchParams: ReadonlyURLSearchParams, - isMember: boolean -) { - const isBrowser = typeof window !== "undefined" - const sessionData = isBrowser - ? sessionStorage.getItem(SESSION_STORAGE_KEY) - : null - - let roomData: BookingData - let selectRateUrl: string - if (searchParams?.size) { - const data = getQueryParamsForEnterDetails(searchParams) - roomData = data - selectRateUrl = `select-rate?${createSelectRateUrl(data)}` - } - - const defaultUserData: EnterDetailsState["userData"] = { - bedType: undefined, - breakfast: undefined, - countryCode: "", - email: "", - firstName: "", - lastName: "", - phoneNumber: "", - join: false, - zipCode: "", - dateOfBirth: undefined, - termsAccepted: false, - membershipNo: "", - } - - let inputUserData = {} - if (sessionData) { - inputUserData = JSON.parse(sessionData) - } - - const validPaths = [StepEnum.selectBed] - - let initialData: EnterDetailsState["userData"] = defaultUserData - - const isValid = { - [StepEnum.selectBed]: false, - [StepEnum.breakfast]: false, - [StepEnum.details]: false, - [StepEnum.payment]: false, - } - - const validatedBedType = bedTypeSchema.safeParse(inputUserData) - if (validatedBedType.success) { - validPaths.push(StepEnum.breakfast) - initialData = { ...initialData, ...validatedBedType.data } - isValid[StepEnum.selectBed] = true - } - const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData) - if (validatedBreakfast.success) { - validPaths.push(StepEnum.details) - initialData = { ...initialData, ...validatedBreakfast.data } - isValid[StepEnum.breakfast] = true - } - const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema - const validatedDetails = detailsSchema.safeParse(inputUserData) - if (validatedDetails.success) { - validPaths.push(StepEnum.payment) - initialData = { ...initialData, ...validatedDetails.data } - isValid[StepEnum.details] = true - } - - if (!validPaths.includes(currentStep)) { - currentStep = validPaths.pop()! // We will always have at least one valid path - if (isBrowser) { - window.history.pushState( - { step: currentStep }, - "", - currentStep + window.location.search - ) - } - } - - return create<EnterDetailsState>()((set, get) => ({ - userData: initialData, - roomData, - selectRateUrl, - steps: Object.values(StepEnum), - totalPrice: { - local: { price: 0, currency: "" }, - euro: { price: 0, currency: "" }, - }, - isSummaryOpen: false, - isSubmittingDisabled: false, - setCurrentStep: (step) => set({ currentStep: step }), - navigate: (step, updatedData) => - set( - produce((state) => { - const sessionStorage = window.sessionStorage - - const previousDataString = sessionStorage.getItem(SESSION_STORAGE_KEY) - - const previousData = JSON.parse(previousDataString || "{}") - - sessionStorage.setItem( - SESSION_STORAGE_KEY, - JSON.stringify({ ...previousData, ...updatedData }) - ) - - state.currentStep = step - window.history.pushState({ step }, "", step + window.location.search) - }) - ), - currentStep, - isValid, - completeStep: (updatedData) => - set( - produce((state: EnterDetailsState) => { - state.isValid[state.currentStep] = true - - const nextStep = - state.steps[state.steps.indexOf(state.currentStep) + 1] - - state.userData = { - ...state.userData, - ...updatedData, - } - state.currentStep = nextStep - get().navigate(nextStep, updatedData) - }) - ), - toggleSummaryOpen: () => set({ isSummaryOpen: !get().isSummaryOpen }), - setTotalPrice: (totalPrice) => set({ totalPrice: totalPrice }), - setIsSubmittingDisabled: (isSubmittingDisabled) => - set({ isSubmittingDisabled }), - })) -} - -export type EnterDetailsStore = ReturnType<typeof initEditDetailsState> - -export const EnterDetailsContext = createContext<EnterDetailsStore | null>(null) - -export const useEnterDetailsStore = <T>( - selector: (store: EnterDetailsState) => T -): T => { - const enterDetailsContextStore = useContext(EnterDetailsContext) - - if (!enterDetailsContextStore) { - throw new Error( - `useEnterDetailsStore must be used within EnterDetailsContextProvider` - ) - } - - return useStore(enterDetailsContextStore, selector) -} diff --git a/stores/steps.ts b/stores/steps.ts new file mode 100644 index 000000000..f1e456af2 --- /dev/null +++ b/stores/steps.ts @@ -0,0 +1,159 @@ +"use client" +import merge from "deepmerge" +import { produce } from "immer" +import { useContext } from "react" +import { create, useStore } from "zustand" + +import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" +import { StepsContext } from "@/contexts/Steps" + +import { storageName as detailsStorageName } from "./details" + +import { StepEnum } from "@/types/enums/step" +import type { DetailsState } from "@/types/stores/details" +import type { StepState } from "@/types/stores/steps" + +function push(data: Record<string, string>, url: string) { + if (typeof window !== "undefined") { + window.history.pushState(data, "", url + window.location.search) + } +} + +export function createStepsStore( + currentStep: StepEnum, + isMember: boolean, + noBedChoices: boolean, + noBreakfast: boolean +) { + const isBrowser = typeof window !== "undefined" + const steps = [ + StepEnum.selectBed, + StepEnum.breakfast, + StepEnum.details, + StepEnum.payment, + ] + + /** + * TODO: + * - when included in rate, can packages still be received? + * - no hotels yet with breakfast included in the rate so + * impossible to build for atm. + * + * matching breakfast first so the steps array is altered + * before the bedTypes possible step altering + */ + if (noBreakfast) { + steps.splice(1, 1) + if (currentStep === StepEnum.breakfast) { + currentStep = steps[1] + push({ step: currentStep }, currentStep) + } + } + + if (noBedChoices) { + if (currentStep === StepEnum.selectBed) { + currentStep = steps[1] + push({ step: currentStep }, currentStep) + } + } + + const detailsStorageUnparsed = isBrowser + ? sessionStorage.getItem(detailsStorageName) + : null + if (detailsStorageUnparsed) { + const detailsStorage: Record< + "state", + Pick<DetailsState, "data"> + > = JSON.parse(detailsStorageUnparsed) + + const validPaths = [StepEnum.selectBed] + + const validatedBedType = bedTypeSchema.safeParse(detailsStorage.state.data) + if (validatedBedType.success) { + validPaths.push(steps[1]) + } + + const validatedBreakfast = breakfastStoreSchema.safeParse( + detailsStorage.state.data + ) + if (validatedBreakfast.success) { + validPaths.push(StepEnum.details) + } + + const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema + const validatedDetails = detailsSchema.safeParse(detailsStorage.state.data) + if (validatedDetails.success) { + validPaths.push(StepEnum.payment) + } + + if (!validPaths.includes(currentStep) && isBrowser) { + // We will always have at least one valid path + currentStep = validPaths.pop()! + push({ step: currentStep }, currentStep) + } + } + + const initalData = { + currentStep, + steps, + } + + return create<StepState>()((set) => + merge( + { + currentStep: StepEnum.selectBed, + steps: [], + + completeStep() { + return set( + produce((state: StepState) => { + const currentStepIndex = state.steps.indexOf(state.currentStep) + const nextStep = state.steps[currentStepIndex + 1] + state.currentStep = nextStep + window.history.pushState( + { step: nextStep }, + "", + nextStep + window.location.search + ) + }) + ) + }, + navigate(step: StepEnum) { + return set( + produce((state) => { + state.currentStep = step + window.history.pushState( + { step }, + "", + step + window.location.search + ) + }) + ) + }, + setStep(step: StepEnum) { + return set( + produce((state: StepState) => { + state.currentStep = step + }) + ) + }, + }, + initalData + ) + ) +} + +export function useStepsStore<T>(selector: (store: StepState) => T) { + const store = useContext(StepsContext) + + if (!store) { + throw new Error(`useStepsStore must be used within StepsProvider`) + } + + return useStore(store, selector) +} diff --git a/types/components/hotelReservation/enterDetails/breakfast.ts b/types/components/hotelReservation/enterDetails/breakfast.ts index 21ba37bd0..283d85028 100644 --- a/types/components/hotelReservation/enterDetails/breakfast.ts +++ b/types/components/hotelReservation/enterDetails/breakfast.ts @@ -17,5 +17,5 @@ export interface BreakfastPackage extends z.output<typeof breakfastPackageSchema> {} export interface BreakfastProps { - packages: BreakfastPackages | null + packages: BreakfastPackages } diff --git a/types/components/hotelReservation/enterDetails/step.ts b/types/components/hotelReservation/enterDetails/step.ts index 45de5a009..8c8c967ef 100644 --- a/types/components/hotelReservation/enterDetails/step.ts +++ b/types/components/hotelReservation/enterDetails/step.ts @@ -1,9 +1,4 @@ -export enum StepEnum { - selectBed = "select-bed", - breakfast = "breakfast", - details = "details", - payment = "payment", -} +import { StepEnum } from "@/types/enums/step" export const StepStoreKeys: Record<StepEnum, "bedType" | "breakfast" | null> = { "select-bed": "bedType", diff --git a/types/components/hotelReservation/enterDetails/store.ts b/types/components/hotelReservation/enterDetails/store.ts deleted file mode 100644 index 45dbd5f75..000000000 --- a/types/components/hotelReservation/enterDetails/store.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { StepEnum } from "./step" - -export type EnterDetailsProviderProps = { step: StepEnum; isMember: boolean } diff --git a/types/components/hotelReservation/enterDetails/summary.ts b/types/components/hotelReservation/enterDetails/summary.ts new file mode 100644 index 000000000..901113414 --- /dev/null +++ b/types/components/hotelReservation/enterDetails/summary.ts @@ -0,0 +1,6 @@ +import type { RoomsData } from "./bookingData" + +export interface SummaryProps { + showMemberPrice: boolean + room: RoomsData +} diff --git a/types/components/hotelReservation/selectRate/sectionAccordion.ts b/types/components/hotelReservation/selectRate/sectionAccordion.ts index 46b194db3..c50207f3a 100644 --- a/types/components/hotelReservation/selectRate/sectionAccordion.ts +++ b/types/components/hotelReservation/selectRate/sectionAccordion.ts @@ -1,4 +1,4 @@ -import { StepEnum } from "../enterDetails/step" +import { StepEnum } from "@/types/enums/step" export interface SectionAccordionProps { header: string diff --git a/types/contexts/details.ts b/types/contexts/details.ts new file mode 100644 index 000000000..ea6b65edd --- /dev/null +++ b/types/contexts/details.ts @@ -0,0 +1,3 @@ +import { createDetailsStore } from "@/stores/details" + +export type DetailsStore = ReturnType<typeof createDetailsStore> diff --git a/types/contexts/steps.ts b/types/contexts/steps.ts new file mode 100644 index 000000000..40c3cb55e --- /dev/null +++ b/types/contexts/steps.ts @@ -0,0 +1,3 @@ +import { createStepsStore } from "@/stores/steps" + +export type StepsStore = ReturnType<typeof createStepsStore> diff --git a/types/enums/breakfast.ts b/types/enums/breakfast.ts index 81ff51a2e..723326c37 100644 --- a/types/enums/breakfast.ts +++ b/types/enums/breakfast.ts @@ -1,5 +1,4 @@ export enum BreakfastPackageEnum { FREE_MEMBER_BREAKFAST = "BRF0", REGULAR_BREAKFAST = "BRF1", - NO_BREAKFAST = "NO_BREAKFAST", } diff --git a/types/enums/step.ts b/types/enums/step.ts new file mode 100644 index 000000000..e52d3c856 --- /dev/null +++ b/types/enums/step.ts @@ -0,0 +1,6 @@ +export enum StepEnum { + selectBed = "select-bed", + breakfast = "breakfast", + details = "details", + payment = "payment", +} diff --git a/types/providers/details.ts b/types/providers/details.ts new file mode 100644 index 000000000..c58effb2c --- /dev/null +++ b/types/providers/details.ts @@ -0,0 +1,3 @@ +export interface DetailsProviderProps extends React.PropsWithChildren { + isMember: boolean +} diff --git a/types/providers/steps.ts b/types/providers/steps.ts new file mode 100644 index 000000000..8c24fdc8f --- /dev/null +++ b/types/providers/steps.ts @@ -0,0 +1,10 @@ +import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" +import { StepEnum } from "@/types/enums/step" +import type { BreakfastPackage } from "../components/hotelReservation/enterDetails/breakfast" + +export interface StepsProviderProps extends React.PropsWithChildren { + bedTypes: BedTypeSelection[] + breakfastPackages: BreakfastPackage[] | null + isMember: boolean + step: StepEnum +} diff --git a/types/stores/details.ts b/types/stores/details.ts new file mode 100644 index 000000000..ef6d101dc --- /dev/null +++ b/types/stores/details.ts @@ -0,0 +1,40 @@ +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" +import { StepEnum } from "@/types/enums/step" + +export interface DetailsState { + actions: { + setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void + setTotalPrice: (totalPrice: TotalPrice) => void + toggleSummaryOpen: () => void, + updateBedType: (data: BedTypeSchema) => void + updateBreakfast: (data: BreakfastPackage | false) => void + updateDetails: (data: DetailsSchema) => void + updateValidity: (property: StepEnum, isValid: boolean) => void + } + data: DetailsSchema & { + bedType: BedTypeSchema | undefined + breakfast: BreakfastPackage | false | undefined + booking: BookingData + } + isSubmittingDisabled: boolean + isSummaryOpen: boolean + isValid: Record<StepEnum, boolean> + totalPrice: TotalPrice +} + +export interface InitialState extends Partial<DetailsState> { + booking: BookingData +} + +interface Price { + currency: string + price: number +} + +export interface TotalPrice { + euro: Price | undefined + local: Price +} \ No newline at end of file diff --git a/types/stores/steps.ts b/types/stores/steps.ts new file mode 100644 index 000000000..bfdafdae7 --- /dev/null +++ b/types/stores/steps.ts @@ -0,0 +1,10 @@ +import { StepEnum } from "@/types/enums/step" + +export interface StepState { + completeStep: () => void + navigate: (step: StepEnum) => void + setStep: (step: StepEnum) => void + + currentStep: StepEnum + steps: StepEnum[] +} From 400f35516cb5bb2e296cffe1596cc029d9097434 Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Mon, 18 Nov 2024 17:30:55 +0000 Subject: [PATCH 040/101] Merged in fix/remove-filter-to-show-all-hotels (pull request #925) Fix/remove filter to show all hotels * fix: remove filter to show all hotels on select-hotel-page * fix: add missing translations Approved-by: Pontus Dreij --- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + server/routers/hotels/output.ts | 2 +- server/routers/hotels/query.ts | 9 +++------ server/routers/hotels/schemas/room.ts | 4 ++-- 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 076544355..336f5eef9 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -356,6 +356,7 @@ "This room is not available": "Dette værelse er ikke tilgængeligt", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "For at få medlemsprisen <span>{amount} {currency}</span>, log ind eller tilmeld dig, når du udfylder bookingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", + "Total": "Total", "Total Points": "Samlet antal point", "Total price": "Samlet pris", "Total price (incl VAT)": "Samlet pris (inkl. moms)", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 433a0ba33..78d9fd5cb 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -354,6 +354,7 @@ "This room is not available": "Dieses Zimmer ist nicht verfügbar", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "Um den Mitgliederpreis von <span>{amount} {currency}</span> zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", + "Total": "Gesamt", "Total Points": "Gesamtpunktzahl", "Total price": "Gesamtpreis", "Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 38a5da82b..cb8a071d9 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -384,6 +384,7 @@ "This room is not available": "This room is not available", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", + "Total": "Total", "Total Points": "Total Points", "Total cost": "Total cost", "Total price": "Total price", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 218abed13..48931f7f2 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -356,6 +356,7 @@ "This room is not available": "Tämä huone ei ole käytettävissä", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", + "Total": "Kokonais", "Total Points": "Kokonaispisteet", "Total price": "Kokonaishinta", "Total price (incl VAT)": "Kokonaishinta (sis. ALV)", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 7cf8e985a..313d0799d 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -353,6 +353,7 @@ "This room is not available": "Dette rommet er ikke tilgjengelig", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "For å få medlemsprisen <span>{amount} {currency}</span>, logg inn eller bli med når du fullfører bestillingen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.", + "Total": "Total", "Total Points": "Totale poeng", "Total incl VAT": "Sum inkl mva", "Total price": "Totalpris", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 7f935f88a..878bce352 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -353,6 +353,7 @@ "This room is not available": "Detta rum är inte tillgängligt", "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "För att få medlemsprisen <span>{amount} {currency}</span>, logga in eller bli medlem när du slutför bokningen.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.", + "Total": "Totalt", "Total Points": "Poäng totalt", "Total incl VAT": "Totalt inkl moms", "Total price": "Totalpris", diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index b68e87fc0..d4f3d89c9 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -105,7 +105,7 @@ const hotelContentSchema = z.object({ imageSizes: imageSizesSchema, }), texts: z.object({ - facilityInformation: z.string(), + facilityInformation: z.string().optional(), surroundingInformation: z.string(), descriptions: z.object({ short: z.string(), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index ea094de0f..af9460b7c 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -468,12 +468,9 @@ export const hotelQueryRouter = router({ }) ) return { - availability: validateAvailabilityData.data.data - .filter( - (hotels) => - hotels.attributes.status === AvailabilityEnum.Available - ) - .flatMap((hotels) => hotels.attributes), + availability: validateAvailabilityData.data.data.flatMap( + (hotels) => hotels.attributes + ), } }), rooms: serviceProcedure diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index 5a1480097..8b9291c1f 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -11,8 +11,8 @@ const roomContentSchema = z.object({ ), texts: z.object({ descriptions: z.object({ - short: z.string(), - medium: z.string(), + short: z.string().optional(), + medium: z.string().optional(), }), }), }) From d6e1e222ccee6379692c9333223a5d8fc0627a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= <bananashell@gmail.com> Date: Tue, 19 Nov 2024 08:04:17 +0100 Subject: [PATCH 041/101] fix: add missing loading.tsx and remove redundant ones --- .../[contentType]/[uid]/@breadcrumbs/loading.tsx | 5 ----- app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx | 1 - app/[lang]/(live)/@footer/[...path]/loading.tsx | 1 - app/[lang]/(live)/@header/[...path]/loading.tsx | 1 - app/[lang]/(live)/@header/loading.tsx | 5 ----- app/[lang]/(live)/@header/page.tsx | 9 ++++++++- app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx | 1 - 7 files changed, 8 insertions(+), 15 deletions(-) delete mode 100644 app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx delete mode 100644 app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx delete mode 100644 app/[lang]/(live)/@footer/[...path]/loading.tsx delete mode 100644 app/[lang]/(live)/@header/[...path]/loading.tsx delete mode 100644 app/[lang]/(live)/@header/loading.tsx delete mode 100644 app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx deleted file mode 100644 index aed94918c..000000000 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" - -export default function Loading() { - return <BreadcrumbsSkeleton /> -} diff --git a/app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx b/app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx deleted file mode 100644 index 1c031d2a1..000000000 --- a/app/[lang]/(live)/@bookingwidget/[...path]/loading.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../loading" diff --git a/app/[lang]/(live)/@footer/[...path]/loading.tsx b/app/[lang]/(live)/@footer/[...path]/loading.tsx deleted file mode 100644 index 1c031d2a1..000000000 --- a/app/[lang]/(live)/@footer/[...path]/loading.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../loading" diff --git a/app/[lang]/(live)/@header/[...path]/loading.tsx b/app/[lang]/(live)/@header/[...path]/loading.tsx deleted file mode 100644 index 1c031d2a1..000000000 --- a/app/[lang]/(live)/@header/[...path]/loading.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../loading" diff --git a/app/[lang]/(live)/@header/loading.tsx b/app/[lang]/(live)/@header/loading.tsx deleted file mode 100644 index a54009331..000000000 --- a/app/[lang]/(live)/@header/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import HeaderFallback from "@/components/Current/Header/HeaderFallback" - -export default function LoadingFooter() { - return <HeaderFallback /> -} diff --git a/app/[lang]/(live)/@header/page.tsx b/app/[lang]/(live)/@header/page.tsx index 9d777ffef..51e91035d 100644 --- a/app/[lang]/(live)/@header/page.tsx +++ b/app/[lang]/(live)/@header/page.tsx @@ -1,6 +1,9 @@ +import { Suspense } from "react" + import { env } from "@/env/server" import CurrentHeader from "@/components/Current/Header" +import HeaderFallback from "@/components/Current/Header/HeaderFallback" import Header from "@/components/Header" import { setLang } from "@/i18n/serverContext" @@ -10,7 +13,11 @@ export default function HeaderPage({ params }: PageArgs<LangParams>) { setLang(params.lang) if (env.HIDE_FOR_NEXT_RELEASE) { - return <CurrentHeader /> + return ( + <Suspense fallback={<HeaderFallback />}> + <CurrentHeader /> + </Suspense> + ) } return <Header /> diff --git a/app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx b/app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx deleted file mode 100644 index 1c031d2a1..000000000 --- a/app/[lang]/(live)/@sitewidealert/[...path]/loading.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../loading" From 7addf7874040aa64bc1ca8b69a90c067ba539e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= <bananashell@gmail.com> Date: Tue, 19 Nov 2024 08:04:39 +0100 Subject: [PATCH 042/101] fix: add missing loading.tsx and remove redundant ones --- .../hotelreservation/(standard)/[step]/@summary/loading.tsx | 5 +++++ .../(standard)/select-hotel/@modal/(.)map/loading.tsx | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/loading.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/loading.tsx new file mode 100644 index 000000000..78b79a040 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingSummaryHeader() { + return <LoadingSpinner /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx new file mode 100644 index 000000000..8f6f8657c --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function LoadingModal() { + return <LoadingSpinner /> +} From 17df6d6c47347bd4a005bae1ae103c27e41b4c52 Mon Sep 17 00:00:00 2001 From: Arvid Norlin <arvid.norlin@scandichotels.com> Date: Wed, 6 Nov 2024 16:14:57 +0100 Subject: [PATCH 043/101] feat(SW-612): Add popover component --- .../(standard)/step/@summary/page.tsx | 2 + .../EnterDetails/Summary/index.tsx | 21 ++++++-- .../EnterDetails/Summary/summary.module.css | 8 +++ .../Popover/Arrow/arrow.module.css | 22 ++++++++ .../TempDesignSystem/Popover/Arrow/index.tsx | 19 +++++++ components/TempDesignSystem/Popover/index.tsx | 50 +++++++++++++++++++ .../Popover/popover.module.css | 27 ++++++++++ .../TempDesignSystem/Popover/popover.ts | 6 +++ server/routers/hotels/query.ts | 5 ++ .../enterDetails/bookingData.ts | 1 + 10 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 components/TempDesignSystem/Popover/Arrow/arrow.module.css create mode 100644 components/TempDesignSystem/Popover/Arrow/index.tsx create mode 100644 components/TempDesignSystem/Popover/index.tsx create mode 100644 components/TempDesignSystem/Popover/popover.module.css create mode 100644 components/TempDesignSystem/Popover/popover.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index da3554f50..337d501b3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -104,6 +104,7 @@ export default async function SummaryPage({ euroPrice: prices.euro, adults, children, + rateDetails: availability.rateDetails, cancellationText: availability.cancellationText, packages, }} @@ -122,6 +123,7 @@ export default async function SummaryPage({ euroPrice: prices.euro, adults, children, + rateDetails: availability.rateDetails, cancellationText: availability.cancellationText, packages, }} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index c447c7f75..2e0bdb2cd 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -11,6 +11,7 @@ import { ArrowRightIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" +import Popover from "@/components/TempDesignSystem/Popover" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -178,9 +179,23 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { <Caption color="uiTextMediumContrast"> {room.cancellationText} </Caption> - <Link color="burgundy" href="#" variant="underscored" size="small"> - {intl.formatMessage({ id: "Rate details" })} - </Link> + <Popover + placement={"bottom left"} + triggerContent={ + <Caption color="burgundy" type="underline"> + {intl.formatMessage({ id: "Rate details" })} + </Caption> + } + > + <aside className={styles.rateDetailsPopover}> + <header> + <Caption type={"bold"}>{room.cancellationText}</Caption> + </header> + {room.rateDetails?.map((detail, idx) => ( + <Caption key={`rateDetails-${idx}`}>{detail}</Caption> + ))} + </aside> + </Popover> </div> {room.packages ? room.packages.map((roomPackage) => ( diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index 426afbc7d..e4ee465a8 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -41,6 +41,13 @@ gap: var(--Spacing-x-one-and-half); } +.rateDetailsPopover { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); + max-width: 360px; +} + .entry { display: flex; gap: var(--Spacing-x-half); @@ -50,6 +57,7 @@ .entry > :last-child { justify-items: flex-end; } + .total { display: flex; flex-direction: column; diff --git a/components/TempDesignSystem/Popover/Arrow/arrow.module.css b/components/TempDesignSystem/Popover/Arrow/arrow.module.css new file mode 100644 index 000000000..522b5aff8 --- /dev/null +++ b/components/TempDesignSystem/Popover/Arrow/arrow.module.css @@ -0,0 +1,22 @@ +.arrow { + transform-origin: center; + transform: translateY(-2px); +} + +[data-placement="left"] .arrow, +[data-placement="left top"] .arrow, +[data-placement="left bottom"] .arrow { + transform: rotate(270deg) translateY(-6px); +} + +[data-placement="right"] .arrow, +[data-placement="right top"] .arrow, +[data-placement="right bottom"] .arrow { + transform: rotate(90deg) translateY(-6px); +} + +[data-placement="bottom"] .arrow, +[data-placement="bottom left"] .arrow, +[data-placement="bottom right"] .arrow { + transform: rotate(180deg) translateY(-2px); +} diff --git a/components/TempDesignSystem/Popover/Arrow/index.tsx b/components/TempDesignSystem/Popover/Arrow/index.tsx new file mode 100644 index 000000000..4c67b059b --- /dev/null +++ b/components/TempDesignSystem/Popover/Arrow/index.tsx @@ -0,0 +1,19 @@ +import styles from "./arrow.module.css" + +export function Arrow() { + return ( + <div className={styles.arrow}> + <svg + xmlns="http://www.w3.org/2000/svg" + width="27" + height="13" + fill="none" + > + <path + fill="#fff" + d="M13.093 12.193.9 0h25.8L14.508 12.193a1 1 0 0 1-1.415 0Z" + /> + </svg> + </div> + ) +} diff --git a/components/TempDesignSystem/Popover/index.tsx b/components/TempDesignSystem/Popover/index.tsx new file mode 100644 index 000000000..1d389c648 --- /dev/null +++ b/components/TempDesignSystem/Popover/index.tsx @@ -0,0 +1,50 @@ +import { useRef } from "react" +import { + Button, + Dialog, + DialogTrigger, + OverlayArrow, + Popover as RAPopover, +} from "react-aria-components" + +import { CloseLargeIcon } from "@/components/Icons" + +import { Arrow } from "./Arrow" +import { PopoverProps } from "./popover" + +import styles from "./popover.module.css" + +export default function Popover({ + triggerContent, + children, + ...props +}: PopoverProps) { + let triggerRef = useRef(null) + + return ( + <DialogTrigger> + <Button className={styles.trigger}>{triggerContent}</Button> + + <RAPopover + {...props} + offset={16} + crossOffset={-24} + className={styles.root} + > + <OverlayArrow> + <Arrow /> + </OverlayArrow> + <Dialog> + {({ close }) => ( + <> + <Button className={styles.closeButton} onPress={close}> + <CloseLargeIcon height={20} width={20} /> + </Button> + {children} + </> + )} + </Dialog> + </RAPopover> + </DialogTrigger> + ) +} diff --git a/components/TempDesignSystem/Popover/popover.module.css b/components/TempDesignSystem/Popover/popover.module.css new file mode 100644 index 000000000..242bd5072 --- /dev/null +++ b/components/TempDesignSystem/Popover/popover.module.css @@ -0,0 +1,27 @@ +.root { + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + padding: var(--Spacing-x2); + max-width: calc(360px + var(--Spacing-x2) * 2); +} + +.root section:focus-visible { + outline: none; +} + +.trigger { + background: none; + border: none; + padding: 0; + cursor: pointer; +} +.closeButton { + position: absolute; + top: 8px; + right: 8px; + background: none; + border: none; + cursor: pointer; + padding: 0; +} diff --git a/components/TempDesignSystem/Popover/popover.ts b/components/TempDesignSystem/Popover/popover.ts new file mode 100644 index 000000000..c774ca398 --- /dev/null +++ b/components/TempDesignSystem/Popover/popover.ts @@ -0,0 +1,6 @@ +import type { PopoverProps as RAPopoverProps } from "react-aria-components" + +export interface PopoverProps extends Omit<RAPopoverProps, "children"> { + triggerContent: React.ReactNode + children: React.ReactNode +} diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index af9460b7c..7f2ef84c0 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -725,6 +725,10 @@ export const hotelQueryRouter = router({ return null } + const rateDetails = validateAvailabilityData.data.rateDefinitions.find( + (rateDef) => rateDef.rateCode === rateCode + )?.generalTerms + const rateTypes = selectedRoom.products.find( (rate) => rate.productType.public.rateCode === rateCode || @@ -782,6 +786,7 @@ export const hotelQueryRouter = router({ return { selectedRoom, + rateDetails, mustBeGuaranteed, cancellationText, memberRate: rates?.member, diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 628fa3f8b..0afabf91a 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -28,6 +28,7 @@ export type RoomsData = { euroPrice: Price | undefined adults: number children?: Child[] + rateDetails?: string[] cancellationText: string packages: Packages | null } From 1004871afb7f19401c87ca78e8a9065a43435478 Mon Sep 17 00:00:00 2001 From: Arvid Norlin <arvid.norlin@scandichotels.com> Date: Fri, 15 Nov 2024 13:59:51 +0100 Subject: [PATCH 044/101] fix: use new Popover component in FlexibilityOption --- .../RoomSelection/FlexibilityOption/index.tsx | 67 ++++--------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 5a046978c..941d6257d 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -1,13 +1,12 @@ "use client" -import { useRef, useState } from "react" -import { Button } from "react-aria-components" + import { useIntl } from "react-intl" import { CheckIcon, InfoCircleIcon } from "@/components/Icons" import Label from "@/components/TempDesignSystem/Form/Label" +import Popover from "@/components/TempDesignSystem/Popover" import Caption from "@/components/TempDesignSystem/Text/Caption" -import PricePopover from "./Popover" import PriceTable from "./PriceList" import styles from "./flexibilityOption.module.css" @@ -25,18 +24,8 @@ export default function FlexibilityOption({ petRoomPackage, handleSelectRate, }: FlexibilityOptionProps) { - const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined) - const [isPopoverOpen, setIsPopoverOpen] = useState(false) - let triggerRef = useRef<HTMLButtonElement>(null) - const buttonClickedRef = useRef(false) const intl = useIntl() - function setRef(node: Element | null) { - if (node) { - setRootDiv(node) - } - } - if (!product) { return ( <div className={styles.noPricesCard}> @@ -68,15 +57,6 @@ export default function FlexibilityOption({ handleSelectRate(rate) } - function togglePopover() { - buttonClickedRef.current = !buttonClickedRef.current - setIsPopoverOpen(buttonClickedRef.current) - } - - function handlePopoverChange(isOpen: boolean) { - setIsPopoverOpen(isOpen) - } - return ( <label> <input @@ -86,37 +66,16 @@ export default function FlexibilityOption({ onChange={onChange} /> <div className={styles.card}> - <div className={styles.header} ref={setRef}> - <Button - aria-label="Help" - className={styles.button} - onPress={() => { - togglePopover() - }} - ref={triggerRef} - > - <InfoCircleIcon - width={16} - height={16} - color="uiTextMediumContrast" - /> - </Button> - <PricePopover - placement="bottom" - className={styles.popover} - isNonModal - shouldFlip={false} - shouldUpdatePosition={false} - /** - * react-aria uses portals to render Popover in body - * unless otherwise specified. We need it to be contained - * by this component to both access css variables assigned - * on the container as well as to not overflow it at any time. - */ - UNSTABLE_portalContainer={rootDiv} - triggerRef={triggerRef} - isOpen={isPopoverOpen} - onOpenChange={handlePopoverChange} + <div className={styles.header}> + <Popover + placement="bottom left" + triggerContent={ + <InfoCircleIcon + width={16} + height={16} + color="uiTextMediumContrast" + /> + } > <Caption color="uiTextHighContrast" @@ -134,7 +93,7 @@ export default function FlexibilityOption({ {info} </Caption> ))} - </PricePopover> + </Popover> <Caption color="uiTextHighContrast">{name}</Caption> <Caption color="uiTextPlaceholder">({paymentTerm})</Caption> </div> From e18a2fdc445cf37ca485abace0e6cf9cb879a68d Mon Sep 17 00:00:00 2001 From: Arvid Norlin <arvid.norlin@scandichotels.com> Date: Fri, 15 Nov 2024 15:14:00 +0100 Subject: [PATCH 045/101] feat: add useSetOverflowVisibleOnRA hook --- .../EnterDetails/Summary/index.tsx | 4 +-- .../FlexibilityOption/Popover/index.tsx | 36 ------------------- .../Popover/popover.module.css | 12 ------- components/TempDesignSystem/Popover/index.tsx | 6 ++-- .../selectRate/pricePopover.ts | 5 --- 5 files changed, 5 insertions(+), 58 deletions(-) delete mode 100644 components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx delete mode 100644 components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css delete mode 100644 types/components/hotelReservation/selectRate/pricePopover.ts diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 2e0bdb2cd..7507a6945 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -180,7 +180,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { {room.cancellationText} </Caption> <Popover - placement={"bottom left"} + placement="bottom left" triggerContent={ <Caption color="burgundy" type="underline"> {intl.formatMessage({ id: "Rate details" })} @@ -189,7 +189,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { > <aside className={styles.rateDetailsPopover}> <header> - <Caption type={"bold"}>{room.cancellationText}</Caption> + <Caption type="bold">{room.cancellationText}</Caption> </header> {room.rateDetails?.map((detail, idx) => ( <Caption key={`rateDetails-${idx}`}>{detail}</Caption> diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx deleted file mode 100644 index 62979d2c7..000000000 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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 ( - <Popover {...props}> - <OverlayArrow className={styles.arrow}> - <svg - width="12" - height="12" - viewBox="0 0 12 12" - style={{ display: "block", transform: "rotate(180deg)" }} - > - <path d="M0 0L6 6L12 0" fill="white" /> - </svg> - </OverlayArrow> - <Dialog> - <Button - onPress={() => props.onOpenChange?.(false)} - className={styles.closeButton} - > - <CloseIcon className={styles.closeIcon} /> - </Button> - {children} - </Dialog> - </Popover> - ) -} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css deleted file mode 100644 index bb60ba100..000000000 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.arrow { - top: -6px; -} - -.closeButton { - position: absolute; - top: 5px; - right: 5px; - background: none; - border: none; - cursor: pointer; -} diff --git a/components/TempDesignSystem/Popover/index.tsx b/components/TempDesignSystem/Popover/index.tsx index 1d389c648..b1b03f340 100644 --- a/components/TempDesignSystem/Popover/index.tsx +++ b/components/TempDesignSystem/Popover/index.tsx @@ -1,4 +1,3 @@ -import { useRef } from "react" import { Button, Dialog, @@ -8,6 +7,7 @@ import { } from "react-aria-components" import { CloseLargeIcon } from "@/components/Icons" +import useSetOverFlowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA" import { Arrow } from "./Arrow" import { PopoverProps } from "./popover" @@ -19,10 +19,10 @@ export default function Popover({ children, ...props }: PopoverProps) { - let triggerRef = useRef(null) + const setOverflowVisible = useSetOverFlowVisibleOnRA() return ( - <DialogTrigger> + <DialogTrigger onOpenChange={setOverflowVisible}> <Button className={styles.trigger}>{triggerContent}</Button> <RAPopover diff --git a/types/components/hotelReservation/selectRate/pricePopover.ts b/types/components/hotelReservation/selectRate/pricePopover.ts deleted file mode 100644 index 2a7c84d56..000000000 --- a/types/components/hotelReservation/selectRate/pricePopover.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PopoverProps } from "react-aria-components" - -export interface PricePopoverProps extends Omit<PopoverProps, "children"> { - children: React.ReactNode -} From 275fdd8a78db9326456bbcab8efcb65f3f11bfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= <bananashell@gmail.com> Date: Tue, 19 Nov 2024 08:31:52 +0100 Subject: [PATCH 046/101] fix: add loading.tsx to route group --- app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx new file mode 100644 index 000000000..92ff5739e --- /dev/null +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return <LoadingSpinner fullPage /> +} From c0751968b5d0a87a2398044ea980f355854fab41 Mon Sep 17 00:00:00 2001 From: Arvid Norlin <arvid.norlin@scandichotels.com> Date: Wed, 6 Nov 2024 16:14:57 +0100 Subject: [PATCH 047/101] feat(SW-612): Add popover component --- .../(standard)/step/@summary/page.tsx | 2 + .../EnterDetails/Summary/index.tsx | 21 ++++++-- .../EnterDetails/Summary/summary.module.css | 8 +++ .../Popover/Arrow/arrow.module.css | 22 ++++++++ .../TempDesignSystem/Popover/Arrow/index.tsx | 19 +++++++ components/TempDesignSystem/Popover/index.tsx | 50 +++++++++++++++++++ .../Popover/popover.module.css | 27 ++++++++++ .../TempDesignSystem/Popover/popover.ts | 6 +++ server/routers/hotels/query.ts | 5 ++ .../enterDetails/bookingData.ts | 1 + 10 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 components/TempDesignSystem/Popover/Arrow/arrow.module.css create mode 100644 components/TempDesignSystem/Popover/Arrow/index.tsx create mode 100644 components/TempDesignSystem/Popover/index.tsx create mode 100644 components/TempDesignSystem/Popover/popover.module.css create mode 100644 components/TempDesignSystem/Popover/popover.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index da3554f50..337d501b3 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -104,6 +104,7 @@ export default async function SummaryPage({ euroPrice: prices.euro, adults, children, + rateDetails: availability.rateDetails, cancellationText: availability.cancellationText, packages, }} @@ -122,6 +123,7 @@ export default async function SummaryPage({ euroPrice: prices.euro, adults, children, + rateDetails: availability.rateDetails, cancellationText: availability.cancellationText, packages, }} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index c447c7f75..2e0bdb2cd 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -11,6 +11,7 @@ import { ArrowRightIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" +import Popover from "@/components/TempDesignSystem/Popover" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" @@ -178,9 +179,23 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { <Caption color="uiTextMediumContrast"> {room.cancellationText} </Caption> - <Link color="burgundy" href="#" variant="underscored" size="small"> - {intl.formatMessage({ id: "Rate details" })} - </Link> + <Popover + placement={"bottom left"} + triggerContent={ + <Caption color="burgundy" type="underline"> + {intl.formatMessage({ id: "Rate details" })} + </Caption> + } + > + <aside className={styles.rateDetailsPopover}> + <header> + <Caption type={"bold"}>{room.cancellationText}</Caption> + </header> + {room.rateDetails?.map((detail, idx) => ( + <Caption key={`rateDetails-${idx}`}>{detail}</Caption> + ))} + </aside> + </Popover> </div> {room.packages ? room.packages.map((roomPackage) => ( diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index 426afbc7d..e4ee465a8 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -41,6 +41,13 @@ gap: var(--Spacing-x-one-and-half); } +.rateDetailsPopover { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); + max-width: 360px; +} + .entry { display: flex; gap: var(--Spacing-x-half); @@ -50,6 +57,7 @@ .entry > :last-child { justify-items: flex-end; } + .total { display: flex; flex-direction: column; diff --git a/components/TempDesignSystem/Popover/Arrow/arrow.module.css b/components/TempDesignSystem/Popover/Arrow/arrow.module.css new file mode 100644 index 000000000..522b5aff8 --- /dev/null +++ b/components/TempDesignSystem/Popover/Arrow/arrow.module.css @@ -0,0 +1,22 @@ +.arrow { + transform-origin: center; + transform: translateY(-2px); +} + +[data-placement="left"] .arrow, +[data-placement="left top"] .arrow, +[data-placement="left bottom"] .arrow { + transform: rotate(270deg) translateY(-6px); +} + +[data-placement="right"] .arrow, +[data-placement="right top"] .arrow, +[data-placement="right bottom"] .arrow { + transform: rotate(90deg) translateY(-6px); +} + +[data-placement="bottom"] .arrow, +[data-placement="bottom left"] .arrow, +[data-placement="bottom right"] .arrow { + transform: rotate(180deg) translateY(-2px); +} diff --git a/components/TempDesignSystem/Popover/Arrow/index.tsx b/components/TempDesignSystem/Popover/Arrow/index.tsx new file mode 100644 index 000000000..4c67b059b --- /dev/null +++ b/components/TempDesignSystem/Popover/Arrow/index.tsx @@ -0,0 +1,19 @@ +import styles from "./arrow.module.css" + +export function Arrow() { + return ( + <div className={styles.arrow}> + <svg + xmlns="http://www.w3.org/2000/svg" + width="27" + height="13" + fill="none" + > + <path + fill="#fff" + d="M13.093 12.193.9 0h25.8L14.508 12.193a1 1 0 0 1-1.415 0Z" + /> + </svg> + </div> + ) +} diff --git a/components/TempDesignSystem/Popover/index.tsx b/components/TempDesignSystem/Popover/index.tsx new file mode 100644 index 000000000..1d389c648 --- /dev/null +++ b/components/TempDesignSystem/Popover/index.tsx @@ -0,0 +1,50 @@ +import { useRef } from "react" +import { + Button, + Dialog, + DialogTrigger, + OverlayArrow, + Popover as RAPopover, +} from "react-aria-components" + +import { CloseLargeIcon } from "@/components/Icons" + +import { Arrow } from "./Arrow" +import { PopoverProps } from "./popover" + +import styles from "./popover.module.css" + +export default function Popover({ + triggerContent, + children, + ...props +}: PopoverProps) { + let triggerRef = useRef(null) + + return ( + <DialogTrigger> + <Button className={styles.trigger}>{triggerContent}</Button> + + <RAPopover + {...props} + offset={16} + crossOffset={-24} + className={styles.root} + > + <OverlayArrow> + <Arrow /> + </OverlayArrow> + <Dialog> + {({ close }) => ( + <> + <Button className={styles.closeButton} onPress={close}> + <CloseLargeIcon height={20} width={20} /> + </Button> + {children} + </> + )} + </Dialog> + </RAPopover> + </DialogTrigger> + ) +} diff --git a/components/TempDesignSystem/Popover/popover.module.css b/components/TempDesignSystem/Popover/popover.module.css new file mode 100644 index 000000000..242bd5072 --- /dev/null +++ b/components/TempDesignSystem/Popover/popover.module.css @@ -0,0 +1,27 @@ +.root { + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1); + padding: var(--Spacing-x2); + max-width: calc(360px + var(--Spacing-x2) * 2); +} + +.root section:focus-visible { + outline: none; +} + +.trigger { + background: none; + border: none; + padding: 0; + cursor: pointer; +} +.closeButton { + position: absolute; + top: 8px; + right: 8px; + background: none; + border: none; + cursor: pointer; + padding: 0; +} diff --git a/components/TempDesignSystem/Popover/popover.ts b/components/TempDesignSystem/Popover/popover.ts new file mode 100644 index 000000000..c774ca398 --- /dev/null +++ b/components/TempDesignSystem/Popover/popover.ts @@ -0,0 +1,6 @@ +import type { PopoverProps as RAPopoverProps } from "react-aria-components" + +export interface PopoverProps extends Omit<RAPopoverProps, "children"> { + triggerContent: React.ReactNode + children: React.ReactNode +} diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index af9460b7c..7f2ef84c0 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -725,6 +725,10 @@ export const hotelQueryRouter = router({ return null } + const rateDetails = validateAvailabilityData.data.rateDefinitions.find( + (rateDef) => rateDef.rateCode === rateCode + )?.generalTerms + const rateTypes = selectedRoom.products.find( (rate) => rate.productType.public.rateCode === rateCode || @@ -782,6 +786,7 @@ export const hotelQueryRouter = router({ return { selectedRoom, + rateDetails, mustBeGuaranteed, cancellationText, memberRate: rates?.member, diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 628fa3f8b..0afabf91a 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -28,6 +28,7 @@ export type RoomsData = { euroPrice: Price | undefined adults: number children?: Child[] + rateDetails?: string[] cancellationText: string packages: Packages | null } From bb49dccc9e282b970aad35370b8bc2a5ea8e175f Mon Sep 17 00:00:00 2001 From: Arvid Norlin <arvid.norlin@scandichotels.com> Date: Fri, 15 Nov 2024 13:59:51 +0100 Subject: [PATCH 048/101] fix: use new Popover component in FlexibilityOption --- .../RoomSelection/FlexibilityOption/index.tsx | 67 ++++--------------- 1 file changed, 13 insertions(+), 54 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 5a046978c..941d6257d 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -1,13 +1,12 @@ "use client" -import { useRef, useState } from "react" -import { Button } from "react-aria-components" + import { useIntl } from "react-intl" import { CheckIcon, InfoCircleIcon } from "@/components/Icons" import Label from "@/components/TempDesignSystem/Form/Label" +import Popover from "@/components/TempDesignSystem/Popover" import Caption from "@/components/TempDesignSystem/Text/Caption" -import PricePopover from "./Popover" import PriceTable from "./PriceList" import styles from "./flexibilityOption.module.css" @@ -25,18 +24,8 @@ export default function FlexibilityOption({ petRoomPackage, handleSelectRate, }: FlexibilityOptionProps) { - const [rootDiv, setRootDiv] = useState<Element | undefined>(undefined) - const [isPopoverOpen, setIsPopoverOpen] = useState(false) - let triggerRef = useRef<HTMLButtonElement>(null) - const buttonClickedRef = useRef(false) const intl = useIntl() - function setRef(node: Element | null) { - if (node) { - setRootDiv(node) - } - } - if (!product) { return ( <div className={styles.noPricesCard}> @@ -68,15 +57,6 @@ export default function FlexibilityOption({ handleSelectRate(rate) } - function togglePopover() { - buttonClickedRef.current = !buttonClickedRef.current - setIsPopoverOpen(buttonClickedRef.current) - } - - function handlePopoverChange(isOpen: boolean) { - setIsPopoverOpen(isOpen) - } - return ( <label> <input @@ -86,37 +66,16 @@ export default function FlexibilityOption({ onChange={onChange} /> <div className={styles.card}> - <div className={styles.header} ref={setRef}> - <Button - aria-label="Help" - className={styles.button} - onPress={() => { - togglePopover() - }} - ref={triggerRef} - > - <InfoCircleIcon - width={16} - height={16} - color="uiTextMediumContrast" - /> - </Button> - <PricePopover - placement="bottom" - className={styles.popover} - isNonModal - shouldFlip={false} - shouldUpdatePosition={false} - /** - * react-aria uses portals to render Popover in body - * unless otherwise specified. We need it to be contained - * by this component to both access css variables assigned - * on the container as well as to not overflow it at any time. - */ - UNSTABLE_portalContainer={rootDiv} - triggerRef={triggerRef} - isOpen={isPopoverOpen} - onOpenChange={handlePopoverChange} + <div className={styles.header}> + <Popover + placement="bottom left" + triggerContent={ + <InfoCircleIcon + width={16} + height={16} + color="uiTextMediumContrast" + /> + } > <Caption color="uiTextHighContrast" @@ -134,7 +93,7 @@ export default function FlexibilityOption({ {info} </Caption> ))} - </PricePopover> + </Popover> <Caption color="uiTextHighContrast">{name}</Caption> <Caption color="uiTextPlaceholder">({paymentTerm})</Caption> </div> From bdbdfa80ab9ed6bb1b5c6c001ad6160fd5004f51 Mon Sep 17 00:00:00 2001 From: Arvid Norlin <arvid.norlin@scandichotels.com> Date: Fri, 15 Nov 2024 15:14:00 +0100 Subject: [PATCH 049/101] feat: add useSetOverflowVisibleOnRA hook --- .../EnterDetails/Summary/index.tsx | 4 +-- .../FlexibilityOption/Popover/index.tsx | 36 ------------------- .../Popover/popover.module.css | 12 ------- components/TempDesignSystem/Popover/index.tsx | 6 ++-- .../selectRate/pricePopover.ts | 5 --- 5 files changed, 5 insertions(+), 58 deletions(-) delete mode 100644 components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx delete mode 100644 components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css delete mode 100644 types/components/hotelReservation/selectRate/pricePopover.ts diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 2e0bdb2cd..7507a6945 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -180,7 +180,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { {room.cancellationText} </Caption> <Popover - placement={"bottom left"} + placement="bottom left" triggerContent={ <Caption color="burgundy" type="underline"> {intl.formatMessage({ id: "Rate details" })} @@ -189,7 +189,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { > <aside className={styles.rateDetailsPopover}> <header> - <Caption type={"bold"}>{room.cancellationText}</Caption> + <Caption type="bold">{room.cancellationText}</Caption> </header> {room.rateDetails?.map((detail, idx) => ( <Caption key={`rateDetails-${idx}`}>{detail}</Caption> diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx deleted file mode 100644 index 62979d2c7..000000000 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -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 ( - <Popover {...props}> - <OverlayArrow className={styles.arrow}> - <svg - width="12" - height="12" - viewBox="0 0 12 12" - style={{ display: "block", transform: "rotate(180deg)" }} - > - <path d="M0 0L6 6L12 0" fill="white" /> - </svg> - </OverlayArrow> - <Dialog> - <Button - onPress={() => props.onOpenChange?.(false)} - className={styles.closeButton} - > - <CloseIcon className={styles.closeIcon} /> - </Button> - {children} - </Dialog> - </Popover> - ) -} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css deleted file mode 100644 index bb60ba100..000000000 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/Popover/popover.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.arrow { - top: -6px; -} - -.closeButton { - position: absolute; - top: 5px; - right: 5px; - background: none; - border: none; - cursor: pointer; -} diff --git a/components/TempDesignSystem/Popover/index.tsx b/components/TempDesignSystem/Popover/index.tsx index 1d389c648..b1b03f340 100644 --- a/components/TempDesignSystem/Popover/index.tsx +++ b/components/TempDesignSystem/Popover/index.tsx @@ -1,4 +1,3 @@ -import { useRef } from "react" import { Button, Dialog, @@ -8,6 +7,7 @@ import { } from "react-aria-components" import { CloseLargeIcon } from "@/components/Icons" +import useSetOverFlowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA" import { Arrow } from "./Arrow" import { PopoverProps } from "./popover" @@ -19,10 +19,10 @@ export default function Popover({ children, ...props }: PopoverProps) { - let triggerRef = useRef(null) + const setOverflowVisible = useSetOverFlowVisibleOnRA() return ( - <DialogTrigger> + <DialogTrigger onOpenChange={setOverflowVisible}> <Button className={styles.trigger}>{triggerContent}</Button> <RAPopover diff --git a/types/components/hotelReservation/selectRate/pricePopover.ts b/types/components/hotelReservation/selectRate/pricePopover.ts deleted file mode 100644 index 2a7c84d56..000000000 --- a/types/components/hotelReservation/selectRate/pricePopover.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { PopoverProps } from "react-aria-components" - -export interface PricePopoverProps extends Omit<PopoverProps, "children"> { - children: React.ReactNode -} From dac345371b1f610eedf242f36cfc143810ff1470 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 15:22:13 +0100 Subject: [PATCH 050/101] feat(SW-589): Added correct breakfastMessage, also fixed some small issues --- .../HotelReservation/HotelCard/index.tsx | 2 +- .../RoomSelection/RoomCard/index.tsx | 23 ++++++++++++++++--- .../RoomCard/roomCard.module.css | 13 +---------- i18n/dictionaries/da.json | 2 ++ i18n/dictionaries/de.json | 2 ++ i18n/dictionaries/en.json | 2 ++ i18n/dictionaries/fi.json | 2 ++ i18n/dictionaries/no.json | 2 ++ i18n/dictionaries/sv.json | 2 ++ 9 files changed, 34 insertions(+), 16 deletions(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 808e9ac9f..75fe1c345 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -136,7 +136,7 @@ export default function HotelCard({ {hotelData.specialAlerts.length > 0 && ( <div className={styles.specialAlerts}> {hotelData.specialAlerts.map((alert) => ( - <Alert key={alert.id} type={alert.type} text={alert.text} /> + <Alert key={alert.id} type={alert.type} text={alert.heading} /> ))} </div> )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index fb713fddb..e2829a1d8 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -59,6 +59,25 @@ export default function RoomCard({ ?.generalTerms } + function getBreakfastInformation(rate: RateDefinition | undefined) { + return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) + ?.breakfastIncluded + } + + const breakfastMessage = (rate: RateDefinition | undefined) => { + const breakfastInfo = getBreakfastInformation(rate) + switch (breakfastInfo) { + case true: + return intl.formatMessage({ id: "Breakfast is included." }) + case false: + return intl.formatMessage({ id: "Breakfast selection in next step." }) + default: + return intl.formatMessage({ + id: "Breakfast deal can be purchased at the hotel.", + }) + } + } + const petRoomPackage = (selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) && packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) || @@ -168,9 +187,7 @@ export default function RoomCard({ </div> <div className={styles.container}> <Caption color="uiTextHighContrast" type="bold"> - {intl.formatMessage({ - id: "Breakfast selection in next step.", - })} + {breakfastMessage(rates.flexRate)} </Caption> {roomConfiguration.status === "NotAvailable" ? ( <div className={styles.noRoomsContainer}> diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 8ec1978e6..8e32bdb1d 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -12,18 +12,7 @@ .card.noAvailability { justify-content: flex-start; -} - -.card.noAvailability:before { - background-color: rgba(0, 0, 0, 40%); - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 2; + opacity: 0.6; } .specification { diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 336f5eef9..6a2c12e20 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -47,8 +47,10 @@ "Booking number": "Bookingnummer", "Breakfast": "Morgenmad", "Breakfast buffet": "Morgenbuffet", + "Breakfast deal can be purchased at the hotel.": "Morgenmad kan købes på hotellet.", "Breakfast excluded": "Morgenmad ikke inkluderet", "Breakfast included": "Morgenmad inkluderet", + "Breakfast is included.": "Morgenmad er inkluderet.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Valg af morgenmad i næste trin.", "Bus terminal": "Busstation", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 78d9fd5cb..5968f5cee 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -47,8 +47,10 @@ "Booking number": "Buchungsnummer", "Breakfast": "Frühstück", "Breakfast buffet": "Frühstücksbuffet", + "Breakfast deal can be purchased at the hotel.": "Frühstücksangebot kann im Hotel gekauft werden.", "Breakfast excluded": "Frühstück nicht inbegriffen", "Breakfast included": "Frühstück inbegriffen", + "Breakfast is included.": "Frühstück ist inbegriffen.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Frühstücksauswahl in nächsten Schritt.", "Bus terminal": "Busbahnhof", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index cb8a071d9..910af641a 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -51,8 +51,10 @@ "Booking number": "Booking number", "Breakfast": "Breakfast", "Breakfast buffet": "Breakfast buffet", + "Breakfast deal can be purchased at the hotel.": "Breakfast deal can be purchased at the hotel.", "Breakfast excluded": "Breakfast excluded", "Breakfast included": "Breakfast included", + "Breakfast is included.": "Breakfast is included.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Breakfast selection in next step.", "Bus terminal": "Bus terminal", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 48931f7f2..bc79d0cf0 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -47,8 +47,10 @@ "Booking number": "Varausnumero", "Breakfast": "Aamiainen", "Breakfast buffet": "Aamiaisbuffet", + "Breakfast deal can be purchased at the hotel.": "Aamiaisdeali voidaan ostaa hotellissa.", "Breakfast excluded": "Aamiainen ei sisälly", "Breakfast included": "Aamiainen sisältyy", + "Breakfast is included.": "Aamiainen sisältyy.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Aamiaisvalinta seuraavassa vaiheessa.", "Bus terminal": "Bussiasema", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 313d0799d..a5dd81fc4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -47,8 +47,10 @@ "Booking number": "Bestillingsnummer", "Breakfast": "Frokost", "Breakfast buffet": "Breakfast buffet", + "Breakfast deal can be purchased at the hotel.": "Frokostdeal kan kjøpes på hotellet.", "Breakfast excluded": "Frokost ekskludert", "Breakfast included": "Frokost inkludert", + "Breakfast is included.": "Frokost er inkludert.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Frokostvalg i neste steg.", "Bus terminal": "Bussterminal", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 878bce352..0c5816f51 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -47,8 +47,10 @@ "Booking number": "Bokningsnummer", "Breakfast": "Frukost", "Breakfast buffet": "Frukostbuffé", + "Breakfast deal can be purchased at the hotel.": "Frukostdeal kan köpas på hotellet.", "Breakfast excluded": "Frukost ingår ej", "Breakfast included": "Frukost ingår", + "Breakfast is included.": "Frukost ingår.", "Breakfast restaurant": "Breakfast restaurant", "Breakfast selection in next step.": "Frukostval i nästa steg.", "Bus terminal": "Bussterminal", From 43d760eedb59fb84c7214583d6a420c5588f822e Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:41:02 +0100 Subject: [PATCH 051/101] feat(SW-589) Updated after comments --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index e2829a1d8..8cf0b5c21 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -59,13 +59,11 @@ export default function RoomCard({ ?.generalTerms } - function getBreakfastInformation(rate: RateDefinition | undefined) { - return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) - ?.breakfastIncluded - } + const getBreakfastMessage = (rate: RateDefinition | undefined) => { + const breakfastInfo = rateDefinitions.find( + (def) => def.rateCode === rate?.rateCode + )?.breakfastIncluded - const breakfastMessage = (rate: RateDefinition | undefined) => { - const breakfastInfo = getBreakfastInformation(rate) switch (breakfastInfo) { case true: return intl.formatMessage({ id: "Breakfast is included." }) @@ -187,7 +185,7 @@ export default function RoomCard({ </div> <div className={styles.container}> <Caption color="uiTextHighContrast" type="bold"> - {breakfastMessage(rates.flexRate)} + {getBreakfastMessage(rates.flexRate)} </Caption> {roomConfiguration.status === "NotAvailable" ? ( <div className={styles.noRoomsContainer}> From da89c73d0724b9b0f1562012e4f19a74aebf8239 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:43:02 +0100 Subject: [PATCH 052/101] feat(SW-589) reverted change on hotel card for alert (this will be removed) --- components/HotelReservation/HotelCard/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 75fe1c345..808e9ac9f 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -136,7 +136,7 @@ export default function HotelCard({ {hotelData.specialAlerts.length > 0 && ( <div className={styles.specialAlerts}> {hotelData.specialAlerts.map((alert) => ( - <Alert key={alert.id} type={alert.type} text={alert.heading} /> + <Alert key={alert.id} type={alert.type} text={alert.text} /> ))} </div> )} From a9db6c806329b245f561b11137d0a91be7411b76 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:54:18 +0100 Subject: [PATCH 053/101] feat(SW-589) updated getRateDefinitionForRate --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 8cf0b5c21..a81e4169e 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -54,15 +54,12 @@ export default function RoomCard({ : undefined } - function getPriceInformationForRate(rate: RateDefinition | undefined) { + function getRateDefinitionForRate(rate: RateDefinition | undefined) { return rateDefinitions.find((def) => def.rateCode === rate?.rateCode) - ?.generalTerms } const getBreakfastMessage = (rate: RateDefinition | undefined) => { - const breakfastInfo = rateDefinitions.find( - (def) => def.rateCode === rate?.rateCode - )?.breakfastIncluded + const breakfastInfo = getRateDefinitionForRate(rate)?.breakfastIncluded switch (breakfastInfo) { case true: @@ -207,7 +204,7 @@ export default function RoomCard({ value={key.toLowerCase()} paymentTerm={key === "flexRate" ? payLater : payNow} product={findProductForRate(rate)} - priceInformation={getPriceInformationForRate(rate)} + priceInformation={getRateDefinitionForRate(rate)?.generalTerms} handleSelectRate={handleSelectRate} roomType={roomConfiguration.roomType} roomTypeCode={roomConfiguration.roomTypeCode} From 70d92ee16809aaa2029158c86e68f389f675d46c Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Mon, 18 Nov 2024 16:55:17 +0100 Subject: [PATCH 054/101] feat(SW-589): updated breakfastIncluded --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index a81e4169e..d69391122 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -59,9 +59,9 @@ export default function RoomCard({ } const getBreakfastMessage = (rate: RateDefinition | undefined) => { - const breakfastInfo = getRateDefinitionForRate(rate)?.breakfastIncluded + const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded - switch (breakfastInfo) { + switch (breakfastIncluded) { case true: return intl.formatMessage({ id: "Breakfast is included." }) case false: From 01031448150948fdf6348e061555022b99d6170c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= <bananashell@gmail.com> Date: Tue, 19 Nov 2024 08:47:46 +0100 Subject: [PATCH 055/101] fix: add missing loading.tsx --- .../(public)/hotelreservation/(confirmation)/loading.tsx | 5 +++++ .../(live)/(public)/hotelreservation/(standard)/loading.tsx | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx new file mode 100644 index 000000000..92ff5739e --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return <LoadingSpinner fullPage /> +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx new file mode 100644 index 000000000..92ff5739e --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/loading.tsx @@ -0,0 +1,5 @@ +import LoadingSpinner from "@/components/LoadingSpinner" + +export default function Loading() { + return <LoadingSpinner fullPage /> +} From 117cbcd20d6e22ddf62abd92a4901266b064bd51 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Fri, 15 Nov 2024 16:31:11 +0100 Subject: [PATCH 056/101] feat: add new design for join scandic friends checkbox --- .../ChildSelector/ChildInfoSelector.tsx | 3 +- .../EnterDetails/Details/Signup/index.tsx | 24 +--- components/LoginButton/index.tsx | 26 ++--- .../TempDesignSystem/Form/Checkbox/index.tsx | 6 +- .../Form/ChoiceCard/Checkbox.tsx | 7 -- .../Form/JoinScandicFriendsCard/index.tsx | 108 ++++++++++++++++++ .../joinScandicFriendsCard.module.css | 30 +++++ .../TempDesignSystem/Link/link.module.css | 21 +++- components/TempDesignSystem/Link/variants.ts | 2 + i18n/dictionaries/da.json | 4 +- i18n/dictionaries/de.json | 4 +- i18n/dictionaries/en.json | 6 +- i18n/dictionaries/fi.json | 4 +- i18n/dictionaries/no.json | 4 +- i18n/dictionaries/sv.json | 4 +- types/components/tracking.ts | 1 + 16 files changed, 189 insertions(+), 65 deletions(-) delete mode 100644 components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx create mode 100644 components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx create mode 100644 components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index b5a34c168..8252f2d2a 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -33,11 +33,10 @@ export default function ChildInfoSelector({ const ageLabel = intl.formatMessage({ id: "Age" }) const bedLabel = intl.formatMessage({ id: "Bed" }) const errorMessage = intl.formatMessage({ id: "Child age is required" }) - const { setValue, formState, register, trigger } = useFormContext() + const { setValue, formState, register } = useFormContext() function updateSelectedBed(bed: number) { setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) - trigger() } function updateSelectedAge(age: number) { diff --git a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx index 74a17e911..1846a7cd1 100644 --- a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx @@ -7,9 +7,9 @@ import { useIntl } from "react-intl" import { privacyPolicy } from "@/constants/currentWebHrefs" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" -import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox" import DateSelect from "@/components/TempDesignSystem/Form/Date" import Input from "@/components/TempDesignSystem/Form/Input" +import JoinScandicFriendsCard from "@/components/TempDesignSystem/Form/JoinScandicFriendsCard" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" @@ -31,29 +31,11 @@ export default function Signup({ name }: { name: string }) { 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 ( <div className={styles.container}> - <CheckboxCard - highlightSubtitle - list={list} + <JoinScandicFriendsCard name={name} - subtitle={intl.formatMessage( - { - id: "{difference}{amount} {currency}", - }, - { - amount: "491", - currency: "SEK", - difference: "-", - } - )} - title={intl.formatMessage({ id: "Join Scandic Friends" })} + difference={{ price: 1000, currency: "SEK" }} /> {isJoinChecked ? ( <div className={styles.additionalFormData}> diff --git a/components/LoginButton/index.tsx b/components/LoginButton/index.tsx index f6b163b38..6258f6e88 100644 --- a/components/LoginButton/index.tsx +++ b/components/LoginButton/index.tsx @@ -13,19 +13,16 @@ import { trackLoginClick } from "@/utils/tracking" import { TrackingPosition } from "@/types/components/tracking" export default function LoginButton({ - className, position, trackingId, children, - color = "black", - variant = "navigation", -}: PropsWithChildren<{ - className: string - trackingId: string - position: TrackingPosition - color?: LinkProps["color"] - variant?: "navigation" | "signupVerification" -}>) { + ...props +}: PropsWithChildren< + { + trackingId: string + position: TrackingPosition + } & Omit<LinkProps, "href"> +>) { const lang = useLang() const pathName = useLazyPathname({ includeSearchParams: true }) @@ -45,14 +42,7 @@ export default function LoginButton({ }, [position, trackingId]) return ( - <Link - className={className} - id={trackingId} - color={color} - href={href} - prefetch={false} - variant={variant} - > + <Link id={trackingId} prefetch={false} {...props} href={href}> {children} </Link> ) diff --git a/components/TempDesignSystem/Form/Checkbox/index.tsx b/components/TempDesignSystem/Form/Checkbox/index.tsx index 60bd81935..bd4639e39 100644 --- a/components/TempDesignSystem/Form/Checkbox/index.tsx +++ b/components/TempDesignSystem/Form/Checkbox/index.tsx @@ -12,6 +12,7 @@ import styles from "./checkbox.module.css" import { CheckboxProps } from "@/types/components/checkbox" export default function Checkbox({ + className, name, children, registerOptions, @@ -25,16 +26,17 @@ export default function Checkbox({ return ( <AriaCheckbox - className={styles.container} + className={`${styles.container} ${className}`} isSelected={field.value} onChange={field.onChange} data-testid={name} isDisabled={registerOptions?.disabled} + excludeFromTabOrder > {({ isSelected }) => ( <> <span className={styles.checkboxContainer}> - <span className={styles.checkbox}> + <span className={styles.checkbox} tabIndex={0}> {isSelected && <CheckIcon color="white" />} </span> {children} diff --git a/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx b/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx deleted file mode 100644 index ca32951cb..000000000 --- a/components/TempDesignSystem/Form/ChoiceCard/Checkbox.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Card from "./_Card" - -import type { CheckboxProps } from "./_Card/card" - -export default function CheckboxCard(props: CheckboxProps) { - return <Card {...props} type="checkbox" /> -} diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx b/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx new file mode 100644 index 000000000..629a904fc --- /dev/null +++ b/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx @@ -0,0 +1,108 @@ +"use client" + +import { useIntl } from "react-intl" + +import { privacyPolicy } from "@/constants/currentWebHrefs" + +import { CheckIcon } from "@/components/Icons" +import LoginButton from "@/components/LoginButton" +import useLang from "@/hooks/useLang" + +import Link from "../../Link" +import Caption from "../../Text/Caption" +import Footnote from "../../Text/Footnote" +import Checkbox from "../Checkbox" + +import styles from "./joinScandicFriendsCard.module.css" + +type JoinScandicFriendsCardProps = { + name: string + difference: { price: number; currency: string } +} + +export default function JoinScandicFriendsCard({ + name, + difference, +}: JoinScandicFriendsCardProps) { + const lang = useLang() + 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" }) }, + ] + + return ( + <label className={styles.cardContainer}> + <header className={styles.header}> + <Checkbox name={name} className={styles.checkBox} /> + <div> + <Caption type="label" textTransform="uppercase" color="red"> + {intl.formatMessage( + { + id: "Only pay {amount} {currency}", + }, + { + amount: intl.formatNumber(difference.price), + currency: difference.currency, + } + )} + </Caption> + <Caption + type="label" + textTransform="uppercase" + color="uiTextHighContrast" + > + {intl.formatMessage({ id: "Join Scandic Friends" })} + </Caption> + </div> + <Footnote color="uiTextHighContrast"> + {intl.formatMessage({ id: "Already a friend?" })}{" "} + <LoginButton + color="burgundy" + position="enter details" + trackingId="join-scandic-friends-enter-details" + variant="breadcrumb" + target="_blank" + > + {intl.formatMessage({ id: "Log in" })} + </LoginButton> + </Footnote> + </header> + + <div className={styles.list}> + {list.map((item) => ( + <Caption + key={item.title} + color="uiTextPlaceholder" + className={styles.listItem} + > + <CheckIcon color="uiTextPlaceholder" height="20" /> {item.title} + </Caption> + ))} + </div> + <Footnote color="uiTextPlaceholder"> + {intl.formatMessage<React.ReactNode>( + { + id: "signup.terms", + }, + { + termsLink: (str) => ( + <Link + variant="default" + textDecoration="underline" + size="tiny" + target="_blank" + color="uiTextPlaceholder" + href={privacyPolicy[lang]} + > + {str} + </Link> + ), + } + )} + </Footnote> + </label> + ) +} diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css b/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css new file mode 100644 index 000000000..42dcc86d0 --- /dev/null +++ b/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css @@ -0,0 +1,30 @@ +.cardContainer { + align-self: flex-start; + background-color: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Large); + display: grid; + cursor: pointer; + gap: var(--Spacing-x2); + padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + width: min(100%, 600px); +} + +.header { + display: grid; + gap: var(--Spacing-x-one-and-half); + grid-template-columns: auto 1fr auto; +} + +.checkBox { + align-self: center; +} + +.list { + display: flex; + gap: var(--Spacing-x1); +} + +.listItem { + display: flex; +} diff --git a/components/TempDesignSystem/Link/link.module.css b/components/TempDesignSystem/Link/link.module.css index 138c8094e..948574ed2 100644 --- a/components/TempDesignSystem/Link/link.module.css +++ b/components/TempDesignSystem/Link/link.module.css @@ -16,7 +16,7 @@ .breadcrumb { font-family: var(--typography-Footnote-Bold-fontFamily); font-size: var(--typography-Footnote-Bold-fontSize); - font-weight: var(--typography-Footnote-Bold-fontWeight); + font-weight: 450; /* var(--typography-Footnote-Bold-fontWeight); */ letter-spacing: var(--typography-Footnote-Bold-letterSpacing); line-height: var(--typography-Footnote-Bold-lineHeight); } @@ -24,7 +24,7 @@ .link.breadcrumb { font-family: var(--typography-Footnote-Bold-fontFamily); font-size: var(--typography-Footnote-Bold-fontSize); - font-weight: var(--typography-Footnote-Bold-fontWeight); + font-weight: 450; /* var(--typography-Footnote-Bold-fontWeight); */ letter-spacing: var(--typography-Footnote-Bold-letterSpacing); line-height: var(--typography-Footnote-Bold-lineHeight); } @@ -128,6 +128,15 @@ color: #000; } +.uiTextPlaceholder { + color: var(--Base-Text-Placeholder); +} + +.uiTextPlaceholder:hover, +.uiTextPlaceholder:active { + color: var(--Base-Text-Medium-contrast); +} + .burgundy { color: var(--Base-Text-High-contrast); } @@ -211,6 +220,14 @@ line-height: var(--typography-Caption-Regular-lineHeight); } +.tiny { + font-family: var(--typography-Footnote-Regular-fontFamily); + font-size: var(--typography-Footnote-Regular-fontSize); + font-weight: var(--typography-Footnote-Regular-fontWeight); + letter-spacing: var(--typography-Footnote-Regular-letterSpacing); + line-height: var(--typography-Footnote-Regular-lineHeight); +} + .activeSmall { font-family: var(--typography-Caption-Bold-fontFamily); font-size: var(--typography-Caption-Bold-fontSize); diff --git a/components/TempDesignSystem/Link/variants.ts b/components/TempDesignSystem/Link/variants.ts index 61c7f8493..0a0c1dd6d 100644 --- a/components/TempDesignSystem/Link/variants.ts +++ b/components/TempDesignSystem/Link/variants.ts @@ -17,10 +17,12 @@ export const linkVariants = cva(styles.link, { peach80: styles.peach80, white: styles.white, red: styles.red, + uiTextPlaceholder: styles.uiTextPlaceholder, }, size: { small: styles.small, regular: styles.regular, + tiny: styles.tiny, }, textDecoration: { none: styles.noDecoration, diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 6a2c12e20..7ec920d05 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -238,6 +238,7 @@ "Number of parking spots": "Antal parkeringspladser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din rejse", + "Only pay {amount} {currency}": "Betal kun {amount} {currency}", "Open": "Åben", "Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}", "Open image gallery": "Åbn billedgalleri", @@ -458,6 +459,5 @@ "to": "til", "uppercase letter": "stort bogstav", "{amount} out of {total}": "{amount} ud af {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 5968f5cee..80b3b6116 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -236,6 +236,7 @@ "Number of parking spots": "Anzahl der Parkplätze", "OTHER PAYMENT METHODS": "ANDERE BEZAHLMETHODE", "On your journey": "Auf deiner Reise", + "Only pay {amount} {currency}": "Nur bezahlen {amount} {currency}", "Open": "Offen", "Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen", "Open image gallery": "Bildergalerie öffnen", @@ -456,6 +457,5 @@ "to": "zu", "uppercase letter": "großbuchstabe", "{amount} out of {total}": "{amount} von {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 910af641a..6080fc36b 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -255,6 +255,7 @@ "Number of parking spots": "Number of parking spots", "OTHER PAYMENT METHODS": "OTHER PAYMENT METHODS", "On your journey": "On your journey", + "Only pay {amount} {currency}": "Only pay {amount} {currency}", "Open": "Open", "Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}", "Open image gallery": "Open image gallery", @@ -427,7 +428,6 @@ "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", "Yes": "Yes", - "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", "Yes, discard changes": "Yes, discard changes", "Yes, remove my card": "Yes, remove my card", "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", @@ -490,12 +490,12 @@ "points": "Points", "room type": "room type", "room types": "room types", + "signup.terms": "By signing up you accept the Scandic Friends <termsLink>Terms and Conditions</termsLink>. Your membership is valid until further notice, and you can terminate your membership at any time by sending an email to Scandic’s customer service", "special character": "special character", "spendable points expiring by": "{points} spendable points expiring by {date}", "to": "to", "uppercase letter": "uppercase letter", "{amount} out of {total}": "{amount} out of {total}", "{amount} {currency}": "{amount} {currency}", - "{card} ending with {cardno}": "{card} ending with {cardno}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{card} ending with {cardno}": "{card} ending with {cardno}" } diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index bc79d0cf0..0c1a08d35 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -238,6 +238,7 @@ "Number of parking spots": "Pysäköintipaikkojen määrä", "OTHER PAYMENT METHODS": "MUISE KORT", "On your journey": "Matkallasi", + "Only pay {amount} {currency}": "Vain maksaa {amount} {currency}", "Open": "Avata", "Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}", "Open image gallery": "Avaa kuvagalleria", @@ -456,6 +457,5 @@ "to": "to", "uppercase letter": "iso kirjain", "{amount} out of {total}": "{amount}/{total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index a5dd81fc4..2aca61a85 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -236,6 +236,7 @@ "Number of parking spots": "Antall parkeringsplasser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På reisen din", + "Only pay {amount} {currency}": "Bare betal {amount} {currency}", "Open": "Åpen", "Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}", "Open image gallery": "Åpne bildegalleri", @@ -454,6 +455,5 @@ "to": "til", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 0c5816f51..db300b7c2 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -236,6 +236,7 @@ "Number of parking spots": "Antal parkeringsplatser", "OTHER PAYMENT METHODS": "ANDRE BETALINGSMETODER", "On your journey": "På din resa", + "Only pay {amount} {currency}": "Betala endast {amount} {currency}", "Open": "Öppna", "Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}", "Open image gallery": "Öppna bildgalleri", @@ -457,6 +458,5 @@ "types": "typer", "uppercase letter": "stor bokstav", "{amount} out of {total}": "{amount} av {total}", - "{amount} {currency}": "{amount} {currency}", - "{difference}{amount} {currency}": "{difference}{amount} {currency}" + "{amount} {currency}": "{amount} {currency}" } diff --git a/types/components/tracking.ts b/types/components/tracking.ts index aa42ae5fe..fa6e82a95 100644 --- a/types/components/tracking.ts +++ b/types/components/tracking.ts @@ -80,3 +80,4 @@ export type TrackingPosition = | "hamburger menu" | "join scandic friends sidebar" | "sign up verification" + | "enter details" From 4c9e9de11350f06fe924af827749e3b8b5171f91 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 18 Nov 2024 13:13:41 +0100 Subject: [PATCH 057/101] fix: add mobile design for details form --- .../EnterDetails/Details/details.module.css | 32 ++++++++++++- .../Form/Checkbox/checkbox.module.css | 1 + .../Form/JoinScandicFriendsCard/index.tsx | 48 ++++++++++--------- .../joinScandicFriendsCard.module.css | 3 +- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Details/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index f89dfa7cc..2dc4266ed 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,11 +1,12 @@ .form { display: grid; - gap: var(--Spacing-x2); + gap: var(--Spacing-x1); + padding: var(--Spacing-x3) 0px; } .container { display: grid; - gap: var(--Spacing-x2); + gap: var(--Spacing-x1); grid-template-columns: 1fr 1fr; width: min(100%, 600px); } @@ -23,3 +24,30 @@ justify-items: flex-start; margin-top: var(--Spacing-x1); } + +@media screen and (min-width: 1367px) { + .form { + gap: var(--Spacing-x2); + padding: var(--Spacing-x3) 0px; + } + + .container { + gap: var(--Spacing-x2); + grid-template-columns: 1fr 1fr; + width: min(100%, 600px); + } + + .country, + .email, + .membershipNo, + .phone { + grid-column: 1/-1; + } + + .footer { + display: grid; + gap: var(--Spacing-x3); + justify-items: flex-start; + margin-top: var(--Spacing-x1); + } +} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 2e924b226..11c558af9 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -2,6 +2,7 @@ display: flex; flex-direction: column; color: var(--text-color); + cursor: pointer; } .container[data-selected] .checkbox { diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx b/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx index 629a904fc..70f03e6fe 100644 --- a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx +++ b/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx @@ -34,29 +34,31 @@ export default function JoinScandicFriendsCard({ ] return ( - <label className={styles.cardContainer}> + <div className={styles.cardContainer}> <header className={styles.header}> - <Checkbox name={name} className={styles.checkBox} /> - <div> - <Caption type="label" textTransform="uppercase" color="red"> - {intl.formatMessage( - { - id: "Only pay {amount} {currency}", - }, - { - amount: intl.formatNumber(difference.price), - currency: difference.currency, - } - )} - </Caption> - <Caption - type="label" - textTransform="uppercase" - color="uiTextHighContrast" - > - {intl.formatMessage({ id: "Join Scandic Friends" })} - </Caption> - </div> + <Checkbox name={name} className={styles.checkBox}> + <div> + <Caption type="label" textTransform="uppercase" color="red"> + {intl.formatMessage( + { + id: "Only pay {amount} {currency}", + }, + { + amount: intl.formatNumber(difference.price), + currency: difference.currency, + } + )} + </Caption> + <Caption + type="label" + textTransform="uppercase" + color="uiTextHighContrast" + > + {intl.formatMessage({ id: "Join Scandic Friends" })} + </Caption> + </div> + </Checkbox> + <Footnote color="uiTextHighContrast"> {intl.formatMessage({ id: "Already a friend?" })}{" "} <LoginButton @@ -103,6 +105,6 @@ export default function JoinScandicFriendsCard({ } )} </Footnote> - </label> + </div> ) } diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css b/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css index 42dcc86d0..8238c0bf2 100644 --- a/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css +++ b/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css @@ -4,7 +4,6 @@ border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Large); display: grid; - cursor: pointer; gap: var(--Spacing-x2); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); width: min(100%, 600px); @@ -13,7 +12,7 @@ .header { display: grid; gap: var(--Spacing-x-one-and-half); - grid-template-columns: auto 1fr auto; + grid-template-columns: 1fr auto; } .checkBox { From 9094b08345fb3c6a45dca99b6cbc112d8a0742cc Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 18 Nov 2024 15:04:37 +0100 Subject: [PATCH 058/101] fix: booking widget locking scroll --- components/BookingWidget/Client.tsx | 2 +- components/Forms/BookingWidget/index.tsx | 4 ++-- types/components/form/bookingwidget.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index cb5e308b5..922522742 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -177,7 +177,7 @@ export default function BookingWidgetClient({ > <CloseLargeIcon /> </button> - <Form locations={locations} type={type} setIsOpen={setIsOpen} /> + <Form locations={locations} type={type} onClose={closeMobileSearch} /> </div> </section> <div className={styles.backdrop} onClick={closeMobileSearch} /> diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index b47ae74aa..3f7e5aa82 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -20,7 +20,7 @@ const formId = "booking-widget" export default function Form({ locations, type, - setIsOpen, + onClose, }: BookingWidgetFormProps) { const router = useRouter() const lang = useLang() @@ -56,7 +56,7 @@ export default function Form({ ) }) }) - setIsOpen(false) + onClose() router.push(`${bookingFlowPage}?${bookingWidgetParams.toString()}`) } diff --git a/types/components/form/bookingwidget.ts b/types/components/form/bookingwidget.ts index 887c4eae2..bded82de3 100644 --- a/types/components/form/bookingwidget.ts +++ b/types/components/form/bookingwidget.ts @@ -4,7 +4,7 @@ import type { Location, Locations } from "@/types/trpc/routers/hotel/locations" export interface BookingWidgetFormProps { locations: Locations type?: BookingWidgetType - setIsOpen: (isOpen: boolean) => void + onClose: () => void } export interface BookingWidgetFormContentProps { From d5c6b6809c9333a854daa13d71f120d056d4b9c1 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 18 Nov 2024 15:07:23 +0100 Subject: [PATCH 059/101] fix: responsivity of fields and order of signup form --- .../FormContent/formContent.module.css | 10 +- components/Forms/Signup/index.tsx | 1 - .../EnterDetails/Details/Signup/index.tsx | 68 +++------- .../EnterDetails/Details/details.module.css | 32 +---- .../EnterDetails/Details/index.tsx | 37 +++--- .../EnterDetails/Details/schema.ts | 10 -- .../EnterDetails/SectionAccordion/index.tsx | 48 ++++--- .../sectionAccordion.module.css | 63 ++++----- .../SelectedRoom/selectedRoom.module.css | 2 +- .../Form/ChoiceCard/_Card/card.module.css | 1 + .../Form/Date/date.module.css | 11 ++ .../TempDesignSystem/Form/Date/index.tsx | 1 + .../Form/JoinScandicFriendsCard/index.tsx | 72 +++++------ .../joinScandicFriendsCard.module.css | 38 +++++- .../TempDesignSystem/Form/Phone/index.tsx | 120 +++++++++--------- .../Form/Phone/phone.module.css | 11 ++ stores/details.ts | 1 - 17 files changed, 264 insertions(+), 262 deletions(-) diff --git a/components/Forms/Edit/Profile/FormContent/formContent.module.css b/components/Forms/Edit/Profile/FormContent/formContent.module.css index 64eb85410..aec012aa6 100644 --- a/components/Forms/Edit/Profile/FormContent/formContent.module.css +++ b/components/Forms/Edit/Profile/FormContent/formContent.module.css @@ -3,12 +3,14 @@ align-self: flex-start; display: grid; gap: var(--Spacing-x2); + container-name: addressContainer; + container-type: inline-size; } .container { display: grid; gap: var(--Spacing-x2); - grid-template-columns: max(164px) 1fr; + grid-template-columns: minmax(100px, 164px) 1fr; } @media (min-width: 768px) { @@ -16,3 +18,9 @@ display: none; } } + +@container addressContainer (max-width: 350px) { + .container { + grid-template-columns: 1fr; + } +} diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index ad66f6282..d6d5fc7da 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -48,7 +48,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { zipCode: "", }, password: "", - termsAccepted: false, }, mode: "all", criteriaMode: "all", diff --git a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx index 1846a7cd1..85559aa4c 100644 --- a/components/HotelReservation/EnterDetails/Details/Signup/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/Signup/index.tsx @@ -4,14 +4,8 @@ import { useEffect, useState } from "react" import { useWatch } from "react-hook-form" import { useIntl } from "react-intl" -import { privacyPolicy } from "@/constants/currentWebHrefs" - -import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import DateSelect from "@/components/TempDesignSystem/Form/Date" import Input from "@/components/TempDesignSystem/Form/Input" -import JoinScandicFriendsCard from "@/components/TempDesignSystem/Form/JoinScandicFriendsCard" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import useLang from "@/hooks/useLang" @@ -31,49 +25,27 @@ export default function Signup({ name }: { name: string }) { setIsJoinChecked(joinValue) }, [joinValue]) - return ( - <div className={styles.container}> - <JoinScandicFriendsCard - name={name} - difference={{ price: 1000, currency: "SEK" }} + return isJoinChecked ? ( + <div className={styles.additionalFormData}> + <Input + name="zipCode" + label={intl.formatMessage({ id: "Zip code" })} + registerOptions={{ required: true }} /> - {isJoinChecked ? ( - <div className={styles.additionalFormData}> - <div className={styles.dateField}> - <header> - <Caption type="bold"> - {intl.formatMessage({ id: "Birth date" })} * - </Caption> - </header> - <DateSelect - name="dateOfBirth" - registerOptions={{ required: true }} - /> - <Input - name="zipCode" - label={intl.formatMessage({ id: "Zip code" })} - registerOptions={{ required: true }} - /> - </div> - <div> - <Checkbox name="termsAccepted" registerOptions={{ required: true }}> - <Body> - {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", - })}{" "} - <Link - variant="underscored" - color="peach80" - target="_blank" - href={privacyPolicy[lang]} - > - {intl.formatMessage({ id: "Scandic's Privacy Policy." })} - </Link> - </Body> - </Checkbox> - </div> - </div> - ) : null} + <div className={styles.dateField}> + <header> + <Caption type="bold"> + {intl.formatMessage({ id: "Birth date" })} * + </Caption> + </header> + <DateSelect name="dateOfBirth" registerOptions={{ required: true }} /> + </div> </div> + ) : ( + <Input + label={intl.formatMessage({ id: "Membership no" })} + name="membershipNo" + type="tel" + /> ) } diff --git a/components/HotelReservation/EnterDetails/Details/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index 2dc4266ed..c6571ea9e 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,34 +1,30 @@ .form { display: grid; - gap: var(--Spacing-x1); - padding: var(--Spacing-x3) 0px; + gap: var(--Spacing-x3); + margin-bottom: var(--Spacing-x3); } .container { display: grid; - gap: var(--Spacing-x1); - grid-template-columns: 1fr 1fr; + gap: var(--Spacing-x2); width: min(100%, 600px); } +.header, .country, .email, -.membershipNo, +.signup, .phone { grid-column: 1/-1; } .footer { - display: grid; - gap: var(--Spacing-x3); - justify-items: flex-start; margin-top: var(--Spacing-x1); } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 768px) { .form { - gap: var(--Spacing-x2); - padding: var(--Spacing-x3) 0px; + gap: var(--Spacing-x3); } .container { @@ -36,18 +32,4 @@ grid-template-columns: 1fr 1fr; width: min(100%, 600px); } - - .country, - .email, - .membershipNo, - .phone { - grid-column: 1/-1; - } - - .footer { - display: grid; - gap: var(--Spacing-x3); - justify-items: flex-start; - margin-top: var(--Spacing-x1); - } } diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index dd5959c31..3c0c55b9b 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -10,6 +10,7 @@ import { useStepsStore } from "@/stores/steps" import Button from "@/components/TempDesignSystem/Button" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" +import JoinScandicFriendsCard from "@/components/TempDesignSystem/Form/JoinScandicFriendsCard" import Phone from "@/components/TempDesignSystem/Form/Phone" import Footnote from "@/components/TempDesignSystem/Text/Footnote" @@ -35,7 +36,6 @@ export default function Details({ user }: DetailsProps) { join: state.data.join, dateOfBirth: state.data.dateOfBirth, zipCode: state.data.zipCode, - termsAccepted: state.data.termsAccepted, membershipNo: state.data.membershipNo, })) @@ -52,7 +52,6 @@ export default function Details({ user }: DetailsProps) { join: initialData.join, dateOfBirth: initialData.dateOfBirth, zipCode: initialData.zipCode, - termsAccepted: initialData.termsAccepted, membershipNo: initialData.membershipNo, }, criteriaMode: "all", @@ -69,6 +68,7 @@ export default function Details({ user }: DetailsProps) { [completeStep, updateDetails] ) + const joinValue = methods.watch("join") return ( <FormProvider {...methods}> <form @@ -76,15 +76,21 @@ export default function Details({ user }: DetailsProps) { id={formID} onSubmit={methods.handleSubmit(onSubmit)} > - {user ? null : <Signup name="join" />} - <Footnote - color="uiTextHighContrast" - textTransform="uppercase" - type="label" - > - {intl.formatMessage({ id: "Guest information" })} - </Footnote> + {user ? null : ( + <JoinScandicFriendsCard + name="join" + difference={{ price: 1000, currency: "SEK" }} + /> + )} <div className={styles.container}> + <Footnote + color="uiTextHighContrast" + textTransform="uppercase" + type="label" + className={styles.header} + > + {intl.formatMessage({ id: "Guest information" })} + </Footnote> <Input label={intl.formatMessage({ id: "First name" })} name="firstName" @@ -118,13 +124,10 @@ export default function Details({ user }: DetailsProps) { readOnly={!!user} registerOptions={{ required: true }} /> - {user || methods.watch("join") ? null : ( - <Input - className={styles.membershipNo} - label={intl.formatMessage({ id: "Membership no" })} - name="membershipNo" - type="tel" - /> + {user ? null : ( + <div className={styles.signup}> + <Signup name="join" /> + </div> )} </div> <footer className={styles.footer}> diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 2b8075da9..abb29ac2b 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -15,7 +15,6 @@ export const notJoinDetailsSchema = baseDetailsSchema.merge( join: z.literal<boolean>(false), zipCode: z.string().optional(), dateOfBirth: z.string().optional(), - termsAccepted: z.boolean().default(false), membershipNo: z .string() .optional() @@ -39,15 +38,6 @@ export const joinDetailsSchema = baseDetailsSchema.merge( join: z.literal<boolean>(true), zipCode: z.string().min(1, { message: "Zip code is required" }), dateOfBirth: z.string().min(1, { message: "Date of birth is required" }), - termsAccepted: z.literal<boolean>(true, { - errorMap: (err, ctx) => { - switch (err.code) { - case "invalid_literal": - return { message: "You must accept the terms and conditions" } - } - return { message: ctx.defaultError } - }, - }), membershipNo: z.string().optional(), }) ) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index ce548ae74..a740fb224 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -66,7 +66,7 @@ export default function SectionAccordion({ const textColor = isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled" return ( - <section className={styles.wrapper} data-open={isOpen} data-step={step}> + <div className={styles.main} data-open={isOpen} data-step={step}> <div className={styles.iconWrapper}> <div className={styles.circle} data-checked={isComplete}> {isComplete ? ( @@ -74,31 +74,27 @@ export default function SectionAccordion({ ) : null} </div> </div> - <div className={styles.main}> - <header> - <button onClick={onModify} className={styles.modifyButton}> - <Footnote - className={styles.title} - asChild - textTransform="uppercase" - type="label" - color={textColor} - > - <h2>{header}</h2> - </Footnote> - <Subtitle className={styles.selection} type="two" color={textColor}> - {title} - </Subtitle> + <header className={styles.header}> + <button onClick={onModify} className={styles.modifyButton}> + <Footnote + className={styles.title} + asChild + textTransform="uppercase" + type="label" + color={textColor} + > + <h2>{header}</h2> + </Footnote> + <Subtitle className={styles.selection} type="two" color={textColor}> + {title} + </Subtitle> - {isComplete && !isOpen && ( - <ChevronDownIcon className={styles.button} color="burgundy" /> - )} - </button> - </header> - <div className={styles.content}> - <div className={styles.contentWrapper}>{children}</div> - </div> - </div> - </section> + {isComplete && !isOpen && ( + <ChevronDownIcon className={styles.button} color="burgundy" /> + )} + </button> + </header> + <div className={styles.content}>{children}</div> + </div> ) } diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index ed91cb9e2..f4990b28b 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -1,15 +1,25 @@ -.wrapper { - position: relative; - display: flex; - flex-direction: row; - gap: var(--Spacing-x-one-and-half); +.main { + gap: var(--Spacing-x3); + width: 100%; padding-top: var(--Spacing-x3); + transition: 0.4s ease-out; + + display: grid; + grid-template-areas: "circle header" "content content"; + grid-template-columns: auto 1fr; + grid-template-rows: 2.4em 0fr; + + column-gap: var(--Spacing-x-one-and-half); } -.wrapper:last-child .main { +.main:last-child .main { border-bottom: none; } +.header { + grid-area: header; +} + .modifyButton { display: grid; grid-template-areas: "title button" "selection button"; @@ -17,6 +27,7 @@ background-color: transparent; border: none; width: 100%; + padding: 0; } .title { @@ -29,15 +40,6 @@ justify-self: flex-end; } -.main { - display: grid; - 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; -} - .selection { font-weight: 450; font-size: var(--typography-Title-4-fontSize); @@ -46,6 +48,7 @@ .iconWrapper { position: relative; + grid-area: circle; } .circle { @@ -63,42 +66,42 @@ background-color: var(--UI-Input-Controls-Fill-Selected); } -.wrapper[data-open="true"] .circle[data-checked="false"] { +.main[data-open="true"] .circle[data-checked="false"] { background-color: var(--UI-Text-Placeholder); } -.wrapper[data-open="false"] .circle[data-checked="false"] { +.main[data-open="false"] .circle[data-checked="false"] { background-color: var(--Base-Surface-Subtle-Hover); } -.wrapper[data-open="true"] .main { - grid-template-rows: 2em 1fr; +.main[data-open="true"] { + grid-template-rows: 2.4em 1fr; + gap: var(--Spacing-x3); } .content { overflow: hidden; + grid-area: content; + border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } -.contentWrapper { - padding-top: var(--Spacing-x3); -} - -@media screen and (min-width: 1367px) { - .wrapper { +@media screen and (min-width: 768px) { + .main { gap: var(--Spacing-x3); + grid-template-areas: "circle header" "circle content"; } .iconWrapper { top: var(--Spacing-x1); } - .wrapper:not(:last-child)::after { + .main:not(:last-child) .iconWrapper::after { position: absolute; left: 12px; - bottom: 0; - top: var(--Spacing-x7); - height: 100%; + bottom: calc(0px - var(--Spacing-x7)); + top: 24px; + content: ""; border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); } -} \ No newline at end of file +} diff --git a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css index 3149cf709..e979c1ee2 100644 --- a/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css +++ b/components/HotelReservation/EnterDetails/SelectedRoom/selectedRoom.module.css @@ -63,7 +63,7 @@ justify-content: flex-start; } -@media screen and (min-width: 1367px) { +@media screen and (min-width: 768px) { .wrapper { gap: var(--Spacing-x3); padding-top: var(--Spacing-x3); diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css index fa7d6d13a..69044abb2 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css @@ -9,6 +9,7 @@ padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); transition: all 200ms ease; width: min(100%, 600px); + height: 100%; } .label:hover { diff --git a/components/TempDesignSystem/Form/Date/date.module.css b/components/TempDesignSystem/Form/Date/date.module.css index fde0b7e03..7f492a293 100644 --- a/components/TempDesignSystem/Form/Date/date.module.css +++ b/components/TempDesignSystem/Form/Date/date.module.css @@ -1,4 +1,8 @@ /* Leaving, will most likely get deleted */ +.datePicker { + container-name: datePickerContainer; + container-type: inline-size; +} .container { display: grid; gap: var(--Spacing-x2); @@ -27,3 +31,10 @@ .year.invalid > div > div { border-color: var(--Scandic-Red-60); } + +@container datePickerContainer (max-width: 350px) { + .container { + display: flex; + flex-direction: column; + } +} diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 4e8f4a7af..fa4b27528 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -115,6 +115,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { ref={field.ref} value={dateValue} data-testid={name} + className={styles.datePicker} > <Group> <DateInput className={styles.container}> diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx b/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx index 70f03e6fe..8a6c4383c 100644 --- a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx +++ b/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx @@ -35,43 +35,41 @@ export default function JoinScandicFriendsCard({ return ( <div className={styles.cardContainer}> - <header className={styles.header}> - <Checkbox name={name} className={styles.checkBox}> - <div> - <Caption type="label" textTransform="uppercase" color="red"> - {intl.formatMessage( - { - id: "Only pay {amount} {currency}", - }, - { - amount: intl.formatNumber(difference.price), - currency: difference.currency, - } - )} - </Caption> - <Caption - type="label" - textTransform="uppercase" - color="uiTextHighContrast" - > - {intl.formatMessage({ id: "Join Scandic Friends" })} - </Caption> - </div> - </Checkbox> - - <Footnote color="uiTextHighContrast"> - {intl.formatMessage({ id: "Already a friend?" })}{" "} - <LoginButton - color="burgundy" - position="enter details" - trackingId="join-scandic-friends-enter-details" - variant="breadcrumb" - target="_blank" + <Checkbox name={name} className={styles.checkBox}> + <div> + <Caption type="label" textTransform="uppercase" color="red"> + {intl.formatMessage( + { + id: "Only pay {amount} {currency}", + }, + { + amount: intl.formatNumber(difference.price), + currency: difference.currency, + } + )} + </Caption> + <Caption + type="label" + textTransform="uppercase" + color="uiTextHighContrast" > - {intl.formatMessage({ id: "Log in" })} - </LoginButton> - </Footnote> - </header> + {intl.formatMessage({ id: "Join Scandic Friends" })} + </Caption> + </div> + </Checkbox> + + <Footnote color="uiTextHighContrast" className={styles.login}> + {intl.formatMessage({ id: "Already a friend?" })}{" "} + <LoginButton + color="burgundy" + position="enter details" + trackingId="join-scandic-friends-enter-details" + variant="breadcrumb" + target="_blank" + > + {intl.formatMessage({ id: "Log in" })} + </LoginButton> + </Footnote> <div className={styles.list}> {list.map((item) => ( @@ -84,7 +82,7 @@ export default function JoinScandicFriendsCard({ </Caption> ))} </div> - <Footnote color="uiTextPlaceholder"> + <Footnote color="uiTextPlaceholder" className={styles.terms}> {intl.formatMessage<React.ReactNode>( { id: "signup.terms", diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css b/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css index 8238c0bf2..507633fe4 100644 --- a/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css +++ b/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css @@ -4,26 +4,52 @@ border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Large); display: grid; - gap: var(--Spacing-x2); + gap: var(--Spacing-x-one-and-half); padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); + grid-template-areas: + "checkbox" + "list" + "login" + "terms"; width: min(100%, 600px); } -.header { - display: grid; - gap: var(--Spacing-x-one-and-half); - grid-template-columns: 1fr auto; +.login { + grid-area: login; } .checkBox { align-self: center; + grid-area: checkbox; } .list { - display: flex; + display: grid; + grid-area: list; gap: var(--Spacing-x1); } .listItem { display: flex; } + +.terms { + border-top: 1px solid var(--Base-Border-Normal); + grid-area: terms; + padding-top: var(--Spacing-x1); +} + +@media screen and (min-width: 768px) { + .cardContainer { + grid-template-columns: 1fr auto; + gap: var(--Spacing-x2); + grid-template-areas: + "checkbox login" + "list list" + "terms terms"; + } + .list { + display: flex; + gap: var(--Spacing-x1); + } +} diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index 9c413fc0a..5ff8f3482 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -78,67 +78,69 @@ export default function Phone({ } return ( - <div className={`${styles.phone} ${className}`}> - <CountrySelector - disabled={readOnly} - dropdownArrowClassName={styles.arrow} - flagClassName={styles.flag} - onSelect={handleSelectCountry} - preferredCountries={["de", "dk", "fi", "no", "se", "gb"]} - selectedCountry={country.iso2} - renderButtonWrapper={(props) => ( - <button - {...props.rootProps} - className={styles.select} - tabIndex={0} - type="button" - data-testid="country-selector" - > - <Label required={!!registerOptions.required} size="small"> - {intl.formatMessage({ id: "Country code" })} - </Label> - <span className={styles.selectContainer}> - {props.children} - <Body asChild fontOnly> - <DialCodePreview - className={styles.dialCode} - dialCode={country.dialCode} - prefix="+" + <div className={`${styles.wrapper} ${className}`}> + <div className={styles.phone}> + <CountrySelector + disabled={readOnly} + dropdownArrowClassName={styles.arrow} + flagClassName={styles.flag} + onSelect={handleSelectCountry} + preferredCountries={["de", "dk", "fi", "no", "se", "gb"]} + selectedCountry={country.iso2} + renderButtonWrapper={(props) => ( + <button + {...props.rootProps} + className={styles.select} + tabIndex={0} + type="button" + data-testid="country-selector" + > + <Label required={!!registerOptions.required} size="small"> + {intl.formatMessage({ id: "Country code" })} + </Label> + <span className={styles.selectContainer}> + {props.children} + <Body asChild fontOnly> + <DialCodePreview + className={styles.dialCode} + dialCode={country.dialCode} + prefix="+" + /> + </Body> + <ChevronDownIcon + className={styles.chevron} + color="grey80" + height={18} + width={18} /> - </Body> - <ChevronDownIcon - className={styles.chevron} - color="grey80" - height={18} - width={18} - /> - </span> - </button> - )} - /> - <TextField - aria-label={ariaLabel} - defaultValue={field.value} - isDisabled={disabled ?? field.disabled} - isInvalid={fieldState.invalid} - isRequired={!!registerOptions?.required} - isReadOnly={readOnly} - name={field.name} - type="tel" - > - <AriaInputWithLabel - {...field} - id={field.name} - label={label} - onChange={handleChange} - placeholder={placeholder} - readOnly={readOnly} - required={!!registerOptions.required} - type="tel" - value={inputValue} + </span> + </button> + )} /> - <ErrorMessage errors={formState.errors} name={field.name} /> - </TextField> + <TextField + aria-label={ariaLabel} + defaultValue={field.value} + isDisabled={disabled ?? field.disabled} + isInvalid={fieldState.invalid} + isRequired={!!registerOptions?.required} + isReadOnly={readOnly} + name={field.name} + type="tel" + > + <AriaInputWithLabel + {...field} + id={field.name} + label={label} + onChange={handleChange} + placeholder={placeholder} + readOnly={readOnly} + required={!!registerOptions.required} + type="tel" + value={inputValue} + /> + <ErrorMessage errors={formState.errors} name={field.name} /> + </TextField> + </div> </div> ) } diff --git a/components/TempDesignSystem/Form/Phone/phone.module.css b/components/TempDesignSystem/Form/Phone/phone.module.css index f2b75136b..31de5be30 100644 --- a/components/TempDesignSystem/Form/Phone/phone.module.css +++ b/components/TempDesignSystem/Form/Phone/phone.module.css @@ -1,3 +1,7 @@ +.wrapper { + container-name: phoneContainer; + container-type: inline-size; +} .phone { display: grid; gap: var(--Spacing-x2); @@ -100,3 +104,10 @@ justify-self: flex-start; padding: 0; } + +@container phoneContainer (max-width: 350px) { + .phone { + display: flex; + flex-direction: column; + } +} diff --git a/stores/details.ts b/stores/details.ts index 5d23248e5..a382ad7d7 100644 --- a/stores/details.ts +++ b/stores/details.ts @@ -94,7 +94,6 @@ export function createDetailsStore( state.data.membershipNo = data.membershipNo } state.data.phoneNumber = data.phoneNumber - state.data.termsAccepted = data.termsAccepted state.data.zipCode = data.zipCode }) ) From bd0720dc0ff61525b14b27a766e56faeed11ca98 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Mon, 18 Nov 2024 15:30:12 +0100 Subject: [PATCH 060/101] fix: break out css variables --- .../hotelreservation/(standard)/step/page.tsx | 9 +++- .../Details}/JoinScandicFriendsCard/index.tsx | 42 +++++++++---------- .../joinScandicFriendsCard.module.css | 0 .../EnterDetails/Details/index.tsx | 10 ++--- .../EnterDetails/SectionAccordion/index.tsx | 2 +- .../sectionAccordion.module.css | 27 ++++++------ components/LoginButton/index.tsx | 9 ++-- .../Form/ChoiceCard/_Card/card.module.css | 2 - .../TempDesignSystem/Link/link.module.css | 4 +- .../hotelReservation/enterDetails/details.ts | 8 ++++ 10 files changed, 63 insertions(+), 50 deletions(-) rename components/{TempDesignSystem/Form => HotelReservation/EnterDetails/Details}/JoinScandicFriendsCard/index.tsx (75%) rename components/{TempDesignSystem/Form => HotelReservation/EnterDetails/Details}/JoinScandicFriendsCard/joinScandicFriendsCard.module.css (100%) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index 648cdff93..7ffe57562 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -111,6 +111,13 @@ export default async function StepPage({ publicPrice: roomAvailability.publicRate!.localPrice.pricePerStay, } + const memberPrice = roomAvailability.memberRate + ? { + price: roomAvailability.memberRate.localPrice.pricePerStay, + currency: roomAvailability.memberRate.localPrice.currency, + } + : undefined + return ( <StepsProvider bedTypes={roomAvailability.bedTypes} @@ -152,7 +159,7 @@ export default async function StepPage({ step={StepEnum.details} label={intl.formatMessage({ id: "Enter your details" })} > - <Details user={user} /> + <Details user={user} memberPrice={memberPrice} /> </SectionAccordion> <SectionAccordion diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/index.tsx similarity index 75% rename from components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx rename to components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/index.tsx index 8a6c4383c..e6d0e500b 100644 --- a/components/TempDesignSystem/Form/JoinScandicFriendsCard/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/index.tsx @@ -6,23 +6,19 @@ import { privacyPolicy } from "@/constants/currentWebHrefs" import { CheckIcon } from "@/components/Icons" import LoginButton from "@/components/LoginButton" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" import useLang from "@/hooks/useLang" -import Link from "../../Link" -import Caption from "../../Text/Caption" -import Footnote from "../../Text/Footnote" -import Checkbox from "../Checkbox" - import styles from "./joinScandicFriendsCard.module.css" -type JoinScandicFriendsCardProps = { - name: string - difference: { price: number; currency: string } -} +import { JoinScandicFriendsCardProps } from "@/types/components/hotelReservation/enterDetails/details" export default function JoinScandicFriendsCard({ name, - difference, + memberPrice, }: JoinScandicFriendsCardProps) { const lang = useLang() const intl = useIntl() @@ -33,21 +29,25 @@ export default function JoinScandicFriendsCard({ { title: intl.formatMessage({ id: "Join at no cost" }) }, ] + const saveOnJoiningLabel = intl.formatMessage( + { + id: "Only pay {amount} {currency}", + }, + { + amount: intl.formatNumber(memberPrice?.price ?? 0), + currency: memberPrice?.currency ?? "SEK", + } + ) + return ( <div className={styles.cardContainer}> <Checkbox name={name} className={styles.checkBox}> <div> - <Caption type="label" textTransform="uppercase" color="red"> - {intl.formatMessage( - { - id: "Only pay {amount} {currency}", - }, - { - amount: intl.formatNumber(difference.price), - currency: difference.currency, - } - )} - </Caption> + {memberPrice ? ( + <Caption type="label" textTransform="uppercase" color="red"> + {saveOnJoiningLabel} + </Caption> + ) : null} <Caption type="label" textTransform="uppercase" diff --git a/components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css b/components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/joinScandicFriendsCard.module.css similarity index 100% rename from components/TempDesignSystem/Form/JoinScandicFriendsCard/joinScandicFriendsCard.module.css rename to components/HotelReservation/EnterDetails/Details/JoinScandicFriendsCard/joinScandicFriendsCard.module.css diff --git a/components/HotelReservation/EnterDetails/Details/index.tsx b/components/HotelReservation/EnterDetails/Details/index.tsx index 3c0c55b9b..819ad5243 100644 --- a/components/HotelReservation/EnterDetails/Details/index.tsx +++ b/components/HotelReservation/EnterDetails/Details/index.tsx @@ -10,10 +10,10 @@ import { useStepsStore } from "@/stores/steps" import Button from "@/components/TempDesignSystem/Button" import CountrySelect from "@/components/TempDesignSystem/Form/Country" import Input from "@/components/TempDesignSystem/Form/Input" -import JoinScandicFriendsCard from "@/components/TempDesignSystem/Form/JoinScandicFriendsCard" import Phone from "@/components/TempDesignSystem/Form/Phone" import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import { guestDetailsSchema, signedInDetailsSchema } from "./schema" import Signup from "./Signup" @@ -25,7 +25,7 @@ import type { } from "@/types/components/hotelReservation/enterDetails/details" const formID = "enter-details" -export default function Details({ user }: DetailsProps) { +export default function Details({ user, memberPrice }: DetailsProps) { const intl = useIntl() const initialData = useDetailsStore((state) => ({ countryCode: state.data.countryCode, @@ -68,7 +68,6 @@ export default function Details({ user }: DetailsProps) { [completeStep, updateDetails] ) - const joinValue = methods.watch("join") return ( <FormProvider {...methods}> <form @@ -77,10 +76,7 @@ export default function Details({ user }: DetailsProps) { onSubmit={methods.handleSubmit(onSubmit)} > {user ? null : ( - <JoinScandicFriendsCard - name="join" - difference={{ price: 1000, currency: "SEK" }} - /> + <JoinScandicFriendsCard name="join" memberPrice={memberPrice} /> )} <div className={styles.container}> <Footnote diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index a740fb224..b96ef8ef8 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -66,7 +66,7 @@ export default function SectionAccordion({ const textColor = isComplete || isOpen ? "uiTextHighContrast" : "baseTextDisabled" return ( - <div className={styles.main} data-open={isOpen} data-step={step}> + <div className={styles.accordion} data-open={isOpen} data-step={step}> <div className={styles.iconWrapper}> <div className={styles.circle} data-checked={isComplete}> {isComplete ? ( diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index f4990b28b..0bbcf851c 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -1,4 +1,7 @@ -.main { +.accordion { + --header-height: 2.4em; + --circle-height: 24px; + gap: var(--Spacing-x3); width: 100%; padding-top: var(--Spacing-x3); @@ -7,12 +10,12 @@ display: grid; grid-template-areas: "circle header" "content content"; grid-template-columns: auto 1fr; - grid-template-rows: 2.4em 0fr; + grid-template-rows: var(--header-height) 0fr; column-gap: var(--Spacing-x-one-and-half); } -.main:last-child .main { +.accordion:last-child { border-bottom: none; } @@ -52,8 +55,8 @@ } .circle { - width: 24px; - height: 24px; + width: var(--circle-height); + height: var(--circle-height); border-radius: 100px; transition: background-color 0.4s; border: 2px solid var(--Base-Border-Inverted); @@ -66,16 +69,16 @@ background-color: var(--UI-Input-Controls-Fill-Selected); } -.main[data-open="true"] .circle[data-checked="false"] { +.accordion[data-open="true"] .circle[data-checked="false"] { background-color: var(--UI-Text-Placeholder); } -.main[data-open="false"] .circle[data-checked="false"] { +.accordion[data-open="false"] .circle[data-checked="false"] { background-color: var(--Base-Surface-Subtle-Hover); } -.main[data-open="true"] { - grid-template-rows: 2.4em 1fr; +.accordion[data-open="true"] { + grid-template-rows: var(--header-height) 1fr; gap: var(--Spacing-x3); } @@ -86,7 +89,7 @@ } @media screen and (min-width: 768px) { - .main { + .accordion { gap: var(--Spacing-x3); grid-template-areas: "circle header" "circle content"; } @@ -95,11 +98,11 @@ top: var(--Spacing-x1); } - .main:not(:last-child) .iconWrapper::after { + .accordion:not(:last-child) .iconWrapper::after { position: absolute; left: 12px; bottom: calc(0px - var(--Spacing-x7)); - top: 24px; + top: var(--circle-height); content: ""; border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); diff --git a/components/LoginButton/index.tsx b/components/LoginButton/index.tsx index 6258f6e88..89cc3a42a 100644 --- a/components/LoginButton/index.tsx +++ b/components/LoginButton/index.tsx @@ -31,13 +31,14 @@ export default function LoginButton({ : login[lang] useEffect(() => { - document - .getElementById(trackingId) - ?.addEventListener("click", () => trackLoginClick(position)) + function trackLogin() { + trackLoginClick(position) + } + document.getElementById(trackingId)?.addEventListener("click", trackLogin) return () => { document .getElementById(trackingId) - ?.removeEventListener("click", () => trackLoginClick(position)) + ?.removeEventListener("click", trackLogin) } }, [position, trackingId]) diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css index 69044abb2..d50df8a15 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/card.module.css @@ -1,5 +1,4 @@ .label { - align-self: flex-start; background-color: var(--Base-Surface-Primary-light-Normal); border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Large); @@ -9,7 +8,6 @@ padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); transition: all 200ms ease; width: min(100%, 600px); - height: 100%; } .label:hover { diff --git a/components/TempDesignSystem/Link/link.module.css b/components/TempDesignSystem/Link/link.module.css index 948574ed2..3a997bddb 100644 --- a/components/TempDesignSystem/Link/link.module.css +++ b/components/TempDesignSystem/Link/link.module.css @@ -16,7 +16,7 @@ .breadcrumb { font-family: var(--typography-Footnote-Bold-fontFamily); font-size: var(--typography-Footnote-Bold-fontSize); - font-weight: 450; /* var(--typography-Footnote-Bold-fontWeight); */ + font-weight: 500; /* var(--typography-Footnote-Bold-fontWeight); */ letter-spacing: var(--typography-Footnote-Bold-letterSpacing); line-height: var(--typography-Footnote-Bold-lineHeight); } @@ -24,7 +24,7 @@ .link.breadcrumb { font-family: var(--typography-Footnote-Bold-fontFamily); font-size: var(--typography-Footnote-Bold-fontSize); - font-weight: 450; /* var(--typography-Footnote-Bold-fontWeight); */ + font-weight: 500; /* var(--typography-Footnote-Bold-fontWeight); */ letter-spacing: var(--typography-Footnote-Bold-letterSpacing); line-height: var(--typography-Footnote-Bold-lineHeight); } diff --git a/types/components/hotelReservation/enterDetails/details.ts b/types/components/hotelReservation/enterDetails/details.ts index 685fad18a..25004467a 100644 --- a/types/components/hotelReservation/enterDetails/details.ts +++ b/types/components/hotelReservation/enterDetails/details.ts @@ -6,6 +6,14 @@ import type { SafeUser } from "@/types/user" export type DetailsSchema = z.output<typeof guestDetailsSchema> +type MemberPrice = { price: number; currency: string } + export interface DetailsProps { user: SafeUser + memberPrice?: MemberPrice +} + +export type JoinScandicFriendsCardProps = { + name: string + memberPrice?: MemberPrice } From cc15dc2dc2d6f71726b3ffedd344d83ff104345b Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 09:17:58 +0100 Subject: [PATCH 061/101] fix(SW-917): fix correct link to map --- constants/routes/hotelReservation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants/routes/hotelReservation.js b/constants/routes/hotelReservation.js index c7882f159..ed7ae99c5 100644 --- a/constants/routes/hotelReservation.js +++ b/constants/routes/hotelReservation.js @@ -48,7 +48,7 @@ export function selectHotel(lang) { * @param {Lang} lang */ export function selectHotelMap(lang) { - return `${base(lang)}/map` + return `${base(lang)}/select-hotel/map` } /** From 60f1d268a96945d4d9c20b698855c98687b20c14 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 08:22:03 +0000 Subject: [PATCH 062/101] Merged in feat/SW-904-add-back-to-top-button (pull request #923) Feat/SW-904 add back to top button on hotel list page * feat(SW-904): Added back to top button * fix: removed alert on hotel listing page * Remove console.log Approved-by: Niclas Edenvin --- .../HotelReservation/HotelCard/index.tsx | 8 ---- .../HotelCardListing/index.tsx | 22 ++++++++- .../SelectHotel/SelectHotelMap/index.tsx | 15 ++----- .../SelectHotelMap/selectHotelMap.module.css | 11 +---- components/Icons/ArrowUp.tsx | 33 ++++++++++++++ components/Icons/index.tsx | 1 + .../backToTopButton.module.css | 45 +++++++++++++++++++ .../BackToTopButton/index.tsx | 20 +++++++++ 8 files changed, 124 insertions(+), 31 deletions(-) create mode 100644 components/Icons/ArrowUp.tsx create mode 100644 components/TempDesignSystem/BackToTopButton/backToTopButton.module.css create mode 100644 components/TempDesignSystem/BackToTopButton/index.tsx diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 808e9ac9f..1836d4130 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -7,7 +7,6 @@ import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" -import Alert from "@/components/TempDesignSystem/Alert" import Button from "@/components/TempDesignSystem/Button" import Divider from "@/components/TempDesignSystem/Divider" import Link from "@/components/TempDesignSystem/Link" @@ -133,13 +132,6 @@ export default function HotelCard({ hotel={hotelData} showCTA={true} /> - {hotelData.specialAlerts.length > 0 && ( - <div className={styles.specialAlerts}> - {hotelData.specialAlerts.map((alert) => ( - <Alert key={alert.id} type={alert.type} text={alert.text} /> - ))} - </div> - )} </section> <HotelPriceList price={price} hotelId={hotel.hotelData.operaId} /> </div> diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index ab2d45d3e..191c7acde 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,9 +1,12 @@ "use client" import { useSearchParams } from "next/navigation" -import { useMemo } from "react" +import { useEffect, useMemo, useState } from "react" +import { useIntl } from "react-intl" import { useHotelFilterStore } from "@/stores/hotel-filters" +import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" + import HotelCard from "../HotelCard" import { DEFAULT_SORT } from "../SelectHotel/HotelSorter" @@ -22,9 +25,11 @@ export default function HotelCardListing({ activeCard, onHotelCardHover, }: HotelCardListingProps) { + const intl = useIntl() const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) + const [showBackToTop, setShowBackToTop] = useState<boolean>(false) const sortBy = useMemo( () => searchParams.get("sort") ?? DEFAULT_SORT, @@ -82,6 +87,20 @@ export default function HotelCardListing({ return filteredHotels }, [activeFilters, sortedHotels, setResultCount]) + useEffect(() => { + const handleScroll = () => { + const hasScrolledPast = window.scrollY > 490 + setShowBackToTop(hasScrolledPast) + } + + window.addEventListener("scroll", handleScroll, { passive: true }) + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }) + } + return ( <section className={styles.hotelCards}> {hotels?.length @@ -95,6 +114,7 @@ export default function HotelCardListing({ /> )) : null} + {showBackToTop && <BackToTopButton onClick={scrollToTop} />} </section> ) } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 215c7ae66..ce3d2173d 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -7,8 +7,9 @@ import { useMediaQuery } from "usehooks-ts" import { selectHotel } from "@/constants/routes/hotelReservation" -import { CloseIcon, CloseLargeIcon } from "@/components/Icons" +import { ArrowUpIcon, CloseIcon, CloseLargeIcon } from "@/components/Icons" import InteractiveMap from "@/components/Maps/InteractiveMap" +import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton" import Button from "@/components/TempDesignSystem/Button" import useLang from "@/hooks/useLang" @@ -109,17 +110,7 @@ export default function SelectHotelMap({ activeHotelPin={activeHotelPin} setActiveHotelPin={setActiveHotelPin} /> - {showBackToTop && ( - <Button - intent="inverted" - size="small" - theme="base" - className={styles.backToTopButton} - onClick={scrollToTop} - > - {intl.formatMessage({ id: "Back to top" })} - </Button> - )} + {showBackToTop && <BackToTopButton onClick={scrollToTop} />} </div> <InteractiveMap closeButton={closeButton} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css index c3815945e..0aa2ee7bd 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/selectHotelMap.module.css @@ -23,10 +23,6 @@ height: 44px; } -.backToTopButton { - display: none; -} - @media (min-width: 768px) { .container .closeButton { display: flex; @@ -34,12 +30,7 @@ .container .listingContainer .filterContainer .filterContainerCloseButton { display: none; } - .backToTopButton { - position: fixed; - bottom: 24px; - left: 32px; - display: flex; - } + .listingContainer { background-color: var(--Base-Surface-Secondary-light-Normal); padding: var(--Spacing-x3) var(--Spacing-x4); diff --git a/components/Icons/ArrowUp.tsx b/components/Icons/ArrowUp.tsx new file mode 100644 index 000000000..d85323685 --- /dev/null +++ b/components/Icons/ArrowUp.tsx @@ -0,0 +1,33 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ArrowUpIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + <svg + className={classNames} + fill="none" + height="20" + viewBox="0 0 20 20" + width="20" + xmlns="http://www.w3.org/2000/svg" + {...props} + > + <mask + id="a" + width="20" + height="20" + x="0" + y="0" + maskUnits="userSpaceOnUse" + > + <path fill="#D9D9D9" d="M0 0h20v20H0z" /> + </mask> + <path + fill="#4D001B" + d="m9.219 6.541-4.021 4.021a.74.74 0 0 1-.552.235.778.778 0 0 1-.552-.245.796.796 0 0 1-.235-.552.74.74 0 0 1 .235-.552l5.354-5.355a.77.77 0 0 1 .849-.171.77.77 0 0 1 .255.171l5.354 5.355a.782.782 0 0 1 0 1.104.764.764 0 0 1-1.114 0l-4.01-4.01v9.135c0 .215-.077.4-.23.552a.752.752 0 0 1-.552.229.752.752 0 0 1-.552-.23.752.752 0 0 1-.23-.551V6.54Z" + /> + </svg> + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index 1bc195717..7ec502ca0 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -6,6 +6,7 @@ export { default as AirIcon } from "./Air" export { default as AirplaneIcon } from "./Airplane" export { default as AllergyIcon } from "./Allergy" export { default as ArrowRightIcon } from "./ArrowRight" +export { default as ArrowUpIcon } from "./ArrowUp" export { default as BarIcon } from "./Bar" export { default as BathtubIcon } from "./Bathtub" export { default as BedDoubleIcon } from "./BedDouble" diff --git a/components/TempDesignSystem/BackToTopButton/backToTopButton.module.css b/components/TempDesignSystem/BackToTopButton/backToTopButton.module.css new file mode 100644 index 000000000..4ce70972b --- /dev/null +++ b/components/TempDesignSystem/BackToTopButton/backToTopButton.module.css @@ -0,0 +1,45 @@ +.backToTopButton { + border-radius: var(--Corner-radius-Rounded); + cursor: pointer; + display: flex; + align-items: flex-end; + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + background-color: var(--Base-Surface-Primary-light-Normal); + color: var(--Base-Button-Secondary-On-Fill-Normal); + border: 2px solid var(--Base-Button-Secondary-On-Fill-Normal); + gap: var(--Spacing-x-half); + padding: var(--Spacing-x1); + text-align: center; + transition: + background-color 300ms ease, + color 300ms ease; + font-family: var(--typography-Body-Bold-fontFamily); + font-weight: 500; + font-size: var(--typography-Caption-Bold-fontSize); + line-height: var(--typography-Caption-Bold-lineHeight); + letter-spacing: 0.6%; + text-decoration: none; +} + +.backToTopButtonText { + display: none; +} + +@media (min-width: 768px) { + .backToTopButtonText { + display: initial; + } + .backToTopButton:hover { + background-color: var(--Base-Button-Tertiary-Fill-Normal); + color: var(--Base-Button-Tertiary-On-Fill-Hover); + } + .backToTopButton:hover > svg * { + fill: var(--Base-Button-Tertiary-On-Fill-Hover); + } + .backToTopButton { + padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); + } +} diff --git a/components/TempDesignSystem/BackToTopButton/index.tsx b/components/TempDesignSystem/BackToTopButton/index.tsx new file mode 100644 index 000000000..a50f8329f --- /dev/null +++ b/components/TempDesignSystem/BackToTopButton/index.tsx @@ -0,0 +1,20 @@ +"use client" + +import { Button as ButtonRAC } from "react-aria-components" +import { useIntl } from "react-intl" + +import { ArrowUpIcon } from "@/components/Icons" + +import styles from "./backToTopButton.module.css" + +export function BackToTopButton({ onClick }: { onClick: () => void }) { + const intl = useIntl() + return ( + <ButtonRAC className={styles.backToTopButton} onPress={onClick}> + <ArrowUpIcon color="burgundy" /> + <span className={styles.backToTopButtonText}> + {intl.formatMessage({ id: "Back to top" })} + </span> + </ButtonRAC> + ) +} From 94f97dffa994f916363e4d8309f7c4ab5d44e8db Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 09:50:08 +0100 Subject: [PATCH 063/101] fix(SW-565): Fixed that tooltip doesn't block anything. --- components/GuestsRoomsPicker/Form.tsx | 4 ++-- components/TempDesignSystem/Tooltip/index.tsx | 1 + components/TempDesignSystem/Tooltip/tooltip.module.css | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/GuestsRoomsPicker/Form.tsx b/components/GuestsRoomsPicker/Form.tsx index 01ded876a..062347761 100644 --- a/components/GuestsRoomsPicker/Form.tsx +++ b/components/GuestsRoomsPicker/Form.tsx @@ -99,7 +99,7 @@ export default function GuestsRoomsPickerDialog({ <Tooltip heading={disabledBookingOptionsHeader} text={disabledBookingOptionsText} - position="top" + position="bottom" arrow="left" > {rooms.length < 4 ? ( @@ -124,7 +124,7 @@ export default function GuestsRoomsPickerDialog({ <Tooltip heading={disabledBookingOptionsHeader} text={disabledBookingOptionsText} - position="top" + position="bottom" arrow="left" > {rooms.length < 4 ? ( diff --git a/components/TempDesignSystem/Tooltip/index.tsx b/components/TempDesignSystem/Tooltip/index.tsx index 033fc9a04..d73252161 100644 --- a/components/TempDesignSystem/Tooltip/index.tsx +++ b/components/TempDesignSystem/Tooltip/index.tsx @@ -28,6 +28,7 @@ export function Tooltip<P extends TooltipPosition>({ role="tooltip" aria-label={text} onClick={handleToggle} + onTouchStart={handleToggle} data-active={isActive} > <div className={className}> diff --git a/components/TempDesignSystem/Tooltip/tooltip.module.css b/components/TempDesignSystem/Tooltip/tooltip.module.css index e25433f7c..b0ae8cf4f 100644 --- a/components/TempDesignSystem/Tooltip/tooltip.module.css +++ b/components/TempDesignSystem/Tooltip/tooltip.module.css @@ -16,6 +16,7 @@ transition: opacity 0.3s; max-width: 200px; min-width: 150px; + height: fit-content; } .tooltipContainer:hover .tooltip { From ebbdecf8d8cb92c7d042d6c9b8d5d2dad001c993 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 10:34:21 +0100 Subject: [PATCH 064/101] fix(SW-565) fix select hight to see its scrollable --- .../GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx index 8252f2d2a..b39821483 100644 --- a/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/ChildInfoSelector.tsx @@ -94,7 +94,7 @@ export default function ChildInfoSelector({ updateSelectedAge(key as number) }} placeholder={ageLabel} - maxHeight={150} + maxHeight={180} {...register(ageFieldName, { required: true, })} From e1a2532b3f56b4ccd5a11eb455eea8b4e6c9882f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= <joakim.jaderberg@scandichotels.com> Date: Tue, 19 Nov 2024 10:57:52 +0100 Subject: [PATCH 065/101] fix: use suspense over loading.tsx --- .../my-pages/@breadcrumbs/[...path]/loading.tsx | 5 ----- .../my-pages/@breadcrumbs/[...path]/page.tsx | 9 ++++++++- app/[lang]/(live)/@bookingwidget/default.tsx | 14 -------------- 3 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx delete mode 100644 app/[lang]/(live)/@bookingwidget/default.tsx diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx deleted file mode 100644 index aed94918c..000000000 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" - -export default function Loading() { - return <BreadcrumbsSkeleton /> -} diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index a5b818f77..6775fd188 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -1,4 +1,7 @@ +import { Suspense } from "react" + import Breadcrumbs from "@/components/Breadcrumbs" +import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton" import { setLang } from "@/i18n/serverContext" import { LangParams, PageArgs } from "@/types/params" @@ -6,5 +9,9 @@ import { LangParams, PageArgs } from "@/types/params" export default function AllBreadcrumbs({ params }: PageArgs<LangParams>) { setLang(params.lang) - return <Breadcrumbs /> + return ( + <Suspense fallback={<BreadcrumbsSkeleton />}> + <Breadcrumbs /> + </Suspense> + ) } diff --git a/app/[lang]/(live)/@bookingwidget/default.tsx b/app/[lang]/(live)/@bookingwidget/default.tsx deleted file mode 100644 index 6f2a78256..000000000 --- a/app/[lang]/(live)/@bookingwidget/default.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Suspense } from "react" - -import Loading from "./loading" -import Page from "./page" - -import { PageArgs } from "@/types/params" - -export default function Default(props: PageArgs<{}, URLSearchParams>) { - return ( - <Suspense fallback={<Loading />}> - <Page {...props} /> - </Suspense> - ) -} From 1b3999a05031e3906c2e662240917a85eef21a22 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra <erik.tiekstra@scandichotels.com> Date: Tue, 19 Nov 2024 09:50:34 +0100 Subject: [PATCH 066/101] fix(SW-769): removed categoryname enum from pointOfInterestSchema and use z.string() instead --- components/Maps/Markers/utils.ts | 15 +++-------- server/routers/hotels/output.ts | 5 +--- server/routers/hotels/utils.ts | 43 +++++++++++++----------------- types/components/maps/poiMarker.ts | 7 ++--- types/hotel.ts | 20 -------------- 5 files changed, 26 insertions(+), 64 deletions(-) diff --git a/components/Maps/Markers/utils.ts b/components/Maps/Markers/utils.ts index fd574eb87..51a6ed9a9 100644 --- a/components/Maps/Markers/utils.ts +++ b/components/Maps/Markers/utils.ts @@ -1,22 +1,15 @@ import { IconName } from "@/types/components/icon" -import { - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" +import { PointOfInterestGroupEnum } from "@/types/hotel" export function getIconByPoiGroupAndCategory( group: PointOfInterestGroupEnum, - category?: PointOfInterestCategoryNameEnum + category?: string ) { switch (group) { case PointOfInterestGroupEnum.PUBLIC_TRANSPORT: - return category === PointOfInterestCategoryNameEnum.AIRPORT - ? IconName.Airplane - : IconName.Train + return category === "Airport" ? IconName.Airplane : IconName.Train case PointOfInterestGroupEnum.ATTRACTIONS: - return category === PointOfInterestCategoryNameEnum.MUSEUM - ? IconName.Museum - : IconName.Camera + return category === "Museum" ? IconName.Museum : IconName.Camera case PointOfInterestGroupEnum.BUSINESS: return IconName.Business case PointOfInterestGroupEnum.PARKING: diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d4f3d89c9..1af9ba44c 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -13,7 +13,6 @@ import { AlertTypeEnum } from "@/types/enums/alert" import { CurrencyEnum } from "@/types/enums/currency" import { FacilityEnum } from "@/types/enums/facilities" import { PackageTypeEnum } from "@/types/enums/packages" -import { PointOfInterestCategoryNameEnum } from "@/types/hotel" const ratingsSchema = z .object({ @@ -199,14 +198,12 @@ const rewardNightSchema = z.object({ }), }) -const poiCategoryNames = z.nativeEnum(PointOfInterestCategoryNameEnum) - export const pointOfInterestSchema = z .object({ name: z.string(), distance: z.number(), category: z.object({ - name: poiCategoryNames, + name: z.string(), group: z.string(), }), location: locationSchema, diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index 6d384b3cf..c3e785a59 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -12,39 +12,34 @@ import { type Countries, } from "./output" -import type { RequestOptionsWithOutBody } from "@/types/fetch" -import { - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" -import { HotelLocation } from "@/types/trpc/routers/hotel/locations" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" +import type { RequestOptionsWithOutBody } from "@/types/fetch" +import { PointOfInterestGroupEnum } from "@/types/hotel" +import { HotelLocation } from "@/types/trpc/routers/hotel/locations" -export function getPoiGroupByCategoryName( - category: PointOfInterestCategoryNameEnum -) { +export function getPoiGroupByCategoryName(category: string) { switch (category) { - case PointOfInterestCategoryNameEnum.AIRPORT: - case PointOfInterestCategoryNameEnum.BUS_TERMINAL: - case PointOfInterestCategoryNameEnum.TRANSPORTATIONS: + case "Airport": + case "Bus terminal": + case "Transportations": return PointOfInterestGroupEnum.PUBLIC_TRANSPORT - case PointOfInterestCategoryNameEnum.AMUSEMENT_PARK: - case PointOfInterestCategoryNameEnum.MUSEUM: - case PointOfInterestCategoryNameEnum.SPORTS: - case PointOfInterestCategoryNameEnum.THEATRE: - case PointOfInterestCategoryNameEnum.TOURIST: - case PointOfInterestCategoryNameEnum.ZOO: + case "Amusement park": + case "Museum": + case "Sports": + case "Theatre": + case "Tourist": + case "Zoo": return PointOfInterestGroupEnum.ATTRACTIONS - case PointOfInterestCategoryNameEnum.NEARBY_COMPANIES: - case PointOfInterestCategoryNameEnum.FAIR: + case "Nearby companies": + case "Fair": return PointOfInterestGroupEnum.BUSINESS - case PointOfInterestCategoryNameEnum.PARKING_GARAGE: + case "Parking / Garage": return PointOfInterestGroupEnum.PARKING - case PointOfInterestCategoryNameEnum.SHOPPING: - case PointOfInterestCategoryNameEnum.RESTAURANT: + case "Shopping": + case "Restaurant": return PointOfInterestGroupEnum.SHOPPING_DINING - case PointOfInterestCategoryNameEnum.HOSPITAL: + case "Hospital": default: return PointOfInterestGroupEnum.LOCATION } diff --git a/types/components/maps/poiMarker.ts b/types/components/maps/poiMarker.ts index 34fad0e6f..abab6fc9d 100644 --- a/types/components/maps/poiMarker.ts +++ b/types/components/maps/poiMarker.ts @@ -2,14 +2,11 @@ import { poiVariants } from "@/components/Maps/Markers/Poi/variants" import type { VariantProps } from "class-variance-authority" -import { - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" +import type { PointOfInterestGroupEnum } from "@/types/hotel" export interface PoiMarkerProps extends VariantProps<typeof poiVariants> { group: PointOfInterestGroupEnum - categoryName?: PointOfInterestCategoryNameEnum + categoryName?: string size?: number className?: string } diff --git a/types/hotel.ts b/types/hotel.ts index 5052caded..aadaab740 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -26,26 +26,6 @@ export type GalleryImage = z.infer<typeof imageSchema> export type PointOfInterest = z.output<typeof pointOfInterestSchema> -export enum PointOfInterestCategoryNameEnum { - AIRPORT = "Airport", - AMUSEMENT_PARK = "Amusement park", - BUS_TERMINAL = "Bus terminal", - FAIR = "Fair", - HOSPITAL = "Hospital", - HOTEL = "Hotel", - MARKETING_CITY = "Marketing city", - MUSEUM = "Museum", - NEARBY_COMPANIES = "Nearby companies", - PARKING_GARAGE = "Parking / Garage", - RESTAURANT = "Restaurant", - SHOPPING = "Shopping", - SPORTS = "Sports", - THEATRE = "Theatre", - TOURIST = "Tourist", - TRANSPORTATIONS = "Transportations", - ZOO = "Zoo", -} - export enum PointOfInterestGroupEnum { PUBLIC_TRANSPORT = "Public transport", ATTRACTIONS = "Attractions", From 75c5c97841f83cfdef6d8c72f5a403dfacc706ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20J=C3=A4derberg?= <joakim.jaderberg@scandichotels.com> Date: Tue, 19 Nov 2024 12:03:49 +0100 Subject: [PATCH 067/101] chore: upgrade nextjs@14.2.18 --- next-env.d.ts | 2 +- package-lock.json | 88 +++++++++++++++++++++++------------------------ package.json | 2 +- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// <reference types="next/image-types/global" /> // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/package-lock.json b/package-lock.json index a2d63c0f7..e611379b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "graphql-tag": "^2.12.6", "immer": "10.1.1", "libphonenumber-js": "^1.10.60", - "next": "^14.2.7", + "next": "^14.2.18", "next-auth": "^5.0.0-beta.19", "react": "^18", "react-day-picker": "^9.0.8", @@ -3425,9 +3425,9 @@ } }, "node_modules/@next/env": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.7.tgz", - "integrity": "sha512-OTx9y6I3xE/eih+qtthppwLytmpJVPM5PPoJxChFsbjIEFXIayG0h/xLzefHGJviAa3Q5+Fd+9uYojKkHDKxoQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz", + "integrity": "sha512-2vWLOUwIPgoqMJKG6dt35fVXVhgM09tw4tK3/Q34GFXDrfiHlG7iS33VA4ggnjWxjiz9KV5xzfsQzJX6vGAekA==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -3440,9 +3440,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.7.tgz", - "integrity": "sha512-UhZGcOyI9LE/tZL3h9rs/2wMZaaJKwnpAyegUVDGZqwsla6hMfeSj9ssBWQS9yA4UXun3pPhrFLVnw5KXZs3vw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.18.tgz", + "integrity": "sha512-tOBlDHCjGdyLf0ube/rDUs6VtwNOajaWV+5FV/ajPgrvHeisllEdymY/oDgv2cx561+gJksfMUtqf8crug7sbA==", "cpu": [ "arm64" ], @@ -3456,9 +3456,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.7.tgz", - "integrity": "sha512-ys2cUgZYRc+CbyDeLAaAdZgS7N1Kpyy+wo0b/gAj+SeOeaj0Lw/q+G1hp+DuDiDAVyxLBCJXEY/AkhDmtihUTA==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.18.tgz", + "integrity": "sha512-uJCEjutt5VeJ30jjrHV1VIHCsbMYnEqytQgvREx+DjURd/fmKy15NaVK4aR/u98S1LGTnjq35lRTnRyygglxoA==", "cpu": [ "x64" ], @@ -3472,9 +3472,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.7.tgz", - "integrity": "sha512-2xoWtE13sUJ3qrC1lwE/HjbDPm+kBQYFkkiVECJWctRASAHQ+NwjMzgrfqqMYHfMxFb5Wws3w9PqzZJqKFdWcQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.18.tgz", + "integrity": "sha512-IL6rU8vnBB+BAm6YSWZewc+qvdL1EaA+VhLQ6tlUc0xp+kkdxQrVqAnh8Zek1ccKHlTDFRyAft0e60gteYmQ4A==", "cpu": [ "arm64" ], @@ -3488,9 +3488,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.7.tgz", - "integrity": "sha512-+zJ1gJdl35BSAGpkCbfyiY6iRTaPrt3KTl4SF/B1NyELkqqnrNX6cp4IjjjxKpd64/7enI0kf6b9O1Uf3cL0pw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.18.tgz", + "integrity": "sha512-RCaENbIZqKKqTlL8KNd+AZV/yAdCsovblOpYFp0OJ7ZxgLNbV5w23CUU1G5On+0fgafrsGcW+GdMKdFjaRwyYA==", "cpu": [ "arm64" ], @@ -3504,9 +3504,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.7.tgz", - "integrity": "sha512-m6EBqrskeMUzykBrv0fDX/28lWIBGhMzOYaStp0ihkjzIYJiKUOzVYD1gULHc8XDf5EMSqoH/0/TRAgXqpQwmw==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.18.tgz", + "integrity": "sha512-3kmv8DlyhPRCEBM1Vavn8NjyXtMeQ49ID0Olr/Sut7pgzaQTo4h01S7Z8YNE0VtbowyuAL26ibcz0ka6xCTH5g==", "cpu": [ "x64" ], @@ -3520,9 +3520,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.7.tgz", - "integrity": "sha512-gUu0viOMvMlzFRz1r1eQ7Ql4OE+hPOmA7smfZAhn8vC4+0swMZaZxa9CSIozTYavi+bJNDZ3tgiSdMjmMzRJlQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.18.tgz", + "integrity": "sha512-mliTfa8seVSpTbVEcKEXGjC18+TDII8ykW4a36au97spm9XMPqQTpdGPNBJ9RySSFw9/hLuaCMByluQIAnkzlw==", "cpu": [ "x64" ], @@ -3536,9 +3536,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.7.tgz", - "integrity": "sha512-PGbONHIVIuzWlYmLvuFKcj+8jXnLbx4WrlESYlVnEzDsa3+Q2hI1YHoXaSmbq0k4ZwZ7J6sWNV4UZfx1OeOlbQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.18.tgz", + "integrity": "sha512-J5g0UFPbAjKYmqS3Cy7l2fetFmWMY9Oao32eUsBPYohts26BdrMUyfCJnZFQkX9npYaHNDOWqZ6uV9hSDPw9NA==", "cpu": [ "arm64" ], @@ -3552,9 +3552,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.7.tgz", - "integrity": "sha512-BiSY5umlx9ed5RQDoHcdbuKTUkuFORDqzYKPHlLeS+STUWQKWziVOn3Ic41LuTBvqE0TRJPKpio9GSIblNR+0w==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.18.tgz", + "integrity": "sha512-Ynxuk4ZgIpdcN7d16ivJdjsDG1+3hTvK24Pp8DiDmIa2+A4CfhJSEHHVndCHok6rnLUzAZD+/UOKESQgTsAZGg==", "cpu": [ "ia32" ], @@ -3568,9 +3568,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.7.tgz", - "integrity": "sha512-pxsI23gKWRt/SPHFkDEsP+w+Nd7gK37Hpv0ngc5HpWy2e7cKx9zR/+Q2ptAUqICNTecAaGWvmhway7pj/JLEWA==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.18.tgz", + "integrity": "sha512-dtRGMhiU9TN5nyhwzce+7c/4CCeykYS+ipY/4mIrGzJ71+7zNo55ZxCB7cAVuNqdwtYniFNR2c9OFQ6UdFIMcg==", "cpu": [ "x64" ], @@ -15460,12 +15460,12 @@ } }, "node_modules/next": { - "version": "14.2.7", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.7.tgz", - "integrity": "sha512-4Qy2aK0LwH4eQiSvQWyKuC7JXE13bIopEQesWE0c/P3uuNRnZCQanI0vsrMLmUQJLAto+A+/8+sve2hd+BQuOQ==", + "version": "14.2.18", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.18.tgz", + "integrity": "sha512-H9qbjDuGivUDEnK6wa+p2XKO+iMzgVgyr9Zp/4Iv29lKa+DYaxJGjOeEA+5VOvJh/M7HLiskehInSa0cWxVXUw==", "license": "MIT", "dependencies": { - "@next/env": "14.2.7", + "@next/env": "14.2.18", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -15480,15 +15480,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.7", - "@next/swc-darwin-x64": "14.2.7", - "@next/swc-linux-arm64-gnu": "14.2.7", - "@next/swc-linux-arm64-musl": "14.2.7", - "@next/swc-linux-x64-gnu": "14.2.7", - "@next/swc-linux-x64-musl": "14.2.7", - "@next/swc-win32-arm64-msvc": "14.2.7", - "@next/swc-win32-ia32-msvc": "14.2.7", - "@next/swc-win32-x64-msvc": "14.2.7" + "@next/swc-darwin-arm64": "14.2.18", + "@next/swc-darwin-x64": "14.2.18", + "@next/swc-linux-arm64-gnu": "14.2.18", + "@next/swc-linux-arm64-musl": "14.2.18", + "@next/swc-linux-x64-gnu": "14.2.18", + "@next/swc-linux-x64-musl": "14.2.18", + "@next/swc-win32-arm64-msvc": "14.2.18", + "@next/swc-win32-ia32-msvc": "14.2.18", + "@next/swc-win32-x64-msvc": "14.2.18" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/package.json b/package.json index 4afcb9bf1..5871b48b4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "graphql-tag": "^2.12.6", "immer": "10.1.1", "libphonenumber-js": "^1.10.60", - "next": "^14.2.7", + "next": "^14.2.18", "next-auth": "^5.0.0-beta.19", "react": "^18", "react-day-picker": "^9.0.8", From 744af22b08b2e524e6fa8fa2c66db81eec8b85da Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson <simon.emanuelsson@scandichotels.com> Date: Tue, 19 Nov 2024 11:24:17 +0100 Subject: [PATCH 068/101] fix: make sure all searchparams are used in redirect --- .../(standard)/step/@summary/page.tsx | 6 +- .../hotelreservation/(standard)/step/page.tsx | 5 +- .../EnterDetails/Summary/index.tsx | 58 +++++++++---------- .../SelectRate/Rooms/utils.ts | 7 ++- providers/StepsProvider.tsx | 7 ++- server/routers/hotels/output.ts | 11 +++- server/routers/hotels/query.ts | 2 +- stores/steps.ts | 17 +++--- types/providers/steps.ts | 1 + 9 files changed, 65 insertions(+), 49 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index 337d501b3..0444913f1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -83,10 +83,10 @@ export default async function SummaryPage({ price: availability.publicRate.localPrice.pricePerStay, currency: availability.publicRate.localPrice.currency, }, - euro: availability.publicRate.requestedPrice + euro: availability.publicRate?.requestedPrice ? { - price: availability.publicRate.requestedPrice.pricePerStay, - currency: availability.publicRate.requestedPrice.currency, + price: availability.publicRate?.requestedPrice.pricePerStay, + currency: availability.publicRate?.requestedPrice.currency, } : undefined, } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index 7ffe57562..d175fc25f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,6 +1,6 @@ import "./enterDetailsLayout.css" -import { notFound, redirect, RedirectType } from "next/navigation" +import { notFound } from "next/navigation" import { getBreakfastPackages, @@ -38,6 +38,8 @@ export default async function StepPage({ }: PageArgs<LangParams, SelectRateSearchParams & { step: StepEnum }>) { const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) + selectRoomParams.delete("step") + const searchParamsString = selectRoomParams.toString() const { hotel: hotelId, rooms, @@ -123,6 +125,7 @@ export default async function StepPage({ bedTypes={roomAvailability.bedTypes} breakfastPackages={breakfastPackages} isMember={!!user} + searchParams={searchParamsString} step={searchParams.step} > <section> diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 7507a6945..5b0a6a420 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -81,7 +81,6 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { useEffect(() => { setChosenBed(bedType) - setChosenBreakfast(breakfast) if (breakfast || breakfast === false) { setChosenBreakfast(breakfast) @@ -94,9 +93,9 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { euro: room.euroPrice && roomsPriceEuro ? { - price: roomsPriceEuro, - currency: room.euroPrice.currency, - } + price: roomsPriceEuro, + currency: room.euroPrice.currency, + } : undefined, }) } else { @@ -108,11 +107,11 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { euro: room.euroPrice && roomsPriceEuro ? { - price: - roomsPriceEuro + - parseInt(breakfast.requestedPrice.totalPrice), - currency: room.euroPrice.currency, - } + price: + roomsPriceEuro + + parseInt(breakfast.requestedPrice.totalPrice), + currency: room.euroPrice.currency, + } : undefined, }) } @@ -199,24 +198,24 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { </div> {room.packages ? room.packages.map((roomPackage) => ( - <div className={styles.entry} key={roomPackage.code}> - <div> - <Body color="uiTextHighContrast"> - {roomPackage.description} - </Body> - </div> + <div className={styles.entry} key={roomPackage.code}> + <div> + <Body color="uiTextHighContrast"> + {roomPackage.description} + </Body> + </div> - <Caption color="uiTextHighContrast"> - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: roomPackage.localPrice.price, - currency: roomPackage.localPrice.currency, - } - )} - </Caption> - </div> - )) + <Caption color="uiTextHighContrast"> + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: roomPackage.localPrice.price, + currency: roomPackage.localPrice.currency, + } + )} + </Caption> + </div> + )) : null} {chosenBed ? ( <div className={styles.entry}> @@ -263,9 +262,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { )} </Caption> </div> - ) : null - } - </div > + ) : null} + </div> <Divider color="primaryLightSubtle" /> <div className={styles.total}> <div className={styles.entry}> @@ -306,6 +304,6 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { </div> <Divider className={styles.bottomDivider} color="primaryLightSubtle" /> </div> - </section > + </section> ) } diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index af80e1f4d..71b2149cc 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -31,7 +31,10 @@ export function filterDuplicateRoomTypesByLowestPrice( products.forEach((product) => { const { productType } = product - const publicProduct = productType.public + const publicProduct = productType.public || { + requestedPrice: null, + localPrice: null, + } const memberProduct = productType.member || { requestedPrice: null, localPrice: null, @@ -53,7 +56,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Number(memberRequestedPrice?.pricePerNight) ?? Infinity ) const currentLocalPrice = Math.min( - Number(publicLocalPrice.pricePerNight) ?? Infinity, + Number(publicLocalPrice?.pricePerNight) ?? Infinity, Number(memberLocalPrice?.pricePerNight) ?? Infinity ) diff --git a/providers/StepsProvider.tsx b/providers/StepsProvider.tsx index 87594be02..9aaf6166f 100644 --- a/providers/StepsProvider.tsx +++ b/providers/StepsProvider.tsx @@ -1,4 +1,5 @@ "use client" +import { useRouter } from "next/navigation" import { useRef } from "react" import { useDetailsStore } from "@/stores/details" @@ -14,6 +15,7 @@ export default function StepsProvider({ breakfastPackages, children, isMember, + searchParams, step, }: StepsProviderProps) { const storeRef = useRef<StepsStore>() @@ -21,6 +23,7 @@ export default function StepsProvider({ const updateBreakfast = useDetailsStore( (state) => state.actions.updateBreakfast ) + const router = useRouter() if (!storeRef.current) { const noBedChoices = bedTypes.length === 1 @@ -41,7 +44,9 @@ export default function StepsProvider({ step, isMember, noBedChoices, - noBreakfast + noBreakfast, + searchParams, + router.push ) } diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 1af9ba44c..41f9a6b52 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -512,7 +512,16 @@ export const productTypePriceSchema = z.object({ const productSchema = z.object({ productType: z.object({ - public: productTypePriceSchema, + public: productTypePriceSchema.default({ + rateCode: "", + rateType: "", + localPrice: { + currency: "SEK", + pricePerNight: 0, + pricePerStay: 0, + }, + requestedPrice: undefined, + }), member: productTypePriceSchema.optional(), }), }) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 7f2ef84c0..34a1b3722 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -731,7 +731,7 @@ export const hotelQueryRouter = router({ const rateTypes = selectedRoom.products.find( (rate) => - rate.productType.public.rateCode === rateCode || + rate.productType.public?.rateCode === rateCode || rate.productType.member?.rateCode === rateCode ) diff --git a/stores/steps.ts b/stores/steps.ts index f1e456af2..cf14f6768 100644 --- a/stores/steps.ts +++ b/stores/steps.ts @@ -1,6 +1,7 @@ "use client" import merge from "deepmerge" import { produce } from "immer" +import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" import { useContext } from "react" import { create, useStore } from "zustand" @@ -18,17 +19,13 @@ import { StepEnum } from "@/types/enums/step" import type { DetailsState } from "@/types/stores/details" import type { StepState } from "@/types/stores/steps" -function push(data: Record<string, string>, url: string) { - if (typeof window !== "undefined") { - window.history.pushState(data, "", url + window.location.search) - } -} - export function createStepsStore( currentStep: StepEnum, isMember: boolean, noBedChoices: boolean, - noBreakfast: boolean + noBreakfast: boolean, + searchParams: string, + push: AppRouterInstance["push"] ) { const isBrowser = typeof window !== "undefined" const steps = [ @@ -51,14 +48,14 @@ export function createStepsStore( steps.splice(1, 1) if (currentStep === StepEnum.breakfast) { currentStep = steps[1] - push({ step: currentStep }, currentStep) + push(`${currentStep}?${searchParams}`) } } if (noBedChoices) { if (currentStep === StepEnum.selectBed) { currentStep = steps[1] - push({ step: currentStep }, currentStep) + push(`${currentStep}?${searchParams}`) } } @@ -94,7 +91,7 @@ export function createStepsStore( if (!validPaths.includes(currentStep) && isBrowser) { // We will always have at least one valid path currentStep = validPaths.pop()! - push({ step: currentStep }, currentStep) + push(`${currentStep}?${searchParams}`) } } diff --git a/types/providers/steps.ts b/types/providers/steps.ts index 8c24fdc8f..9ba0361eb 100644 --- a/types/providers/steps.ts +++ b/types/providers/steps.ts @@ -6,5 +6,6 @@ export interface StepsProviderProps extends React.PropsWithChildren { bedTypes: BedTypeSelection[] breakfastPackages: BreakfastPackage[] | null isMember: boolean + searchParams: string step: StepEnum } From 11cff0ab11616762cbd0d78f2346cca3f2948c73 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 13:03:42 +0100 Subject: [PATCH 069/101] feat(SW-913): Hide bookingwidget from cms --- components/BookingWidget/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/BookingWidget/index.tsx b/components/BookingWidget/index.tsx index c7674b9d7..ecff629c6 100644 --- a/components/BookingWidget/index.tsx +++ b/components/BookingWidget/index.tsx @@ -1,4 +1,4 @@ -import { getLocations } from "@/lib/trpc/memoizedRequests" +import { getLocations, getSiteConfig } from "@/lib/trpc/memoizedRequests" import BookingWidgetClient from "./Client" @@ -13,8 +13,9 @@ export default async function BookingWidget({ searchParams, }: BookingWidgetProps) { const locations = await getLocations() + const siteConfig = await getSiteConfig() - if (!locations || "error" in locations) { + if (!locations || "error" in locations || siteConfig?.bookingWidgetDisabled) { return null } From b7ffc8588d70e4748b76dec0d3bec3a3e2b447b2 Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Tue, 19 Nov 2024 12:23:06 +0000 Subject: [PATCH 070/101] Merged in fix/small-UI-change-select-hotel (pull request #931) fix: small ui fix hotel card * fix: small ui fix hotel card Approved-by: Pontus Dreij --- .../HotelCard/HotelPriceList/hotelPriceList.module.css | 7 ++++++- components/HotelReservation/HotelCardListing/index.tsx | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css index bd81f1170..fb67d45d9 100644 --- a/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css +++ b/components/HotelReservation/HotelCard/HotelPriceList/hotelPriceList.module.css @@ -15,7 +15,6 @@ display: flex; flex-direction: column; gap: var(--Spacing-x-one-and-half); - max-width: 260px; } .divider { @@ -38,3 +37,9 @@ font-weight: 400; font-size: var(--typography-Caption-Regular-fontSize); } + +@media screen and (min-width: 1367px) { + .prices { + max-width: 260px; + } +} diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 191c7acde..6ebf3006e 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -1,7 +1,6 @@ "use client" import { useSearchParams } from "next/navigation" import { useEffect, useMemo, useState } from "react" -import { useIntl } from "react-intl" import { useHotelFilterStore } from "@/stores/hotel-filters" @@ -25,7 +24,6 @@ export default function HotelCardListing({ activeCard, onHotelCardHover, }: HotelCardListingProps) { - const intl = useIntl() const searchParams = useSearchParams() const activeFilters = useHotelFilterStore((state) => state.activeFilters) const setResultCount = useHotelFilterStore((state) => state.setResultCount) From 602e7ea2ce9b5beeadaa3126c67e528de5cba630 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Tue, 19 Nov 2024 13:25:47 +0100 Subject: [PATCH 071/101] fix: handle undefined pricing --- components/HotelReservation/SelectRate/Rooms/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index 71b2149cc..96d8a8488 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -66,7 +66,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Math.min( Number( previousLowest.products[0].productType.public.requestedPrice - .pricePerNight + ?.pricePerNight ) ?? Infinity, Number( previousLowest.products[0].productType.member?.requestedPrice @@ -77,7 +77,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Math.min( Number( previousLowest.products[0].productType.public.requestedPrice - .pricePerNight + ?.pricePerNight ) ?? Infinity, Number( previousLowest.products[0].productType.member?.requestedPrice From c2ef7f70746a8957045efb654a980ab267e6f47d Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Tue, 19 Nov 2024 13:41:08 +0100 Subject: [PATCH 072/101] fix: padding issues --- .../EnterDetails/Details/details.module.css | 1 - .../SectionAccordion/sectionAccordion.module.css | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/HotelReservation/EnterDetails/Details/details.module.css b/components/HotelReservation/EnterDetails/Details/details.module.css index c6571ea9e..8781a9be2 100644 --- a/components/HotelReservation/EnterDetails/Details/details.module.css +++ b/components/HotelReservation/EnterDetails/Details/details.module.css @@ -1,7 +1,6 @@ .form { display: grid; gap: var(--Spacing-x3); - margin-bottom: var(--Spacing-x3); } .container { diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 0bbcf851c..48d387add 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -79,7 +79,10 @@ .accordion[data-open="true"] { grid-template-rows: var(--header-height) 1fr; - gap: var(--Spacing-x3); +} + +.accordion[data-open="true"] .content { + padding-bottom: var(--Spacing-x3); } .content { @@ -90,7 +93,7 @@ @media screen and (min-width: 768px) { .accordion { - gap: var(--Spacing-x3); + column-gap: var(--Spacing-x3); grid-template-areas: "circle header" "circle content"; } From d43c29e394489329d80e43af35e17a8c52029090 Mon Sep 17 00:00:00 2001 From: Christel Westerberg <christel.westerberg@scandichotels.com> Date: Tue, 19 Nov 2024 14:06:09 +0100 Subject: [PATCH 073/101] fix: disable button on not isComplete --- .../EnterDetails/SectionAccordion/index.tsx | 10 ++++++++-- .../SectionAccordion/sectionAccordion.module.css | 6 +++++- components/HotelReservation/SelectRate/Rooms/utils.ts | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index b96ef8ef8..488d941b5 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -75,7 +75,11 @@ export default function SectionAccordion({ </div> </div> <header className={styles.header}> - <button onClick={onModify} className={styles.modifyButton}> + <button + onClick={onModify} + disabled={!isComplete} + className={styles.modifyButton} + > <Footnote className={styles.title} asChild @@ -94,7 +98,9 @@ export default function SectionAccordion({ )} </button> </header> - <div className={styles.content}>{children}</div> + <div className={styles.content}> + <div className={styles.contentWrapper}>{children}</div> + </div> </div> ) } diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 48d387add..125905317 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -33,6 +33,10 @@ padding: 0; } +.modifyButton:disabled { + cursor: default; +} + .title { grid-area: title; text-align: start; @@ -81,7 +85,7 @@ grid-template-rows: var(--header-height) 1fr; } -.accordion[data-open="true"] .content { +.contentWrapper { padding-bottom: var(--Spacing-x3); } diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index 96d8a8488..fbd353aa1 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -88,7 +88,7 @@ export function filterDuplicateRoomTypesByLowestPrice( Math.min( Number( previousLowest.products[0].productType.public.localPrice - .pricePerNight + ?.pricePerNight ) ?? Infinity, Number( previousLowest.products[0].productType.member?.localPrice From ef2860dd8ece3e4a4f0d66b400e50de697b75266 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 15:11:13 +0100 Subject: [PATCH 074/101] fix: hide occupancy and roomSize if undefined --- .../RoomSelection/RoomCard/index.tsx | 97 ++++++++++--------- .../RoomCard/roomCard.module.css | 1 + 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index d69391122..6c477d39f 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -112,56 +112,61 @@ export default function RoomCard({ : "default", }) + console.log(occupancy) + return ( <div className={classNames}> <div> - {mainImage && ( - <div className={styles.imageContainer}> - <div className={styles.chipContainer}> - {roomConfiguration.roomsLeft < 5 && ( - <span className={styles.chip}> - <Footnote - color="burgundy" - textTransform="uppercase" - >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote> - </span> - )} - {roomConfiguration.features - .filter((feature) => selectedPackages.includes(feature.code)) - .map((feature) => ( - <span className={styles.chip} key={feature.code}> - {createElement(getIconForFeatureCode(feature.code), { - width: 16, - height: 16, - color: "burgundy", - })} - </span> - ))} - </div> - {/*NOTE: images from the test API are hosted on test3.scandichotels.com, - which can't be accessed unless on Scandic's Wifi or using Citrix. */} - <ImageGallery - images={images} - title={roomConfiguration.roomType} - fill - /> - </div> - )} - <div className={styles.specification}> - <Caption color="uiTextMediumContrast" className={styles.guests}> - {intl.formatMessage( - { - id: "booking.guests", - }, - { nrOfGuests: occupancy?.total } + <div className={styles.imageContainer}> + <div className={styles.chipContainer}> + {roomConfiguration.roomsLeft < 5 && ( + <span className={styles.chip}> + <Footnote + color="burgundy" + textTransform="uppercase" + >{`${roomConfiguration.roomsLeft} ${intl.formatMessage({ id: "Left" })}`}</Footnote> + </span> )} - </Caption> - <Caption color="uiTextMediumContrast"> - {roomSize?.min === roomSize?.max - ? roomSize?.min - : `${roomSize?.min}-${roomSize?.max}`} - m² - </Caption> + {roomConfiguration.features + .filter((feature) => selectedPackages.includes(feature.code)) + .map((feature) => ( + <span className={styles.chip} key={feature.code}> + {createElement(getIconForFeatureCode(feature.code), { + width: 16, + height: 16, + color: "burgundy", + })} + </span> + ))} + </div> + {/*NOTE: images from the test API are hosted on test3.scandichotels.com, + which can't be accessed unless on Scandic's Wifi or using Citrix. */} + <ImageGallery + images={images} + title={roomConfiguration.roomType} + fill + /> + </div> + + <div className={styles.specification}> + {occupancy && ( + <Caption color="uiTextMediumContrast" className={styles.guests}> + {intl.formatMessage( + { + id: "booking.guests", + }, + { nrOfGuests: occupancy?.total } + )} + </Caption> + )} + {roomSize && ( + <Caption color="uiTextMediumContrast"> + {roomSize.min === roomSize.max + ? roomSize.min + : `${roomSize.min}-${roomSize.max}`} + m² + </Caption> + )} <div className={styles.toggleSidePeek}> {roomConfiguration.roomTypeCode && ( <ToggleSidePeek diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 8e32bdb1d..b05bbdb8b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -7,6 +7,7 @@ border: 1px solid var(--Base-Border-Subtle); position: relative; height: 100%; + min-height: 730px; justify-content: space-between; } From f1bec6619fd439b3f7ef4c5f72b8289aa2f36b22 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 15:11:55 +0100 Subject: [PATCH 075/101] fix: remove log --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 6c477d39f..2a374b17c 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -112,8 +112,6 @@ export default function RoomCard({ : "default", }) - console.log(occupancy) - return ( <div className={classNames}> <div> From 14b1610d4c6108480b148879d32a86d69f7af01a Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 15:17:39 +0100 Subject: [PATCH 076/101] fix: removed unused code --- .../SelectRate/RoomSelection/RoomCard/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 2a374b17c..d8a37179b 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -60,7 +60,6 @@ export default function RoomCard({ const getBreakfastMessage = (rate: RateDefinition | undefined) => { const breakfastIncluded = getRateDefinitionForRate(rate)?.breakfastIncluded - switch (breakfastIncluded) { case true: return intl.formatMessage({ id: "Breakfast is included." }) @@ -83,7 +82,6 @@ export default function RoomCard({ ) const { roomSize, occupancy, images } = selectedRoom || {} - const mainImage = images?.[0] const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) From ea3aff5dcdc7254cc12a749cca8c5f27a779cc4f Mon Sep 17 00:00:00 2001 From: Bianca Widstam <bianca.widstam@scandichotels.com> Date: Tue, 19 Nov 2024 14:34:45 +0000 Subject: [PATCH 077/101] Merged in fix/translation-hotelpage-header (pull request #934) Fix/SW-932-translation hotelpage header * fix: add translation for hotel page header * fix: add hotel translation * fix: add translation where to when loading * fix: update hotel(s) count if filtered * fix(SW-932): update hotel(s) count Approved-by: Pontus Dreij Approved-by: Niclas Edenvin --- .../(standard)/select-hotel/page.tsx | 4 ++-- .../FormContent/Search/index.tsx | 3 ++- .../SelectHotel/HotelCount/index.tsx | 22 +++++++++++++++++++ i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + 9 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 components/HotelReservation/SelectHotel/HotelCount/index.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index a12639acf..70696a689 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -8,6 +8,7 @@ import { getFiltersFromHotels, } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" +import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter" import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer" @@ -20,7 +21,6 @@ import StaticMap from "@/components/Maps/StaticMap" import Alert from "@/components/TempDesignSystem/Alert" import Button from "@/components/TempDesignSystem/Button" import Link from "@/components/TempDesignSystem/Link" -import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" @@ -72,7 +72,7 @@ export default async function SelectHotelPage({ <div className={styles.title}> <div className={styles.cityInformation}> <Subtitle>{city.name}</Subtitle> - <Preamble>{hotels.length} hotels</Preamble> + <HotelCount /> </div> <div className={styles.sorter}> <HotelSorter discreet /> diff --git a/components/Forms/BookingWidget/FormContent/Search/index.tsx b/components/Forms/BookingWidget/FormContent/Search/index.tsx index abbb28112..0d1ee36d4 100644 --- a/components/Forms/BookingWidget/FormContent/Search/index.tsx +++ b/components/Forms/BookingWidget/FormContent/Search/index.tsx @@ -206,11 +206,12 @@ export default function Search({ locations }: SearchProps) { } export function SearchSkeleton() { + const intl = useIntl() return ( <div className={styles.container}> <div className={styles.label}> <Caption type="bold" color="red" asChild> - <span>Where to</span> + <span>{intl.formatMessage({ id: "Where to" })}</span> </Caption> </div> <div className={styles.input}> diff --git a/components/HotelReservation/SelectHotel/HotelCount/index.tsx b/components/HotelReservation/SelectHotel/HotelCount/index.tsx new file mode 100644 index 000000000..1556dcf30 --- /dev/null +++ b/components/HotelReservation/SelectHotel/HotelCount/index.tsx @@ -0,0 +1,22 @@ +"use client" +import { useIntl } from "react-intl" + +import { useHotelFilterStore } from "@/stores/hotel-filters" + +import Preamble from "@/components/TempDesignSystem/Text/Preamble" + +export default function HotelCount() { + const intl = useIntl() + const resultCount = useHotelFilterStore((state) => state.resultCount) + + return ( + <Preamble> + {intl.formatMessage( + { + id: "Hotel(s)", + }, + { amount: resultCount } + )} + </Preamble> + ) +} diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 7ec920d05..0ad5dce97 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -154,6 +154,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel faciliteter", "Hotel surroundings": "Hotel omgivelser", + "Hotel(s)": "{amount} {amount, plural, one {hotel} other {hoteller}}", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det virker", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 80b3b6116..bd094f4d5 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -154,6 +154,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel-Infos", "Hotel surroundings": "Umgebung des Hotels", + "Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}", "Hotels": "Hotels", "How do you want to sleep?": "Wie möchtest du schlafen?", "How it works": "Wie es funktioniert", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 6080fc36b..afea03c06 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -166,6 +166,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotel facilities", "Hotel surroundings": "Hotel surroundings", + "Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}", "Hotels": "Hotels", "How do you want to sleep?": "How do you want to sleep?", "How it works": "How it works", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 0c1a08d35..57f137e94 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -154,6 +154,7 @@ "Hotel": "Hotelli", "Hotel facilities": "Hotellin palvelut", "Hotel surroundings": "Hotellin ympäristö", + "Hotel(s)": "{amount} {amount, plural, one {hotelli} other {hotellit}}", "Hotels": "Hotellit", "How do you want to sleep?": "Kuinka haluat nukkua?", "How it works": "Kuinka se toimii", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 2aca61a85..1ee68eb82 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -153,6 +153,7 @@ "Hotel": "Hotel", "Hotel facilities": "Hotelfaciliteter", "Hotel surroundings": "Hotellomgivelser", + "Hotel(s)": "{amount} {amount, plural, one {hotell} other {hoteller}}", "Hotels": "Hoteller", "How do you want to sleep?": "Hvordan vil du sove?", "How it works": "Hvordan det fungerer", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index db300b7c2..3177903d2 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -153,6 +153,7 @@ "Hotel": "Hotell", "Hotel facilities": "Hotellfaciliteter", "Hotel surroundings": "Hotellomgivning", + "Hotel(s)": "{amount} hotell", "Hotels": "Hotell", "How do you want to sleep?": "Hur vill du sova?", "How it works": "Hur det fungerar", From ab9c8012c91670a2fa78ed6d2c82d19bdc06cdf2 Mon Sep 17 00:00:00 2001 From: Pontus Dreij <pontus.dreij@scandichotels.com> Date: Tue, 19 Nov 2024 15:36:20 +0100 Subject: [PATCH 078/101] fix(SW-934): Change logic to isAllUnavailable instad of hotels.length --- .../hotelreservation/(standard)/select-hotel/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index a12639acf..c76e6b7b6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -66,6 +66,8 @@ export default async function SelectHotelPage({ const filterList = getFiltersFromHotels(hotels) + const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined) + return ( <> <header className={styles.header}> @@ -123,7 +125,7 @@ export default async function SelectHotelPage({ <HotelFilter filters={filterList} className={styles.filter} /> </div> <div className={styles.hotelList}> - {!hotels.length && ( + {isAllUnavailable && ( <Alert type={AlertTypeEnum.Info} heading={intl.formatMessage({ id: "No availability" })} From c309619cdcee856cc8461000e045c1a9c8e7e813 Mon Sep 17 00:00:00 2001 From: Niclas Edenvin <niclas.edenvin@scandichotels.com> Date: Tue, 19 Nov 2024 14:39:25 +0000 Subject: [PATCH 079/101] Merged in feat/SW-863-remove-filters (pull request #935) feat(SW-863): remove filters without a type * feat(SW-863): remove filters without a type Approved-by: Pontus Dreij --- .../(standard)/select-hotel/utils.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 58b18a25d..bd41510ce 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -19,6 +19,15 @@ const hotelSurroundingsFilterNames = [ "Omgivningar", ] +const hotelFacilitiesFilterNames = [ + "Hotel facilities", + "Hotellfaciliteter", + "Hotelfaciliteter", + "Hotel faciliteter", + "Hotel-Infos", + "Hotellin palvelut", +] + export async function fetchAvailableHotels( input: AvailabilityInput ): Promise<HotelData[]> { @@ -52,6 +61,7 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { const filterList: Filter[] = uniqueFilterIds .map((filterId) => filters.find((filter) => filter.id === filterId)) .filter((filter): filter is Filter => filter !== undefined) + .sort((a, b) => b.sortOrder - a.sortOrder) return filterList.reduce<CategorizedFilters>( (acc, filter) => { @@ -61,10 +71,13 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { surroundingsFilters: [...acc.surroundingsFilters, filter], } - return { - facilityFilters: [...acc.facilityFilters, filter], - surroundingsFilters: acc.surroundingsFilters, - } + if (filter.filter && hotelFacilitiesFilterNames.includes(filter.filter)) + return { + facilityFilters: [...acc.facilityFilters, filter], + surroundingsFilters: acc.surroundingsFilters, + } + + return acc }, { facilityFilters: [], surroundingsFilters: [] } ) From 6958db3ca576b4cc3360c4fdc8c938643cac2ee1 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Fri, 15 Nov 2024 08:30:52 +0100 Subject: [PATCH 080/101] refactor(SW-898): replace signup server action with TRPC --- actions/registerUser.ts | 89 ------------------------------- components/Forms/Signup/index.tsx | 37 ++++++------- server/routers/user/input.ts | 8 +++ server/routers/user/mutation.ts | 72 ++++++++++++++++++++++++- server/routers/user/output.ts | 19 +++++++ server/trpc.ts | 2 +- 6 files changed, 116 insertions(+), 111 deletions(-) delete mode 100644 actions/registerUser.ts diff --git a/actions/registerUser.ts b/actions/registerUser.ts deleted file mode 100644 index 8def69ec3..000000000 --- a/actions/registerUser.ts +++ /dev/null @@ -1,89 +0,0 @@ -"use server" - -import { parsePhoneNumber } from "libphonenumber-js" -import { redirect } from "next/navigation" -import { z } from "zod" - -import { signupVerify } from "@/constants/routes/signup" -import * as api from "@/lib/api" -import { serviceServerActionProcedure } from "@/server/trpc" - -import { signUpSchema } from "@/components/Forms/Signup/schema" -import { passwordValidator } from "@/utils/passwordValidator" -import { phoneValidator } from "@/utils/phoneValidator" - -const registerUserPayload = z.object({ - language: z.string(), - firstName: z.string(), - lastName: z.string(), - email: z.string(), - phoneNumber: phoneValidator("Phone is required"), - dateOfBirth: z.string(), - address: z.object({ - city: z.string().default(""), - country: z.string().default(""), - countryCode: z.string().default(""), - zipCode: z.string().default(""), - streetAddress: z.string().default(""), - }), - password: passwordValidator("Password is required"), -}) - -export const registerUser = serviceServerActionProcedure - .input(signUpSchema) - .mutation(async function ({ ctx, input }) { - const payload = { - ...input, - language: ctx.lang, - phoneNumber: input.phoneNumber.replace(/\s+/g, ""), - } - - const parsedPayload = registerUserPayload.safeParse(payload) - if (!parsedPayload.success) { - console.error( - "registerUser payload validation error", - JSON.stringify({ - query: input, - error: parsedPayload.error, - }) - ) - - return { success: false, error: "Validation error" } - } - - let apiResponse - try { - apiResponse = await api.post(api.endpoints.v1.Profile.profile, { - body: parsedPayload.data, - 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( - "registerUser 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("registerUser: json", json) - - // Note: The redirect needs to be called after the try/catch block. - // See: https://nextjs.org/docs/app/api-reference/functions/redirect - redirect(signupVerify[ctx.lang]) - }) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index d6d5fc7da..d1e2727e0 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -1,12 +1,13 @@ "use client" import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { privacyPolicy } from "@/constants/currentWebHrefs" +import { trpc } from "@/lib/trpc/client" -import { registerUser } from "@/actions/registerUser" import Button from "@/components/TempDesignSystem/Button" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import CountrySelect from "@/components/TempDesignSystem/Form/Country" @@ -30,12 +31,25 @@ import type { SignUpFormProps } from "@/types/components/form/signupForm" export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { const intl = useIntl() + const router = useRouter() const lang = useLang() const country = intl.formatMessage({ id: "Country" }) const email = intl.formatMessage({ id: "Email address" }) const phoneNumber = intl.formatMessage({ id: "Phone number" }) const zipCode = intl.formatMessage({ id: "Zip code" }) + const signup = trpc.user.signup.useMutation({ + onSuccess: (data) => { + if (data.success && data.redirectUrl) { + router.push(data.redirectUrl) + } + }, + onError: (error) => { + toast.error(intl.formatMessage({ id: "Something went wrong!" })) + console.error("Component Signup error:", error) + }, + }) + const methods = useForm<SignUpSchema>({ defaultValues: { firstName: "", @@ -56,19 +70,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { }) async function onSubmit(data: SignUpSchema) { - try { - const result = await registerUser(data) - if (result && !result.success) { - toast.error(intl.formatMessage({ id: "Something went wrong!" })) - } - } catch (error) { - // The server-side redirect will throw an error, which we can ignore - // as it's handled by Next.js. - if (error instanceof Error && error.message.includes("NEXT_REDIRECT")) { - return - } - toast.error(intl.formatMessage({ id: "Something went wrong!" })) - } + signup.mutate({ ...data, language: lang }) } return ( @@ -79,11 +81,6 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { className={styles.form} id="register" onSubmit={methods.handleSubmit(onSubmit)} - /** - * Ignoring since ts doesn't recognize that tRPC - * parses FormData before reaching the route - * @ts-ignore */ - action={registerUser} > <section className={styles.userInfo}> <div className={styles.container}> @@ -194,7 +191,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { type="submit" theme="base" intent="primary" - disabled={methods.formState.isSubmitting} + disabled={methods.formState.isSubmitting || signup.isPending} data-testid="submit" > {intl.formatMessage({ id: "Sign up to Scandic Friends" })} diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index d84875ea3..1d279c2ef 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -1,5 +1,9 @@ import { z } from "zod" +import { Lang } from "@/constants/languages" + +import { signUpSchema } from "@/components/Forms/Signup/schema" + // Query export const staysInput = z .object({ @@ -35,3 +39,7 @@ export const saveCreditCardInput = z.object({ transactionId: z.string(), merchantId: z.string().optional(), }) + +export const signupInput = signUpSchema.extend({ + language: z.nativeEnum(Lang), +}) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index b03e6a68e..e233814ee 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -1,17 +1,21 @@ import { metrics } from "@opentelemetry/api" +import { signupVerify } from "@/constants/routes/signup" import { env } from "@/env/server" import * as api from "@/lib/api" +import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { initiateSaveCardSchema, + signupPayloadSchema, subscriberIdSchema, } from "@/server/routers/user/output" -import { protectedProcedure, router } from "@/server/trpc" +import { protectedProcedure, router,serviceProcedure } from "@/server/trpc" import { addCreditCardInput, deleteCreditCardInput, saveCreditCardInput, + signupInput, } from "./input" const meter = metrics.getMeter("trpc.user") @@ -24,6 +28,9 @@ const generatePreferencesLinkSuccessCounter = meter.createCounter( const generatePreferencesLinkFailCounter = meter.createCounter( "trpc.user.generatePreferencesLink-fail" ) +const signupCounter = meter.createCounter("trpc.user.signup") +const signupSuccessCounter = meter.createCounter("trpc.user.signup-success") +const signupFailCounter = meter.createCounter("trpc.user.signup-fail") export const userMutationRouter = router({ creditCard: router({ @@ -208,4 +215,67 @@ export const userMutationRouter = router({ generatePreferencesLinkSuccessCounter.add(1) return preferencesLink.toString() }), + signup: serviceProcedure.input(signupInput).mutation(async function ({ + ctx, + input, + }) { + const payload = { + ...input, + language: input.language, + phoneNumber: input.phoneNumber.replace(/\s+/g, ""), + } + signupCounter.add(1) + + const parsedPayload = signupPayloadSchema.safeParse(payload) + if (!parsedPayload.success) { + signupFailCounter.add(1, { + error_type: "validation_error", + error: JSON.stringify(parsedPayload.error), + }) + console.error( + "api.user.signup validation error", + JSON.stringify({ + query: input, + error: parsedPayload.error, + }) + ) + throw badRequestError(parsedPayload.error) + } + + const apiResponse = await api.post(api.endpoints.v1.Profile.profile, { + body: parsedPayload.data, + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + signupFailCounter.add(1, { + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }), + }) + console.error( + "api.user.signup api error", + JSON.stringify({ + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, + }) + ) + throw serverErrorByStatus(apiResponse.status, text) + } + signupSuccessCounter.add(1) + console.info("api.user.signup success", JSON.stringify({})) + return { + success: true, + redirectUrl: signupVerify[input.language], + } + }), }) diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index a9e7a0416..4dadd467f 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -1,6 +1,8 @@ import { z } from "zod" import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries" +import { passwordValidator } from "@/utils/passwordValidator" +import { phoneValidator } from "@/utils/phoneValidator" import { getMembership } from "@/utils/user" export const membershipSchema = z.object({ @@ -244,3 +246,20 @@ export const initiateSaveCardSchema = z.object({ export const subscriberIdSchema = z.object({ subscriberId: z.string(), }) + +export const signupPayloadSchema = z.object({ + language: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string(), + phoneNumber: phoneValidator("Phone is required"), + dateOfBirth: z.string(), + address: z.object({ + city: z.string().default(""), + country: z.string().default(""), + countryCode: z.string().default(""), + zipCode: z.string().default(""), + streetAddress: z.string().default(""), + }), + password: passwordValidator("Password is required"), +}) diff --git a/server/trpc.ts b/server/trpc.ts index 688ea01cf..3fc3a10a6 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -121,7 +121,7 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) { }) }) -export const serviceProcedure = t.procedure.use(async (opts) => { +export const serviceProcedure = t.procedure.use(async function (opts) { const { access_token } = await getServiceToken() if (!access_token) { throw internalServerError(`[serviceProcedure] No service token`) From bc5a01fdf4010f45bac5660224e080b6dca84378 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 18 Nov 2024 10:13:05 +0100 Subject: [PATCH 081/101] fix(SW-898): formatting --- server/routers/user/mutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index e233814ee..fd8fe0d41 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -9,7 +9,7 @@ import { signupPayloadSchema, subscriberIdSchema, } from "@/server/routers/user/output" -import { protectedProcedure, router,serviceProcedure } from "@/server/trpc" +import { protectedProcedure, router, serviceProcedure } from "@/server/trpc" import { addCreditCardInput, From cfaa92260a3d31ccf7dd6f4e2324edf69098eff5 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 18 Nov 2024 10:35:27 +0100 Subject: [PATCH 082/101] chore(SW-898): pr comments --- server/routers/user/mutation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index fd8fe0d41..17fc39adf 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -272,7 +272,7 @@ export const userMutationRouter = router({ throw serverErrorByStatus(apiResponse.status, text) } signupSuccessCounter.add(1) - console.info("api.user.signup success", JSON.stringify({})) + console.info("api.user.signup success") return { success: true, redirectUrl: signupVerify[input.language], From 5c571c3c0c30b248d48b8eb9c45ffa2021cf1fa6 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 18 Nov 2024 11:18:55 +0100 Subject: [PATCH 083/101] feat(SW-898): add pending ui text to signup button --- components/Forms/Signup/index.tsx | 10 ++++++++-- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/components/Forms/Signup/index.tsx b/components/Forms/Signup/index.tsx index d1e2727e0..24fe2bd95 100644 --- a/components/Forms/Signup/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -37,6 +37,10 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { const email = intl.formatMessage({ id: "Email address" }) const phoneNumber = intl.formatMessage({ id: "Phone number" }) const zipCode = intl.formatMessage({ id: "Zip code" }) + const signupButtonText = intl.formatMessage({ + id: "Sign up to Scandic Friends", + }) + const signingUpPendingText = intl.formatMessage({ id: "Signing up..." }) const signup = trpc.user.signup.useMutation({ onSuccess: (data) => { @@ -183,7 +187,7 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { onClick={() => methods.trigger()} data-testid="trigger-validation" > - {intl.formatMessage({ id: "Sign up to Scandic Friends" })} + {signupButtonText} </Button> ) : ( <Button @@ -194,7 +198,9 @@ export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { disabled={methods.formState.isSubmitting || signup.isPending} data-testid="submit" > - {intl.formatMessage({ id: "Sign up to Scandic Friends" })} + {methods.formState.isSubmitting || signup.isPending + ? signingUpPendingText + : signupButtonText} </Button> )} </form> diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 0ad5dce97..a9fdcfae4 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -337,6 +337,7 @@ "Show wellness & exercise": "Vis velvære og motion", "Sign up bonus": "Velkomstbonus", "Sign up to Scandic Friends": "Tilmeld dig Scandic Friends", + "Signing up...": "Tilmelder...", "Skip to main content": "Spring over og gå til hovedindhold", "Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index bd094f4d5..96bcef2de 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -336,6 +336,7 @@ "Show wellness & exercise": "Zeige Wellness und Bewegung", "Sign up bonus": "Anmelde-Bonus", "Sign up to Scandic Friends": "Treten Sie Scandic Friends bei", + "Signing up...": "Registrierung läuft...", "Skip to main content": "Direkt zum Inhalt", "Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.", "Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index afea03c06..385861316 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -366,6 +366,7 @@ "Show wellness & exercise": "Show wellness & exercise", "Sign up bonus": "Sign up bonus", "Sign up to Scandic Friends": "Sign up to Scandic Friends", + "Signing up...": "Signing up...", "Skip to main content": "Skip to main content", "Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.", "Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 57f137e94..09fb8a403 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -338,6 +338,7 @@ "Show wellness & exercise": "Näytä hyvinvointi ja liikunta", "Sign up bonus": "Liittymisbonus", "Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi", + "Signing up...": "Rekisteröidytään...", "Skip to main content": "Siirry pääsisältöön", "Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.", "Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 1ee68eb82..dcbcdddf4 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -335,6 +335,7 @@ "Show wellness & exercise": "Vis velvære og trening", "Sign up bonus": "Velkomstbonus", "Sign up to Scandic Friends": "Bli med i Scandic Friends", + "Signing up...": "Registrerer...", "Skip to main content": "Gå videre til hovedsiden", "Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.", "Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 3177903d2..404896e15 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -335,6 +335,7 @@ "Show wellness & exercise": "Visa välbefinnande och träning", "Sign up bonus": "Välkomstbonus", "Sign up to Scandic Friends": "Bli medlem i Scandic Friends", + "Signing up...": "Registrerar...", "Skip to main content": "Fortsätt till huvudinnehåll", "Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.", "Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.", From a68e37c26fc65c71c06ae6eda6032f297abbe16c Mon Sep 17 00:00:00 2001 From: Chuma McPhoy <chuma.mcphoy@scandichotels.com> Date: Mon, 18 Nov 2024 15:47:13 +0100 Subject: [PATCH 084/101] fix(SW-898): remove redundant schema and add transform in signupInput --- server/routers/user/input.ts | 18 +++++++++++++++--- server/routers/user/mutation.ts | 26 ++------------------------ server/routers/user/output.ts | 17 ----------------- 3 files changed, 17 insertions(+), 44 deletions(-) diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts index 1d279c2ef..1e9fee267 100644 --- a/server/routers/user/input.ts +++ b/server/routers/user/input.ts @@ -40,6 +40,18 @@ export const saveCreditCardInput = z.object({ merchantId: z.string().optional(), }) -export const signupInput = signUpSchema.extend({ - language: z.nativeEnum(Lang), -}) +export const signupInput = signUpSchema + .extend({ + language: z.nativeEnum(Lang), + }) + .omit({ termsAccepted: true }) + .transform((data) => ({ + ...data, + phoneNumber: data.phoneNumber.replace(/\s+/g, ""), + address: { + ...data.address, + city: "", + country: "", + streetAddress: "", + }, + })) diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts index 17fc39adf..689c43a26 100644 --- a/server/routers/user/mutation.ts +++ b/server/routers/user/mutation.ts @@ -3,10 +3,9 @@ import { metrics } from "@opentelemetry/api" import { signupVerify } from "@/constants/routes/signup" import { env } from "@/env/server" import * as api from "@/lib/api" -import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" +import { serverErrorByStatus } from "@/server/errors/trpc" import { initiateSaveCardSchema, - signupPayloadSchema, subscriberIdSchema, } from "@/server/routers/user/output" import { protectedProcedure, router, serviceProcedure } from "@/server/trpc" @@ -219,31 +218,10 @@ export const userMutationRouter = router({ ctx, input, }) { - const payload = { - ...input, - language: input.language, - phoneNumber: input.phoneNumber.replace(/\s+/g, ""), - } signupCounter.add(1) - const parsedPayload = signupPayloadSchema.safeParse(payload) - if (!parsedPayload.success) { - signupFailCounter.add(1, { - error_type: "validation_error", - error: JSON.stringify(parsedPayload.error), - }) - console.error( - "api.user.signup validation error", - JSON.stringify({ - query: input, - error: parsedPayload.error, - }) - ) - throw badRequestError(parsedPayload.error) - } - const apiResponse = await api.post(api.endpoints.v1.Profile.profile, { - body: parsedPayload.data, + body: input, headers: { Authorization: `Bearer ${ctx.serviceToken}`, }, diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts index 4dadd467f..616419047 100644 --- a/server/routers/user/output.ts +++ b/server/routers/user/output.ts @@ -246,20 +246,3 @@ export const initiateSaveCardSchema = z.object({ export const subscriberIdSchema = z.object({ subscriberId: z.string(), }) - -export const signupPayloadSchema = z.object({ - language: z.string(), - firstName: z.string(), - lastName: z.string(), - email: z.string(), - phoneNumber: phoneValidator("Phone is required"), - dateOfBirth: z.string(), - address: z.object({ - city: z.string().default(""), - country: z.string().default(""), - countryCode: z.string().default(""), - zipCode: z.string().default(""), - streetAddress: z.string().default(""), - }), - password: passwordValidator("Password is required"), -}) From bc344e64cf713041eb64e8234996e3143a9502ab Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar <hrishikesh.vaipurkar@scandichotels.com> Date: Tue, 19 Nov 2024 14:54:11 +0100 Subject: [PATCH 085/101] feat: SW-601 Implement filters and sort in map view --- .../select-hotel/@modal/(.)map/page.tsx | 4 +- .../filterAndSortModal.module.css | 70 +++++++++++++++++++ .../SelectHotel/FilterAndSortModal/index.tsx | 12 +++- .../HotelFilter/hotelFilter.module.css | 14 ++++ .../SelectHotel/HotelFilter/index.tsx | 10 ++- .../SelectHotel/SelectHotelMap/index.tsx | 5 +- .../SelectHotelMap/selectHotelMap.module.css | 5 ++ .../selectHotel/hotelFilters.ts | 5 ++ .../hotelReservation/selectHotel/map.ts | 3 +- 9 files changed, 120 insertions(+), 8 deletions(-) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx index fd9cc33c5..7c5573536 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -12,7 +12,7 @@ import { import { MapModal } from "@/components/MapModal" import { setLang } from "@/i18n/serverContext" -import { fetchAvailableHotels } from "../../utils" +import { fetchAvailableHotels, getFiltersFromHotels } from "../../utils" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" @@ -57,6 +57,7 @@ export default async function SelectHotelMapPage({ }) const hotelPins = getHotelPins(hotels) + const filterList = getFiltersFromHotels(hotels) return ( <MapModal> @@ -65,6 +66,7 @@ export default async function SelectHotelMapPage({ hotelPins={hotelPins} mapId={googleMapId} hotels={hotels} + filterList={filterList} /> </MapModal> ) diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css index 6768d2c56..299527ad0 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css @@ -81,6 +81,10 @@ flex: 0 0 auto; } +.title { + display: none; +} + .close { background: none; border: none; @@ -97,3 +101,69 @@ flex: 0 0 auto; border-top: 1px solid var(--Base-Border-Subtle); } + +@media screen and (min-width: 768px) { + .modal { + left: 50%; + bottom: 50%; + height: min(80dvh, 680px); + width: min(80dvw, 960px); + translate: -50% 50%; + overflow-y: auto; + } + + .header { + display: grid; + grid-template-columns: auto 1fr; + padding: var(--Spacing-x2) var(--Spacing-x3); + align-items: center; + border-bottom: 1px solid var(--Base-Border-Subtle); + position: sticky; + top: 0; + background: var(--Base-Surface-Primary-light-Normal); + z-index: 1; + border-top-left-radius: var(--Corner-radius-large); + border-top-right-radius: var(--Corner-radius-large); + } + + .title { + display: block; + } + + .content { + gap: var(--Spacing-x4); + height: auto; + } + + .filters { + overflow-y: unset; + } + + .sorter, + .filters, + .footer, + .divider { + padding: 0 var(--Spacing-x3); + } + + .footer { + flex-direction: row-reverse; + justify-content: space-between; + position: sticky; + bottom: 0; + background: var(--Base-Surface-Primary-light-Normal); + z-index: 1; + border-bottom-left-radius: var(--Corner-radius-large); + border-bottom-right-radius: var(--Corner-radius-large); + padding: var(--Spacing-x2) var(--Spacing-x3); + } + + .filters aside h1 { + margin-bottom: var(--Spacing-x2); + } + + .filters aside > div:last-child { + margin-top: var(--Spacing-x4); + padding-bottom: 0; + } +} diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx index be1a1bc9c..40dd606fb 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -12,6 +12,8 @@ import { useHotelFilterStore } from "@/stores/hotel-filters" import { CloseLargeIcon, FilterIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import HotelFilter from "../HotelFilter" import HotelSorter from "../HotelSorter" @@ -47,12 +49,20 @@ export default function FilterAndSortModal({ > <CloseLargeIcon /> </button> + <Subtitle + type="two" + textAlign="center" + className={styles.title} + > + {intl.formatMessage({ id: "Filter and sort" })} + </Subtitle> </header> <div className={styles.sorter}> <HotelSorter /> </div> + <Divider color="subtle" className="divider" /> <div className={styles.filters}> - <HotelFilter filters={filters} /> + <HotelFilter filters={filters} type="modal" /> </div> <footer className={styles.footer}> <Button diff --git a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css index 8a4fcebff..0e204c144 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css +++ b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css @@ -38,3 +38,17 @@ height: 1.25rem; margin: 0; } + +@media screen and (min-width: 768px) { + .facilities ul.modal { + display: grid; + grid-template-columns: auto auto auto; + margin-top: var(--Spacing-x3); + } +} + +@media screen and (min-width: 768px) and (max-width: 1023) { + .facilities ul.modal { + grid-template-columns: auto auto; + } +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index c428894a3..a716ca3a4 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -15,7 +15,11 @@ import styles from "./hotelFilter.module.css" import type { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -export default function HotelFilter({ className, filters }: HotelFiltersProps) { +export default function HotelFilter({ + className, + filters, + type, +}: HotelFiltersProps) { const intl = useIntl() const searchParams = useSearchParams() const pathname = usePathname() @@ -63,7 +67,7 @@ export default function HotelFilter({ className, filters }: HotelFiltersProps) { <Title as="h4">{intl.formatMessage({ id: "Filter by" })}
    {intl.formatMessage({ id: "Hotel facilities" })} -
      +
        {filters.facilityFilters.map((filter) => (
      • {intl.formatMessage({ id: "Hotel surroundings" })} -
          +
            {filters.surroundingsFilters.map((filter) => (
          • - Filter and sort - {/* TODO: Add filter and sort button */} +
    From ebd60d9789b6924c0d51d49c14432e5e3b0b7e98 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Tue, 19 Nov 2024 17:18:12 +0100 Subject: [PATCH 086/101] feat: SW-601 Removed autoclose of map view in desktop --- components/MapModal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/MapModal/index.tsx b/components/MapModal/index.tsx index 2c06a08cc..3f9e3b4b6 100644 --- a/components/MapModal/index.tsx +++ b/components/MapModal/index.tsx @@ -66,7 +66,7 @@ export function MapModal({ children }: { children: React.ReactNode }) { return (
    - + Date: Tue, 19 Nov 2024 17:19:04 +0100 Subject: [PATCH 087/101] feat: SW-601 Optimized css --- .../filterAndSortModal.module.css | 11 +++++++++++ .../SelectHotel/HotelFilter/hotelFilter.module.css | 14 -------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css index 299527ad0..eab6a1c1d 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/filterAndSortModal.module.css @@ -166,4 +166,15 @@ margin-top: var(--Spacing-x4); padding-bottom: 0; } + + .filters aside ul { + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: var(--Spacing-x3); + } +} +@media screen and (min-width: 1024) { + .facilities ul { + grid-template-columns: 1fr 1fr 1fr; + } } diff --git a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css index 0e204c144..8a4fcebff 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css +++ b/components/HotelReservation/SelectHotel/HotelFilter/hotelFilter.module.css @@ -38,17 +38,3 @@ height: 1.25rem; margin: 0; } - -@media screen and (min-width: 768px) { - .facilities ul.modal { - display: grid; - grid-template-columns: auto auto auto; - margin-top: var(--Spacing-x3); - } -} - -@media screen and (min-width: 768px) and (max-width: 1023) { - .facilities ul.modal { - grid-template-columns: auto auto; - } -} From 1330c8b537adb4424b54169d32bfebcaed5dbfe2 Mon Sep 17 00:00:00 2001 From: Hrishikesh Vaipurkar Date: Tue, 19 Nov 2024 17:31:34 +0100 Subject: [PATCH 088/101] feat: SW-601 Optimized code --- .../SelectHotel/FilterAndSortModal/index.tsx | 2 +- .../HotelReservation/SelectHotel/HotelFilter/index.tsx | 10 +++------- .../hotelReservation/selectHotel/hotelFilters.ts | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx index 40dd606fb..286042ca0 100644 --- a/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx +++ b/components/HotelReservation/SelectHotel/FilterAndSortModal/index.tsx @@ -62,7 +62,7 @@ export default function FilterAndSortModal({
    - +