diff --git a/actions/registerUser.ts b/actions/registerUser.ts index 11afb22f4..e65fc357f 100644 --- a/actions/registerUser.ts +++ b/actions/registerUser.ts @@ -1,5 +1,6 @@ "use server" +import { parsePhoneNumber } from "libphonenumber-js" import { redirect } from "next/navigation" import { z } from "zod" @@ -7,7 +8,7 @@ import { signupVerify } from "@/constants/routes/signup" import * as api from "@/lib/api" import { serviceServerActionProcedure } from "@/server/trpc" -import { registerSchema } from "@/components/Forms/Register/schema" +import { signUpSchema } from "@/components/Forms/Signup/schema" import { passwordValidator } from "@/utils/passwordValidator" import { phoneValidator } from "@/utils/phoneValidator" @@ -29,12 +30,14 @@ const registerUserPayload = z.object({ }) export const registerUser = serviceServerActionProcedure - .input(registerSchema) + .input(signUpSchema) .mutation(async function ({ ctx, input }) { const payload = { ...input, language: ctx.lang, - phoneNumber: input.phoneNumber.replace(/\s+/g, ""), + phoneNumber: parsePhoneNumber(input.phoneNumber) + .formatNational() + .replace(/\s+/g, ""), } const parsedPayload = registerUserPayload.safeParse(payload) diff --git a/app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/booking-confirmation/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css new file mode 100644 index 000000000..06b39ef41 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css @@ -0,0 +1,157 @@ +.details, +.guest, +.header, +.hgroup, +.hotel, +.list, +.main, +.section, +.receipt, +.total { + display: flex; + flex-direction: column; +} + +.main { + gap: var(--Spacing-x5); + margin: 0 auto; + width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px); +} + +.header, +.hgroup { + align-items: center; +} + +.header { + gap: var(--Spacing-x3); +} + +.hgroup { + gap: var(--Spacing-x-half); +} + +.body { + max-width: 560px; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x9); +} + +.booking { + display: grid; + gap: var(--Spacing-x-one-and-half); + grid-template-areas: + "image" + "details" + "actions"; +} + +.actions, +.details { + background-color: var(--Base-Surface-Subtle-Normal); + border-radius: var(--Corner-radius-Medium); +} + +.details { + gap: var(--Spacing-x3); + grid-area: details; + padding: var(--Spacing-x2); +} + +.tempImage { + align-items: center; + background-color: lightgrey; + border-radius: var(--Corner-radius-Medium); + display: flex; + grid-area: image; + justify-content: center; +} + +.actions { + display: grid; + grid-area: actions; + padding: var(--Spacing-x1) var(--Spacing-x2); +} + +.list { + gap: var(--Spacing-x-one-and-half); + list-style: none; + margin: 0; + padding: 0; +} + +.listItem { + align-items: center; + display: flex; + gap: var(--Spacing-x1); + justify-content: space-between; +} + +.summary { + display: grid; + gap: var(--Spacing-x3); +} + +.guest, +.hotel { + gap: var(--Spacing-x-half); +} + +.receipt, +.total { + gap: var(--Spacing-x2); +} + +.divider { + grid-column: 1 / -1; +} + +@media screen and (max-width: 767px) { + .actions { + & > button[class*="btn"][class*="icon"][class*="small"] { + border-bottom: 1px solid var(--Base-Border-Subtle); + border-radius: 0; + justify-content: space-between; + + &:last-of-type { + border-bottom: none; + } + + & > svg { + order: 2; + } + } + } + + .tempImage { + min-height: 250px; + } +} + +@media screen and (min-width: 768px) { + .booking { + grid-template-areas: + "details image" + "actions actions"; + grid-template-columns: 1fr minmax(256px, min(256px, 100%)); + } + + .actions { + gap: var(--Spacing-x7); + grid-template-columns: repeat(auto-fit, minmax(50px, auto)); + justify-content: center; + padding: var(--Spacing-x1) var(--Spacing-x3); + } + + .details { + padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2); + } + + .summary { + grid-template-columns: 1fr 1fr; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx new file mode 100644 index 000000000..ef7e818c4 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -0,0 +1,281 @@ +import { dt } from "@/lib/dt" +import { serverClient } from "@/lib/trpc/server" + +import { + CalendarIcon, + DownloadIcon, + ImageIcon, + PrinterIcon, +} from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import Title from "@/components/TempDesignSystem/Text/Title" +import { getIntl } from "@/i18n" + +import styles from "./page.module.css" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function BookingConfirmationPage({ + params, + searchParams, +}: PageArgs) { + const confirmationNumber = searchParams.confirmationNumber + const booking = await serverClient().booking.confirmation({ + confirmationNumber, + }) + + if (!booking) { + return null + } + + const intl = await getIntl() + const text = intl.formatMessage( + { id: "booking.confirmation.text" }, + { + emailLink: (str) => ( + + {str} + + ), + } + ) + + const fromDate = dt(booking.temp.fromDate).locale(params.lang) + const toDate = dt(booking.temp.toDate).locale(params.lang) + const nights = intl.formatMessage( + { id: "booking.nights" }, + { + totalNights: dt(toDate.format("YYYY-MM-DD")).diff( + dt(fromDate.format("YYYY-MM-DD")), + "days" + ), + } + ) + + return ( +
+
+
+ + {intl.formatMessage({ id: "booking.confirmation.title" })} + + + {booking.hotel.name} + +
+ + {text} + +
+
+
+
+
+ + {intl.formatMessage( + { id: "Reference #{bookingNr}" }, + { bookingNr: "A92320VV" } + )} + +
+
    +
  • + {intl.formatMessage({ id: "Check-in" })} + + {`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`} + +
  • +
  • + {intl.formatMessage({ id: "Check-out" })} + + {`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`} + +
  • +
  • + {intl.formatMessage({ id: "Breakfast" })} + + {booking.temp.breakfastFrom} - {booking.temp.breakfastTo} + +
  • +
  • + {intl.formatMessage({ id: "Cancellation policy" })} + + {intl.formatMessage({ id: booking.temp.cancelPolicy })} + +
  • +
  • + {intl.formatMessage({ id: "Rebooking" })} + {`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`} +
  • +
+
+ +
+ + + +
+
+
+
+ + {intl.formatMessage({ id: "Guest" })} + +
+ + {`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`} + + {booking.guest.email} + + {booking.guest.phoneNumber} + +
+
+
+ + {intl.formatMessage({ id: "Your hotel" })} + +
+ + {booking.hotel.name} + + {booking.hotel.email} + + {booking.hotel.phoneNumber} + +
+
+ +
+
+ + {`${booking.temp.room.type}, ${nights}`} + + {booking.temp.room.price} +
+ {booking.temp.packages.map((pkg) => ( +
+ + {pkg.name} + + {pkg.price} +
+ ))} +
+
+
+ + {intl.formatMessage({ id: "VAT" })} + + {booking.temp.room.vat} +
+
+ + {intl.formatMessage({ id: "Total cost" })} + + {booking.temp.total} + + {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} + +
+
+ +
+ + {`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`} + + + {intl.formatMessage( + { id: "{card} ending with {cardno}" }, + { + card: "Mastercard", + cardno: "2202", + } + )} + +
+
+
+
+ ) +} + +// const { email, hotel, stay, summary } = tempConfirmationData + +// const confirmationNumber = useMemo(() => { +// if (typeof window === "undefined") return "" + +// const storedConfirmationNumber = sessionStorage.getItem( +// BOOKING_CONFIRMATION_NUMBER +// ) +// TODO: cleanup stored values +// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) +// return storedConfirmationNumber +// }, []) + +// const bookingStatus = useHandleBookingStatus( +// confirmationNumber, +// BookingStatusEnum.BookingCompleted, +// maxRetries, +// retryInterval +// ) + +// if ( +// confirmationNumber === null || +// bookingStatus.isError || +// (bookingStatus.isFetched && !bookingStatus.data) +// ) { +// // TODO: handle error +// throw new Error("Error fetching booking status") +// } + +// if ( +// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted +// ) { +// return diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css new file mode 100644 index 000000000..3f89e6f51 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.module.css @@ -0,0 +1,5 @@ +.layout { + background-color: var(--Base-Surface-Primary-light-Normal); + min-height: 100dvh; + padding: 80px 0 160px; +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx new file mode 100644 index 000000000..971c66e0d --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/layout.tsx @@ -0,0 +1,15 @@ +import { notFound } from "next/navigation" + +import { env } from "@/env/server" + +import styles from "./layout.module.css" + +// route groups needed as layouts have different bgc +export default function ConfirmedBookingLayout({ + children, +}: React.PropsWithChildren) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return
{children}
+} diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/layout.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx similarity index 80% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx index 5f62c69c9..0e8edd50e 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/layout.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/layout.tsx @@ -1,6 +1,10 @@ import { redirect } from "next/navigation" -import { serverClient } from "@/lib/trpc/server" +import { + getCreditCardsSafely, + getHotelData, + getProfileSafely, +} from "@/lib/trpc/memoizedRequests" import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" @@ -14,15 +18,20 @@ import styles from "./layout.module.css" import { StepEnum } from "@/types/components/enterDetails/step" import type { LangParams, LayoutArgs } from "@/types/params" +function preload(id: string, lang: string) { + void getHotelData(id, lang) + void getProfileSafely() + void getCreditCardsSafely() +} + export default async function StepLayout({ children, params, }: React.PropsWithChildren>) { setLang(params.lang) - const hotel = await serverClient().hotel.hotelData.get({ - hotelId: "811", - language: params.lang, - }) + preload("811", params.lang) + + const hotel = await getHotelData("811", params.lang) if (!hotel?.data) { redirect(`/${params.lang}`) diff --git a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx similarity index 72% rename from app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 8718c8db7..8e8d3c891 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,13 +1,17 @@ import { notFound } from "next/navigation" -import { getProfileSafely } from "@/lib/trpc/memoizedRequests" -import { serverClient } from "@/lib/trpc/server" +import { + getCreditCardsSafely, + getHotelData, + getProfileSafely, +} from "@/lib/trpc/memoizedRequests" import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" +import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" +import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" -import Payment from "@/components/HotelReservation/SelectRate/Payment" import { getIntl } from "@/i18n" import { StepEnum } from "@/types/components/enterDetails/step" @@ -24,12 +28,9 @@ export default async function StepPage({ const intl = await getIntl() - const hotel = await serverClient().hotel.hotelData.get({ - hotelId: "811", - language: lang, - }) - + const hotel = await getHotelData("811", lang) const user = await getProfileSafely() + const savedCreditCards = await getCreditCardsSafely() if (!isValidStep(step) || !hotel) { return notFound() @@ -37,6 +38,7 @@ export default async function StepPage({ return (
+ - +
) diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/layout.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/layout.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/layout.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/page.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx similarity index 95% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx index 89f4e62ca..8dca8f4c7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/map/page.tsx @@ -3,7 +3,7 @@ import { env } from "@/env/server" import { fetchAvailableHotels, getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils" +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" import { setLang } from "@/i18n/serverContext" diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx similarity index 95% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index c0eaf2126..2f2465756 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,4 +1,4 @@ -import { differenceInCalendarDays, format,isWeekend } from "date-fns" +import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { Lang } from "@/constants/languages" import { selectHotelMap } from "@/constants/routes/hotelReservation" @@ -6,7 +6,7 @@ import { selectHotelMap } from "@/constants/routes/hotelReservation" import { fetchAvailableHotels, getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils" +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import { ChevronRightIcon } from "@/components/Icons" diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils.ts rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/select-rate/page.module.css rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.module.css diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx similarity index 68% rename from app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index fdd5436f3..2d80356ac 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -1,7 +1,9 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" +import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection" +import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -15,6 +17,12 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) + const selectRoomParams = new URLSearchParams(searchParams) + const selectRoomParamsObject = + getHotelReservationQueryParams(selectRoomParams) + const adults = selectRoomParamsObject.room[0].adults // TODO: Handle multiple rooms + const children = selectRoomParamsObject.room[0].child?.length // TODO: Handle multiple rooms + const [hotelData, roomConfigurations, user] = await Promise.all([ serverClient().hotel.hotelData.get({ hotelId: searchParams.hotel, @@ -23,9 +31,10 @@ export default async function SelectRatePage({ }), serverClient().hotel.availability.rooms({ hotelId: parseInt(searchParams.hotel, 10), - roomStayStartDate: "2024-11-02", - roomStayEndDate: "2024-11-03", - adults: 1, + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults, + children, }), getProfileSafely(), ]) @@ -42,9 +51,8 @@ export default async function SelectRatePage({ return (
+
- {/* TODO: Add Hotel Listing Card */} -
Hotel Listing Card TBI
{ - if (typeof window === "undefined") return "" - - const storedConfirmationNumber = sessionStorage.getItem( - BOOKING_CONFIRMATION_NUMBER - ) - // TODO: cleanup stored values - // sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) - return storedConfirmationNumber - }, []) - - const bookingStatus = useHandleBookingStatus( - confirmationNumber, - BookingStatusEnum.BookingCompleted, - maxRetries, - retryInterval - ) - - if ( - confirmationNumber === null || - bookingStatus.isError || - (bookingStatus.isFetched && !bookingStatus.data) - ) { - // TODO: handle error - throw new Error("Error fetching booking status") - } - - if ( - bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted - ) { - return ( -
-
- - - -
-
- ) - } - - return -} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx new file mode 100644 index 000000000..e0ff199ee --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/booking-confirmation/page.tsx @@ -0,0 +1,3 @@ +export default function ConfirmedBookingSlot() { + return null +} diff --git a/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/@bookingwidget/hotelreservation/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/@bookingwidget/loading.tsx b/app/[lang]/(live)/@bookingwidget/loading.tsx index 2c203967d..5e05ba68c 100644 --- a/app/[lang]/(live)/@bookingwidget/loading.tsx +++ b/app/[lang]/(live)/@bookingwidget/loading.tsx @@ -1,8 +1,14 @@ +import { env } from "@/env/server" + import LoadingSpinner from "@/components/LoadingSpinner" import styles from "./loading.module.css" export default function LoadingBookingWidget() { + if (env.HIDE_FOR_NEXT_RELEASE) { + return null + } + return (
diff --git a/app/[lang]/(live)/@bookingwidget/page.tsx b/app/[lang]/(live)/@bookingwidget/page.tsx index 13a414cba..7e197d0fa 100644 --- a/app/[lang]/(live)/@bookingwidget/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/page.tsx @@ -1,8 +1,13 @@ +import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" import BookingWidget, { preload } from "@/components/BookingWidget" export default async function BookingWidgetPage() { + if (env.HIDE_FOR_NEXT_RELEASE) { + return null + } + preload() // Get the booking widget show/hide status based on page specific settings diff --git a/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx b/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx new file mode 100644 index 000000000..03a82e5f5 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx @@ -0,0 +1 @@ +export { default } from "../page" diff --git a/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/[contentType]/[uid]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@sitewidealert/default.tsx b/app/[lang]/(live)/@sitewidealert/default.tsx new file mode 100644 index 000000000..83ec2818e --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/default.tsx @@ -0,0 +1 @@ +export { default } from "./page" diff --git a/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx b/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx new file mode 100644 index 000000000..2ebaca014 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/my-pages/[...path]/page.tsx @@ -0,0 +1 @@ +export { default } from "../../page" diff --git a/app/[lang]/(live)/@sitewidealert/page.tsx b/app/[lang]/(live)/@sitewidealert/page.tsx new file mode 100644 index 000000000..be7ae2256 --- /dev/null +++ b/app/[lang]/(live)/@sitewidealert/page.tsx @@ -0,0 +1,23 @@ +import { Suspense } from "react" + +import { env } from "@/env/server" + +import SitewideAlert, { preload } from "@/components/SitewideAlert" +import { setLang } from "@/i18n/serverContext" + +import type { LangParams, PageArgs } from "@/types/params" + +export default function SitewideAlertPage({ params }: PageArgs) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return null + } + + setLang(params.lang) + preload() + + return ( + + + + ) +} diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index fde914bbe..4b19a3eb0 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -25,12 +25,14 @@ export default async function RootLayout({ children, footer, header, + sitewidealert, params, }: React.PropsWithChildren< LayoutArgs & { bookingwidget: React.ReactNode footer: React.ReactNode header: React.ReactNode + sitewidealert: React.ReactNode } >) { setLang(params.lang) @@ -60,6 +62,7 @@ export default async function RootLayout({ + {!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}} {header} {!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}} {children} diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts index 4154aff14..baa8e4b8a 100644 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ b/app/api/web/payment-callback/[lang]/[status]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server" -import { env } from "process" +import { BOOKING_CONFIRMATION_NUMBER } from "@/constants/booking" import { Lang } from "@/constants/languages" import { bookingConfirmation, @@ -17,14 +17,24 @@ export async function GET( console.log(`[payment-callback] callback started`) const lang = params.lang as Lang const status = params.status - const returnUrl = new URL(`${publicURL}/${payment[lang]}`) - if (status === "success") { + const queryParams = request.nextUrl.searchParams + const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER) + + if (status === "success" && confirmationNumber) { const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation[lang]}`) + confirmationUrl.searchParams.set( + BOOKING_CONFIRMATION_NUMBER, + confirmationNumber + ) + console.log(`[payment-callback] redirecting to: ${confirmationUrl}`) return NextResponse.redirect(confirmationUrl) } + const returnUrl = new URL(`${publicURL}/${payment[lang]}`) + returnUrl.search = queryParams.toString() + if (status === "cancel") { returnUrl.searchParams.set("cancel", "true") } diff --git a/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx b/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx index 4c39dafc5..bf6af6294 100644 --- a/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx +++ b/components/Blocks/DynamicContent/SignupFormWrapper/index.tsx @@ -3,7 +3,7 @@ import { redirect } from "next/navigation" import { overview } from "@/constants/routes/myPages" import { auth } from "@/auth" -import Form from "@/components/Forms/Register" +import SignupForm from "@/components/Forms/Signup" import { getLang } from "@/i18n/serverContext" import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent" @@ -16,5 +16,5 @@ export default async function SignupFormWrapper({ // We don't want to allow users to access signup if they are already authenticated. redirect(overview[getLang()]) } - return
+ return } diff --git a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index 5ab6c0b5d..9e63c0f77 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -1,8 +1,12 @@ "use client" + +import { useState } from "react" + import { dt } from "@/lib/dt" import { CalendarIcon } from "@/components/Icons" import Image from "@/components/Image" +import LoadingSpinner from "@/components/LoadingSpinner" import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" @@ -14,6 +18,10 @@ import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" export default function StayCard({ stay }: StayCardProps) { const lang = useLang() + + // TODO: Temporary loading. Remove when current web is deleted. + const [loading, setLoading] = useState(false) + const { checkinDate, checkoutDate, hotelInformation, bookingUrl } = stay.attributes @@ -25,7 +33,11 @@ export default function StayCard({ stay }: StayCardProps) { const departDateTime = depart.format("YYYY-MM-DD") return ( - + setLoading(true)} + >
+ {loading && ( +
+ +
+ )} ) } diff --git a/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css b/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css index 1d3a685d7..bfadbad67 100644 --- a/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css +++ b/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css @@ -6,6 +6,11 @@ overflow: hidden; } +.link { + text-decoration: none; + position: relative; +} + .stay:hover { border: 1.5px solid var(--Base-Border-Hover); } @@ -41,3 +46,15 @@ display: flex; gap: var(--Spacing-x-half); } + +.loadingcontainer { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 70px; + background: rgb(255 255 255 / 80%); + display: flex; + align-items: center; + justify-content: center; +} diff --git a/components/Blocks/ShortcutsList/index.tsx b/components/Blocks/ShortcutsList/index.tsx index 746f56492..12c718229 100644 --- a/components/Blocks/ShortcutsList/index.tsx +++ b/components/Blocks/ShortcutsList/index.tsx @@ -18,17 +18,20 @@ export default function ShortcutsList({ const leftColumn = shortcuts.slice(0, middleIndex) const rightColumn = shortcuts.slice(middleIndex) - const classNames = hasTwoColumns - ? { - section: styles.twoColumnSection, - leftColumn: styles.leftColumn, - rightColumn: styles.rightColumn, - } - : { - section: styles.oneColumnSection, - leftColumn: styles.leftColumnBottomBorder, - rightColumn: "", - } + const classNames = + hasTwoColumns && shortcuts.length > 1 + ? { + section: styles.twoColumnSection, + leftColumn: styles.leftColumn, + rightColumn: styles.rightColumn, + } + : { + section: styles.oneColumnSection, + leftColumn: + shortcuts.length === 1 + ? styles.leftColumnBorderBottomNone + : styles.leftColumnBorderBottom, + } return ( diff --git a/components/Blocks/ShortcutsList/shortcutsList.module.css b/components/Blocks/ShortcutsList/shortcutsList.module.css index 37daea30f..26dd448ac 100644 --- a/components/Blocks/ShortcutsList/shortcutsList.module.css +++ b/components/Blocks/ShortcutsList/shortcutsList.module.css @@ -7,10 +7,14 @@ } .leftColumn, -.leftColumnBottomBorder { +.leftColumnBorderBottom { border-bottom: 1px solid var(--Base-Border-Subtle); } +.leftColumnBorderBottomNone { + border-bottom: none; +} + @media screen and (min-width: 1367px) { .twoColumnSection { grid-template-columns: 1fr 1fr; diff --git a/components/Blocks/TextCols/textcols.module.css b/components/Blocks/TextCols/textcols.module.css index b3e9e2824..8c3a1bda1 100644 --- a/components/Blocks/TextCols/textcols.module.css +++ b/components/Blocks/TextCols/textcols.module.css @@ -2,7 +2,6 @@ display: flex; flex-direction: column; gap: var(--Spacing-x3); - padding: var(--Spacing-x3) var(--Spacing-x4); } .column { diff --git a/components/Blocks/UspGrid/uspgrid.module.css b/components/Blocks/UspGrid/uspgrid.module.css index 042acd9b3..91c6f8fd8 100644 --- a/components/Blocks/UspGrid/uspgrid.module.css +++ b/components/Blocks/UspGrid/uspgrid.module.css @@ -1,7 +1,6 @@ .grid { display: grid; gap: var(--Spacing-x3); - padding: var(--Spacing-x3) var(--Spacing-x4); } @media screen and (min-width: 767px) { .grid { diff --git a/components/BookingWidget/Client.tsx b/components/BookingWidget/Client.tsx index eded233f0..68a6a1fc2 100644 --- a/components/BookingWidget/Client.tsx +++ b/components/BookingWidget/Client.tsx @@ -42,8 +42,8 @@ export default function BookingWidgetClient({ date: { // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. - from: dt().utc().format("YYYY-MM-DD"), - to: dt().utc().add(1, "day").format("YYYY-MM-DD"), + fromDate: dt().utc().format("YYYY-MM-DD"), + toDate: dt().utc().add(1, "day").format("YYYY-MM-DD"), }, bookingCode: "", redemption: false, diff --git a/components/BookingWidget/bookingWidget.module.css b/components/BookingWidget/bookingWidget.module.css index 461057bf1..22ff009ee 100644 --- a/components/BookingWidget/bookingWidget.module.css +++ b/components/BookingWidget/bookingWidget.module.css @@ -42,7 +42,7 @@ box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05); position: sticky; top: 0; - z-index: 9; + z-index: 10; background-color: var(--Base-Surface-Primary-light-Normal); } diff --git a/components/ContentType/ContentPage/contentPage.module.css b/components/ContentType/ContentPage/contentPage.module.css index aa0036f83..c0c94aa42 100644 --- a/components/ContentType/ContentPage/contentPage.module.css +++ b/components/ContentType/ContentPage/contentPage.module.css @@ -37,6 +37,7 @@ display: grid; padding: var(--Spacing-x4) var(--Spacing-x2) 0; gap: var(--Spacing-x4); + align-items: start; } .mainContent { diff --git a/components/ContentType/ContentPage/index.tsx b/components/ContentType/ContentPage/index.tsx index 5c5c63893..b6a556dc5 100644 --- a/components/ContentType/ContentPage/index.tsx +++ b/components/ContentType/ContentPage/index.tsx @@ -44,6 +44,7 @@ export default async function ContentPage() {
) : null} diff --git a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css index f7dcea5fc..e35dc13a6 100644 --- a/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css +++ b/components/ContentType/HotelPage/AmenitiesList/amenitiesList.module.css @@ -20,6 +20,10 @@ gap: var(--Spacing-x1); } +.icon { + flex-shrink: 0; +} + .showAllAmenities { width: fit-content; } diff --git a/components/ContentType/HotelPage/AmenitiesList/index.tsx b/components/ContentType/HotelPage/AmenitiesList/index.tsx index 8b6a1977e..8c90f19c2 100644 --- a/components/ContentType/HotelPage/AmenitiesList/index.tsx +++ b/components/ContentType/HotelPage/AmenitiesList/index.tsx @@ -16,9 +16,7 @@ export default async function AmenitiesList({ detailedFacilities, }: AmenitiesListProps) { const intl = await getIntl() - const sortedAmenities = detailedFacilities - .sort((a, b) => b.sortOrder - a.sortOrder) - .slice(0, 5) + const facilities = detailedFacilities.slice(0, 5) const lang = getLang() return (
@@ -26,11 +24,13 @@ export default async function AmenitiesList({ {intl.formatMessage({ id: "At the hotel" })}
- {sortedAmenities.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.name) + {facilities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) return (
- {IconComponent && } + {IconComponent && ( + + )} {facility.name}
) diff --git a/components/ContentType/HotelPage/TabNavigation/index.tsx b/components/ContentType/HotelPage/TabNavigation/index.tsx index 8e80459b4..6f8b29130 100644 --- a/components/ContentType/HotelPage/TabNavigation/index.tsx +++ b/components/ContentType/HotelPage/TabNavigation/index.tsx @@ -88,7 +88,7 @@ export default function TabNavigation({ scroll={true} onClick={pauseScrollSpy} > - {intl.formatMessage({ id: link.text })} + {link.text} ) })} diff --git a/components/ContentType/HotelPage/data.ts b/components/ContentType/HotelPage/data.ts index ace3a0408..e4043c53e 100644 --- a/components/ContentType/HotelPage/data.ts +++ b/components/ContentType/HotelPage/data.ts @@ -3,21 +3,287 @@ import { FC } from "react" import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name" import { IconName, IconProps } from "@/types/components/icon" +import { FacilityEnum } from "@/types/enums/facilities" -const facilityToIconMap: { [key: string]: IconName } = { - Bar: IconName.Bar, - "Bikes for loan": IconName.Biking, - Gym: IconName.Fitness, - "Free WiFi": IconName.Wifi, - //TODO: Ask design team what icon(s) should be used for meetings. - "Meeting rooms": IconName.People2, - "Meeting / conference facilities": IconName.People2, - "Pet-friendly rooms": IconName.Pets, - Sauna: IconName.Sauna, - Restaurant: IconName.Restaurant, +const facilityToIconMap: Record = { + [FacilityEnum.Bar]: IconName.LocalBar, + [FacilityEnum.Skybar]: IconName.LocalBar, + [FacilityEnum.RooftopBar]: IconName.LocalBar, + [FacilityEnum.BikesForLoan]: IconName.Biking, + [FacilityEnum.Gym]: IconName.Fitness, + [FacilityEnum.GymTrainingFacilities]: IconName.Fitness, + [FacilityEnum.KeyAccessOnlyToHealthClubGym]: IconName.Fitness, + [FacilityEnum.FreeWiFi]: IconName.Wifi, + [FacilityEnum.MeetingRooms]: IconName.People2, + [FacilityEnum.MeetingConferenceFacilities]: IconName.People2, + [FacilityEnum.PetFriendlyRooms]: IconName.Pets, + [FacilityEnum.Sauna]: IconName.Sauna, + [FacilityEnum.Restaurant]: IconName.Restaurant, + [FacilityEnum.ParkingGarage]: IconName.Garage, + [FacilityEnum.ParkingElectricCharging]: IconName.ElectricCar, + [FacilityEnum.ParkingFreeParking]: IconName.Parking, + [FacilityEnum.ParkingOutdoor]: IconName.Parking, + [FacilityEnum.ParkingAdditionalCost]: IconName.Parking, + [FacilityEnum.DisabledParking]: IconName.Parking, + [FacilityEnum.OutdoorTerrace]: IconName.OutdoorFurniture, + [FacilityEnum.RoomService]: IconName.RoomService, + [FacilityEnum.LaundryRoom]: IconName.LaundryMachine, + [FacilityEnum.LaundryService]: IconName.LaundryMachine, + [FacilityEnum.LaundryServiceExpress]: IconName.LaundryMachine, + [FacilityEnum.ScandicShop24Hrs]: IconName.ConvenienceStore24h, + [FacilityEnum.ServesBreakfastAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.ServesBreakfastNotAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.ServesOrganicBreakfastAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.ServesOrganicBreakfastNotAlwaysIncluded]: IconName.CoffeeAlt, + [FacilityEnum.Breakfast]: IconName.CoffeeAlt, + [FacilityEnum.EBikesChargingStation]: IconName.ElectricBike, + [FacilityEnum.Shopping]: IconName.Shopping, + [FacilityEnum.Golf]: IconName.Golf, + [FacilityEnum.GolfCourse0To30Km]: IconName.Golf, + [FacilityEnum.TVWithChromecast1]: IconName.TvCasting, + [FacilityEnum.TVWithChromecast2]: IconName.TvCasting, + [FacilityEnum.DJLiveMusic]: IconName.Nightlife, + [FacilityEnum.DiscoNightClub]: IconName.Nightlife, + [FacilityEnum.CoffeeInReceptionAtCharge]: IconName.CoffeeAlt, + [FacilityEnum.CoffeeShop]: IconName.CoffeeAlt, + [FacilityEnum.CoffeeTeaFacilities]: IconName.CoffeeAlt, + [FacilityEnum.SkateboardsForLoan]: IconName.Skateboarding, + [FacilityEnum.KayaksForLoan]: IconName.Kayaking, + [FacilityEnum.LifestyleConcierge]: IconName.Concierge, + [FacilityEnum.WellnessAndSaunaEntranceFeeAdmission16PlusYears]: + IconName.Sauna, + [FacilityEnum.WellnessPoolSaunaEntranceFeeAdmission16PlusYears]: + IconName.Sauna, + [FacilityEnum.Cafe]: IconName.Restaurant, + [FacilityEnum.Pool]: IconName.Swim, + [FacilityEnum.PoolSwimmingPoolJacuzziAtHotel]: IconName.Swim, + [FacilityEnum.VendingMachineWithNecessities]: IconName.Groceries, + + [FacilityEnum.Jacuzzi]: IconName.StarFilled, + [FacilityEnum.JacuzziInRoom]: IconName.StarFilled, + + [FacilityEnum.AccessibleBathingControls]: IconName.StarFilled, + [FacilityEnum.AccessibleBathtubs]: IconName.StarFilled, + [FacilityEnum.AccessibleElevators]: IconName.StarFilled, + [FacilityEnum.AccessibleLightSwitch]: IconName.StarFilled, + [FacilityEnum.AccessibleRoomsAtHotel1]: IconName.StarFilled, + [FacilityEnum.AccessibleRoomsAtHotel2]: IconName.StarFilled, + [FacilityEnum.AccessibleToilets]: IconName.StarFilled, + [FacilityEnum.AccessibleWashBasins]: IconName.StarFilled, + [FacilityEnum.AdaptedRoomDoors]: IconName.StarFilled, + [FacilityEnum.AdjoiningConventionCentre]: IconName.StarFilled, + [FacilityEnum.AirConAirCooling]: IconName.StarFilled, + [FacilityEnum.AirConditioningInRoom]: IconName.StarFilled, + [FacilityEnum.AirportMaxDistance8Km]: IconName.StarFilled, + [FacilityEnum.AlarmsContinuouslyMonitored]: IconName.StarFilled, + [FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllGuestRooms]: + IconName.StarFilled, + [FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllHallways]: + IconName.StarFilled, + [FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllPublicAreas]: + IconName.StarFilled, + [FacilityEnum.AllAudibleSmokeAlarmsHardwired]: IconName.StarFilled, + [FacilityEnum.AllExteriorDoorsRequireKeyAccessAtNightOrAutomaticallyLock]: + IconName.StarFilled, + [FacilityEnum.AllGuestRoomDoorsHaveViewports]: IconName.StarFilled, + [FacilityEnum.AllGuestRoomDoorsSelfClosing]: IconName.StarFilled, + [FacilityEnum.AllParkingAreasPatrolled]: IconName.StarFilled, + [FacilityEnum.AllParkingAreasWellLit]: IconName.StarFilled, + [FacilityEnum.AllStairsWellsVentilated]: IconName.StarFilled, + [FacilityEnum.ArmchairBed]: IconName.StarFilled, + [FacilityEnum.AudibleAlarms]: IconName.StarFilled, + [FacilityEnum.AudibleSmokeAlarmsInAllHalls]: IconName.StarFilled, + [FacilityEnum.AudibleSmokeAlarmsInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.AudibleSmokeAlarmsInAllRooms]: IconName.StarFilled, + [FacilityEnum.AudioVisualEquipmentAvailable]: IconName.StarFilled, + [FacilityEnum.AutolinkFireDepartment]: IconName.StarFilled, + [FacilityEnum.AutomatedExternalDefibrillatorOnSiteAED]: IconName.StarFilled, + [FacilityEnum.AutomaticFireDoors]: IconName.StarFilled, + [FacilityEnum.AutoRecallElevators]: IconName.StarFilled, + [FacilityEnum.BalconiesAccessibleToAdjoiningRooms]: IconName.StarFilled, + [FacilityEnum.Ballroom]: IconName.StarFilled, + [FacilityEnum.Banquet]: IconName.StarFilled, + [FacilityEnum.BasicMedicalEquipmentOnSite]: IconName.StarFilled, + [FacilityEnum.BathroomsAdaptedForDisabledGuests]: IconName.StarFilled, + [FacilityEnum.Beach]: IconName.StarFilled, + [FacilityEnum.Beach0To1Km]: IconName.StarFilled, + [FacilityEnum.BeautySalon]: IconName.StarFilled, + [FacilityEnum.BedroomsWithWheelchairAccess]: IconName.StarFilled, + [FacilityEnum.Bowling]: IconName.StarFilled, + [FacilityEnum.BrailleLargePrintHotelLiterature]: IconName.StarFilled, + [FacilityEnum.BrailleLargePrintMenus]: IconName.StarFilled, + [FacilityEnum.Business1]: IconName.StarFilled, + [FacilityEnum.Business2]: IconName.StarFilled, + [FacilityEnum.BusinessCentre]: IconName.StarFilled, + [FacilityEnum.CashFree8pmTill6am]: IconName.StarFilled, + [FacilityEnum.CashFreeHotel]: IconName.StarFilled, + [FacilityEnum.ChildrenWelcome]: IconName.StarFilled, + [FacilityEnum.City]: IconName.StarFilled, + [FacilityEnum.ColourTVInRoomsAllScandicHotels]: IconName.StarFilled, + [FacilityEnum.ComplimentaryColdRefreshments]: IconName.StarFilled, + [FacilityEnum.CongressHall]: IconName.StarFilled, + [FacilityEnum.ConventionCentre]: IconName.StarFilled, + [FacilityEnum.Couples]: IconName.StarFilled, + [FacilityEnum.DeadboltsOnConnectingDoors]: IconName.StarFilled, + [FacilityEnum.DeadboltsSecondaryLocksOnAllGuestRoomDoors]: + IconName.StarFilled, + [FacilityEnum.Defibrillator]: IconName.StarFilled, + [FacilityEnum.Desk]: IconName.StarFilled, + [FacilityEnum.DirectDialPhoneInRoomsAllScandic]: IconName.StarFilled, + [FacilityEnum.DisabledEmergencyPlan1]: IconName.StarFilled, + [FacilityEnum.DisabledEmergencyPlan2]: IconName.StarFilled, + [FacilityEnum.DO_NOT_USE_Restaurant]: IconName.StarFilled, + [FacilityEnum.Downtown]: IconName.StarFilled, + [FacilityEnum.DrinkableTapWater]: IconName.StarFilled, + [FacilityEnum.DVDPlayer]: IconName.StarFilled, + [FacilityEnum.ElectronicKeyCards]: IconName.StarFilled, + [FacilityEnum.Elevator]: IconName.StarFilled, + [FacilityEnum.EmergencyBackUpGenerators]: IconName.StarFilled, + [FacilityEnum.EmergencyCallButtonOnPhone]: IconName.StarFilled, + [FacilityEnum.EmergencyCodesOrButtonsInRooms]: IconName.StarFilled, + [FacilityEnum.EmergencyEvacuationPlan1]: IconName.StarFilled, + [FacilityEnum.EmergencyEvacuationPlan2]: IconName.StarFilled, + [FacilityEnum.EmergencyEvaluationDrillFrequency]: IconName.StarFilled, + [FacilityEnum.EmergencyInfoInAllRooms]: IconName.StarFilled, + [FacilityEnum.EmergencyLightingAllScandic]: IconName.StarFilled, + [FacilityEnum.EmergencyLightningInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.EmergencyServiceResponseTimeInMinutes]: IconName.StarFilled, + [FacilityEnum.Entertainment]: IconName.StarFilled, + [FacilityEnum.EventVenue]: IconName.StarFilled, + [FacilityEnum.ExchangeFacility]: IconName.StarFilled, + [FacilityEnum.ExitMapsInRooms]: IconName.StarFilled, + [FacilityEnum.ExitSignsLit]: IconName.StarFilled, + [FacilityEnum.ExtraFamilyFriendly]: IconName.StarFilled, + [FacilityEnum.Families]: IconName.StarFilled, + [FacilityEnum.FaxFacilityInRoom]: IconName.StarFilled, + [FacilityEnum.Financial]: IconName.StarFilled, + [FacilityEnum.FireDetectorsAllScandic]: IconName.StarFilled, + [FacilityEnum.FireDetectorsInAllHalls]: IconName.StarFilled, + [FacilityEnum.FireDetectorsInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.FireDetectorsInAllRooms]: IconName.StarFilled, + [FacilityEnum.FireExtinguishersInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.FireExtinguishersInPublicAreasAllScandic]: IconName.StarFilled, + [FacilityEnum.FireSafetyAllScandic]: IconName.StarFilled, + [FacilityEnum.FirstAidAvailable]: IconName.StarFilled, + [FacilityEnum.FoodDrinks247]: IconName.StarFilled, + [FacilityEnum.GiftShop]: IconName.StarFilled, + [FacilityEnum.GuestRoomDoorsHaveASecondLock]: IconName.StarFilled, + [FacilityEnum.Hairdresser]: IconName.StarFilled, + [FacilityEnum.HairdryerInRoomAllScandic]: IconName.StarFilled, + [FacilityEnum.HandicapFacilities]: IconName.StarFilled, + [FacilityEnum.HandrailsInBathrooms]: IconName.StarFilled, + [FacilityEnum.HearingInductionLoops]: IconName.StarFilled, + [FacilityEnum.Highway1]: IconName.StarFilled, + [FacilityEnum.Highway2]: IconName.StarFilled, + [FacilityEnum.Hiking0To3Km]: IconName.StarFilled, + [FacilityEnum.HotelCompliesWithAAASecurityStandards]: IconName.StarFilled, + [FacilityEnum.HotelIsFollowingScandicsSafetySecurityPolicy]: + IconName.StarFilled, + [FacilityEnum.HotelWorksAccordingToScandicsAccessibilityConcepts]: + IconName.StarFilled, + [FacilityEnum.IceMachine]: IconName.StarFilled, + [FacilityEnum.IceMachineReception]: IconName.StarFilled, + [FacilityEnum.IDRequiredToReplaceAGuestRoomKey]: IconName.StarFilled, + [FacilityEnum.IfNoWhatAreTheHoursUse24ClockEx0000To0600]: IconName.StarFilled, + [FacilityEnum.InCountry]: IconName.StarFilled, + [FacilityEnum.IndustrialPark]: IconName.StarFilled, + [FacilityEnum.InternetHighSpeedInternetConnectionAllScandic]: + IconName.StarFilled, + [FacilityEnum.InternetHotSpotsAllScandic]: IconName.StarFilled, + [FacilityEnum.IroningRoom]: IconName.StarFilled, + [FacilityEnum.IronIroningBoardAllScandic]: IconName.StarFilled, + [FacilityEnum.KeyAccessOnlySecuredFloorsAvailable]: IconName.StarFilled, + [FacilityEnum.KidsPlayRoom]: IconName.StarFilled, + [FacilityEnum.KidsUpToAndIncluding12YearsStayForFree]: IconName.StarFilled, + [FacilityEnum.KitchenInRoom]: IconName.StarFilled, + [FacilityEnum.Lake0To1Km]: IconName.StarFilled, + [FacilityEnum.LakeOrSea0To1Km]: IconName.StarFilled, + [FacilityEnum.LaptopSafe]: IconName.StarFilled, + [FacilityEnum.Leisure]: IconName.StarFilled, + [FacilityEnum.LuggageLockers]: IconName.StarFilled, + [FacilityEnum.Massage]: IconName.StarFilled, + [FacilityEnum.MinibarInRoom]: IconName.StarFilled, + [FacilityEnum.MobileLift]: IconName.StarFilled, + [FacilityEnum.Mountains0To1Km]: IconName.StarFilled, + [FacilityEnum.MovieChannelsInRoomAllScandic]: IconName.StarFilled, + [FacilityEnum.MultipleExitsOnEachFloor]: IconName.StarFilled, + [FacilityEnum.NonSmokingRoomsAllScandic]: IconName.StarFilled, + [FacilityEnum.OnSiteTrainingFacilities]: IconName.StarFilled, + [FacilityEnum.OtherExplainInBriefDescription]: IconName.StarFilled, + [FacilityEnum.OvernightSecurity]: IconName.StarFilled, + [FacilityEnum.ParkingAttendant]: IconName.StarFilled, + [FacilityEnum.PCHookUpInRoom]: IconName.StarFilled, + [FacilityEnum.PillowAlarmsAvailable]: IconName.StarFilled, + [FacilityEnum.PlayStationInPlayArea]: IconName.StarFilled, + [FacilityEnum.PrintingService]: IconName.StarFilled, + [FacilityEnum.PropertyMeetsRequirementsFireSafety]: IconName.StarFilled, + [FacilityEnum.PublicAddressSystem]: IconName.StarFilled, + [FacilityEnum.RelaxationSuite]: IconName.StarFilled, + [FacilityEnum.RestrictedRoomAccessAllScandic]: IconName.StarFilled, + [FacilityEnum.RoomsAccessibleFromTheInterior]: IconName.StarFilled, + [FacilityEnum.RoomWindowsOpen]: IconName.StarFilled, + [FacilityEnum.RoomWindowsThatOpenHaveLockingDevice]: IconName.StarFilled, + [FacilityEnum.Rural1]: IconName.StarFilled, + [FacilityEnum.Rural2]: IconName.StarFilled, + [FacilityEnum.SafeDepositBoxInRoomsAllScandic]: IconName.StarFilled, + [FacilityEnum.SafeDepositBoxInRoomsCanHoldA17InchLaptop]: IconName.StarFilled, + [FacilityEnum.SafeDepositBoxInRoomsCannotHoldALaptop]: IconName.StarFilled, + [FacilityEnum.SafetyChainsOnGuestRoomDoor]: IconName.StarFilled, + [FacilityEnum.SecondaryLocksOnSlidingGlassDoors]: IconName.StarFilled, + [FacilityEnum.SecondaryLocksOnWindows]: IconName.StarFilled, + [FacilityEnum.Security24Hours]: IconName.StarFilled, + [FacilityEnum.SecurityEscortsAvailableOnRequest]: IconName.StarFilled, + [FacilityEnum.SecurityPersonnelOnSite]: IconName.StarFilled, + [FacilityEnum.SeparateFloorsForWomen]: IconName.StarFilled, + [FacilityEnum.ServiceGuideDogsAllowed]: IconName.StarFilled, + [FacilityEnum.ServiceSecurity24Hrs]: IconName.StarFilled, + [FacilityEnum.Skiing0To1Km]: IconName.StarFilled, + [FacilityEnum.SmokeDetectorsAllScandic]: IconName.StarFilled, + [FacilityEnum.Solarium]: IconName.StarFilled, + [FacilityEnum.SpecialNeedsMenus]: IconName.StarFilled, + [FacilityEnum.Sports]: IconName.StarFilled, + [FacilityEnum.SprinklersAllScandic]: IconName.StarFilled, + [FacilityEnum.SprinklersInAllHalls]: IconName.StarFilled, + [FacilityEnum.SprinklersInAllPublicAreas]: IconName.StarFilled, + [FacilityEnum.SprinklersInAllRooms]: IconName.StarFilled, + [FacilityEnum.StaffInDuplicateKeys]: IconName.StarFilled, + [FacilityEnum.StaffRedCrossCertifiedInCPR]: IconName.StarFilled, + [FacilityEnum.StaffTrainedForDisabledGuests]: IconName.StarFilled, + [FacilityEnum.StaffTrainedInAutomatedExternalDefibrillatorUsageAED]: + IconName.StarFilled, + [FacilityEnum.StaffTrainedInCPR]: IconName.StarFilled, + [FacilityEnum.StaffTrainedInFirstAid]: IconName.StarFilled, + [FacilityEnum.StaffTrainedInFirstAidTechniques]: IconName.StarFilled, + [FacilityEnum.StaffTrainedToCaterForDisabledGuestsAllScandic]: + IconName.StarFilled, + [FacilityEnum.Suburbs]: IconName.StarFilled, + [FacilityEnum.SwingboltLock]: IconName.StarFilled, + [FacilityEnum.TeleConferencingFacilitiesAvailable]: IconName.StarFilled, + [FacilityEnum.TelevisionsWithSubtitlesOrClosedCaptions]: IconName.StarFilled, + [FacilityEnum.Tennis1]: IconName.StarFilled, + [FacilityEnum.Tennis2]: IconName.StarFilled, + [FacilityEnum.TennisPadel]: IconName.StarFilled, + [FacilityEnum.Theatre]: IconName.StarFilled, + [FacilityEnum.TrouserPress]: IconName.StarFilled, + [FacilityEnum.UniformSecurityOnPremises]: IconName.StarFilled, + [FacilityEnum.UtilityRoomForIroning]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceInHallways]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceInPublicAreas]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceMonitored24HrsADay]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceOfAllParkingAreas]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceOfExteriorFrontEntrance]: IconName.StarFilled, + [FacilityEnum.VideoSurveillanceRecorded24HrsADayParkingArea]: + IconName.StarFilled, + [FacilityEnum.WallMountedCycleRack]: IconName.StarFilled, + [FacilityEnum.WellLitWalkways]: IconName.StarFilled, + [FacilityEnum.WheelchairAccess]: IconName.StarFilled, + [FacilityEnum.WideCorridors]: IconName.StarFilled, + [FacilityEnum.WideEntrance]: IconName.StarFilled, + [FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled, + [FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled, } -export function mapFacilityToIcon(facilityName: string): FC | null { - const iconName = facilityToIconMap[facilityName] +export function mapFacilityToIcon(id: FacilityEnum): FC | null { + const iconName = facilityToIconMap[id] return getIconByIconName(iconName) || null } diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 09f900159..091444fe2 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -11,7 +11,6 @@ "mapContainer"; margin: 0 auto; max-width: var(--max-width); - z-index: 0; } .hotelImages { @@ -30,6 +29,11 @@ display: none; } +.overview { + display: grid; + gap: var(--Spacing-x3); +} + .introContainer { display: flex; flex-wrap: wrap; @@ -38,6 +42,11 @@ scroll-margin-top: var(--hotel-page-scroll-margin-top); } +.alertsContainer { + display: grid; + gap: var(--Spacing-x2); +} + @media screen and (min-width: 1367px) { .pageContainer { grid-template-areas: @@ -77,10 +86,4 @@ padding-left: var(--Spacing-x5); padding-right: var(--Spacing-x5); } - .introContainer { - grid-template-columns: 38rem minmax(max-content, 16rem); - justify-content: space-between; - gap: var(--Spacing-x2); - align-items: end; - } } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 5b0139d6f..d10244040 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -4,6 +4,7 @@ import { serverClient } from "@/lib/trpc/server" import AccordionSection from "@/components/Blocks/Accordion" import SidePeekProvider from "@/components/SidePeekProvider" +import Alert from "@/components/TempDesignSystem/Alert" import SidePeek from "@/components/TempDesignSystem/SidePeek" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" @@ -49,6 +50,7 @@ export default async function HotelPage() { pointsOfInterest, facilities, faq, + alerts, } = hotelData const topThreePois = pointsOfInterest.slice(0, 3) @@ -69,16 +71,30 @@ export default async function HotelPage() { hasFAQ={!!faq} />
-
- +
+
+ - + +
+ {alerts.length ? ( +
+ {alerts.map((alert) => ( + + ))} +
+ ) : null}
diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx index 503c0c6ac..b07b1a0d8 100644 --- a/components/DatePicker/index.tsx +++ b/components/DatePicker/index.tsx @@ -44,22 +44,22 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { function handleSelectDate(selected: Date) { if (isSelectingFrom) { setValue(name, { - from: dt(selected).format("YYYY-MM-DD"), - to: undefined, + fromDate: dt(selected).format("YYYY-MM-DD"), + toDate: undefined, }) setIsSelectingFrom(false) } else { - const fromDate = dt(selectedDate.from) + const fromDate = dt(selectedDate.fromDate) const toDate = dt(selected) if (toDate.isAfter(fromDate)) { setValue(name, { - from: selectedDate.from, - to: toDate.format("YYYY-MM-DD"), + fromDate: selectedDate.fromDate, + toDate: toDate.format("YYYY-MM-DD"), }) } else { setValue(name, { - from: toDate.format("YYYY-MM-DD"), - to: selectedDate.from, + fromDate: toDate.format("YYYY-MM-DD"), + toDate: selectedDate.fromDate, }) } setIsSelectingFrom(true) @@ -79,11 +79,11 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { } }, [setIsOpen]) - const selectedFromDate = dt(selectedDate.from) + const selectedFromDate = dt(selectedDate.fromDate) .locale(lang) .format("ddd D MMM") - const selectedToDate = !!selectedDate.to - ? dt(selectedDate.to).locale(lang).format("ddd D MMM") + const selectedToDate = !!selectedDate.toDate + ? dt(selectedDate.toDate).locale(lang).format("ddd D MMM") : "" return ( @@ -93,8 +93,8 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { {selectedFromDate} - {selectedToDate} - - + +
{image.meta.caption} diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index bbc763aef..bd7a65ba6 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -27,7 +27,7 @@ export default function FormContent({ const rooms = intl.formatMessage({ id: "Guests & Rooms" }) - const nights = dt(selectedDate.to).diff(dt(selectedDate.from), "days") + const nights = dt(selectedDate.toDate).diff(dt(selectedDate.fromDate), "days") return ( <> diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index cfa3cb03f..aa42b542d 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -18,8 +18,8 @@ export const bookingWidgetSchema = z.object({ bookingCode: z.string(), // Update this as required when working with booking codes component date: z.object({ // Update this as required once started working with Date picker in Nights component - from: z.string(), - to: z.string(), + fromDate: z.string(), + toDate: z.string(), }), location: z.string().refine( (value) => { diff --git a/components/Forms/Register/form.module.css b/components/Forms/Signup/form.module.css similarity index 93% rename from components/Forms/Register/form.module.css rename to components/Forms/Signup/form.module.css index 612cbc8f8..dc913b792 100644 --- a/components/Forms/Register/form.module.css +++ b/components/Forms/Signup/form.module.css @@ -46,4 +46,8 @@ .nameInputs { grid-template-columns: 1fr 1fr; } + + .signUpButton { + width: fit-content; + } } diff --git a/components/Forms/Register/index.tsx b/components/Forms/Signup/index.tsx similarity index 77% rename from components/Forms/Register/index.tsx rename to components/Forms/Signup/index.tsx index ed82617b0..ad66f6282 100644 --- a/components/Forms/Register/index.tsx +++ b/components/Forms/Signup/index.tsx @@ -22,16 +22,21 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { toast } from "@/components/TempDesignSystem/Toasts" import useLang from "@/hooks/useLang" -import { RegisterSchema, registerSchema } from "./schema" +import { SignUpSchema, signUpSchema } from "./schema" import styles from "./form.module.css" -import type { RegisterFormProps } from "@/types/components/form/registerForm" +import type { SignUpFormProps } from "@/types/components/form/signupForm" -export default function Form({ link, subtitle, title }: RegisterFormProps) { +export default function SignupForm({ link, subtitle, title }: SignUpFormProps) { const intl = useIntl() const lang = useLang() - const methods = useForm({ + 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 methods = useForm({ defaultValues: { firstName: "", lastName: "", @@ -47,15 +52,11 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) { }, mode: "all", criteriaMode: "all", - resolver: zodResolver(registerSchema), + resolver: zodResolver(signUpSchema), reValidateMode: "onChange", }) - const country = intl.formatMessage({ id: "Country" }) - const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}` - const phoneNumber = intl.formatMessage({ id: "Phone number" }) - const zipCode = intl.formatMessage({ id: "Zip code" }) - async function handleSubmit(data: RegisterSchema) { + async function onSubmit(data: SignUpSchema) { try { const result = await registerUser(data) if (result && !result.success) { @@ -78,12 +79,12 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
@@ -94,12 +95,12 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
@@ -170,14 +171,36 @@ export default function Form({ link, subtitle, title }: RegisterFormProps) {
- + + {/* + This is a manual validation trigger workaround: + - The Controller component (which Input uses) doesn't re-render on submit, + which prevents automatic error display. + - Future fix requires Input component refactoring (out of scope for now). + */} + {!methods.formState.isValid ? ( + + ) : ( + + )}
diff --git a/components/Forms/Register/schema.ts b/components/Forms/Signup/schema.ts similarity index 52% rename from components/Forms/Register/schema.ts rename to components/Forms/Signup/schema.ts index 982641d2c..6a8eecc22 100644 --- a/components/Forms/Register/schema.ts +++ b/components/Forms/Signup/schema.ts @@ -3,19 +3,14 @@ import { z } from "zod" import { passwordValidator } from "@/utils/passwordValidator" import { phoneValidator } from "@/utils/phoneValidator" -export const registerSchema = z.object({ - firstName: z - .string() - .max(250) - .refine((value) => value.trim().length > 0, { - message: "First name is required", - }), - lastName: z - .string() - .max(250) - .refine((value) => value.trim().length > 0, { - message: "Last name is required", - }), +const countryRequiredMsg = "Country is required" +export const signUpSchema = z.object({ + firstName: z.string().max(250).trim().min(1, { + message: "First name is required", + }), + lastName: z.string().max(250).trim().min(1, { + message: "Last name is required", + }), email: z.string().max(250).email(), phoneNumber: phoneValidator( "Phone is required", @@ -23,7 +18,12 @@ export const registerSchema = z.object({ ), dateOfBirth: z.string().min(1), address: z.object({ - countryCode: z.string(), + countryCode: z + .string({ + required_error: countryRequiredMsg, + invalid_type_error: countryRequiredMsg, + }) + .min(1, countryRequiredMsg), zipCode: z.string().min(1), }), password: passwordValidator("Password is required"), @@ -32,4 +32,4 @@ export const registerSchema = z.object({ }), }) -export type RegisterSchema = z.infer +export type SignUpSchema = z.infer diff --git a/components/GuestsRoomsPicker/ChildSelector/index.tsx b/components/GuestsRoomsPicker/ChildSelector/index.tsx index 5bf3a59d2..827bcd2e6 100644 --- a/components/GuestsRoomsPicker/ChildSelector/index.tsx +++ b/components/GuestsRoomsPicker/ChildSelector/index.tsx @@ -70,7 +70,7 @@ export default function ChildSelector({ roomIndex = 0 }: ChildSelectorProps) { roomIndex={roomIndex} index={index} child={child} - key={index} + key={"child_" + index} /> ))} diff --git a/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx index ee0e835b3..99c1324ab 100644 --- a/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx +++ b/components/GuestsRoomsPicker/GuestsRoomsPicker.tsx @@ -4,7 +4,7 @@ import { useIntl } from "react-intl" import { useGuestsRoomsStore } from "@/stores/guests-rooms" -import { CloseLargeIcon, PlusCircleIcon } from "../Icons" +import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" import Button from "../TempDesignSystem/Button" import Divider from "../TempDesignSystem/Divider" import Subtitle from "../TempDesignSystem/Text/Subtitle" @@ -65,28 +65,51 @@ export default function GuestsRoomsPicker({
))} +
+ + {rooms.length < 4 ? ( + + ) : null} + +
- - {rooms.length < 4 ? ( - - ) : null} - +
+ + {rooms.length < 4 ? ( + + ) : null} + +
- -
- - ) -} diff --git a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css b/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css deleted file mode 100644 index 5b79c3796..000000000 --- a/components/HotelReservation/BookingConfirmation/IntroSection/introSection.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x3); - width: 100%; -} - -.buttons { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--Spacing-x2); -} - -.button { - width: 100%; - max-width: 240px; - justify-content: center; -} - -@media screen and (min-width: 1367px) { - .buttons { - flex-direction: row; - justify-content: space-around; - } -} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx b/components/HotelReservation/BookingConfirmation/StaySection/index.tsx deleted file mode 100644 index 7907ac191..000000000 --- a/components/HotelReservation/BookingConfirmation/StaySection/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useIntl } from "react-intl" - -import { ArrowRightIcon, ScandicLogoIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./staySection.module.css" - -import { StaySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function StaySection({ hotel, stay }: StaySectionProps) { - const intl = useIntl() - - const nightsText = - stay.nights > 1 - ? intl.formatMessage({ id: "nights" }) - : intl.formatMessage({ id: "night" }) - - return ( - <> -
- -
-
- - - {hotel.name} - - - {hotel.address} - {hotel.phone} - -
- - {`${stay.nights} ${nightsText}`} - - {stay.start} - - {stay.end} - - -
-
-
-
- - {intl.formatMessage({ id: "Breakfast" })} - - - {`${intl.formatMessage({ id: "Weekdays" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} - {`${intl.formatMessage({ id: "Weekends" })} ${hotel.breakfast.start}-${hotel.breakfast.end}`} - -
-
- {intl.formatMessage({ id: "Check in" })} - - {intl.formatMessage({ id: "From" })} - {hotel.checkIn} - -
-
- - {intl.formatMessage({ id: "Check out" })} - - - {intl.formatMessage({ id: "At latest" })} - {hotel.checkOut} - -
-
- - ) -} diff --git a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css b/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css deleted file mode 100644 index 1eae5c732..000000000 --- a/components/HotelReservation/BookingConfirmation/StaySection/staySection.module.css +++ /dev/null @@ -1,78 +0,0 @@ -.card { - display: flex; - width: 100%; - background-color: var(--Base-Surface-Primary-light-Normal); - border: 1px solid var(--Base-Border-Subtle); - border-radius: var(--Corner-radius-Small); - overflow: hidden; -} - -.image { - height: 100%; - width: 105px; - object-fit: cover; -} - -.info { - display: flex; - flex-direction: column; - width: 100%; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2); -} - -.hotel, -.stay { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); -} - -.caption { - display: flex; - flex-direction: column; -} - -.dates { - display: flex; - align-items: center; - gap: var(--Spacing-x-half); -} - -.table { - display: flex; - justify-content: space-between; - padding: var(--Spacing-x2); - border-radius: var(--Corner-radius-Small); - background-color: var(--Base-Surface-Primary-dark-Normal); - width: 100%; -} - -.breakfast, -.checkIn, -.checkOut { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); -} - -@media screen and (min-width: 1367px) { - .card { - flex-direction: column; - } - .image { - width: 100%; - max-height: 195px; - } - - .info { - flex-direction: row; - justify-content: space-between; - } - - .hotel, - .stay { - width: 100%; - max-width: 230px; - } -} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx b/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx deleted file mode 100644 index 16eb84330..000000000 --- a/components/HotelReservation/BookingConfirmation/SummarySection/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useIntl } from "react-intl" - -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Title from "@/components/TempDesignSystem/Text/Title" - -import styles from "./summarySection.module.css" - -import { SummarySectionProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export default function SummarySection({ summary }: SummarySectionProps) { - const intl = useIntl() - const roomType = `${intl.formatMessage({ id: "Type of room" })}: ${summary.roomType}` - const bedType = `${intl.formatMessage({ id: "Type of bed" })}: ${summary.bedType}` - const breakfast = `${intl.formatMessage({ id: "Breakfast" })}: ${summary.breakfast}` - const flexibility = `${intl.formatMessage({ id: "Flexibility" })}: ${summary.flexibility}` - - return ( -
- - {intl.formatMessage({ id: "Summary" })} - - - {roomType} - 1648 SEK - - - {bedType} - 0 SEK - - - {breakfast} - 198 SEK - - - {flexibility} - 200 SEK - -
- ) -} diff --git a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css b/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css deleted file mode 100644 index b65d92e76..000000000 --- a/components/HotelReservation/BookingConfirmation/SummarySection/summarySection.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.section { - width: 100%; -} - -.summary { - display: flex; - justify-content: space-between; - border-bottom: 1px solid var(--Base-Border-Subtle); -} - -.summary span { - padding: var(--Spacing-x2) var(--Spacing-x0); -} diff --git a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts b/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts deleted file mode 100644 index 2dbf572e7..000000000 --- a/components/HotelReservation/BookingConfirmation/tempConfirmationData.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BookingConfirmation } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" - -export const tempConfirmationData: BookingConfirmation = { - email: "lisa.andersson@outlook.com", - hotel: { - name: "Helsinki Hub", - address: "Kaisaniemenkatu 7, Helsinki", - location: "Helsinki", - phone: "+358 300 870680", - image: - "https://test3.scandichotels.com/imagevault/publishedmedia/i11isd60bh119s9486b7/downtown-camper-by-scandic-lobby-reception-desk-ch.jpg?w=640", - checkIn: "15.00", - checkOut: "12.00", - breakfast: { start: "06:30", end: "10:00" }, - }, - stay: { - nights: 1, - start: "2024.03.09", - end: "2024.03.10", - }, - summary: { - roomType: "Standard Room", - bedType: "King size", - breakfast: "Yes", - flexibility: "Yes", - }, -} diff --git a/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx new file mode 100644 index 000000000..0ce1b1080 --- /dev/null +++ b/components/HotelReservation/EnterDetails/HistoryStateManager/index.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useCallback, useEffect } from "react" + +import { useEnterDetailsStore } from "@/stores/enter-details" + +export default function HistoryStateManager() { + const setCurrentStep = useEnterDetailsStore((state) => state.setCurrentStep) + const currentStep = useEnterDetailsStore((state) => state.currentStep) + + const handleBackButton = useCallback( + (event: PopStateEvent) => { + if (event.state.step) { + setCurrentStep(event.state.step) + } + }, + [setCurrentStep] + ) + + useEffect(() => { + window.addEventListener("popstate", handleBackButton) + + return () => { + window.removeEventListener("popstate", handleBackButton) + } + }, [handleBackButton]) + + useEffect(() => { + if (!window.history.state.step) { + window.history.replaceState( + { step: currentStep }, + "", + document.location.href + ) + } + }, [currentStep]) + + return null +} diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx new file mode 100644 index 000000000..c076a074b --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/PaymentOption/index.tsx @@ -0,0 +1,49 @@ +import Image from "next/image" +import { useFormContext } from "react-hook-form" + +import { PAYMENT_METHOD_ICONS, PaymentMethodEnum } from "@/constants/booking" + +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import { PaymentOptionProps } from "./paymentOption" + +import styles from "./paymentOption.module.css" + +export default function PaymentOption({ + name, + value, + label, + cardNumber, + registerOptions = {}, +}: PaymentOptionProps) { + const { register } = useFormContext() + + return ( + + ) +} diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css b/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css similarity index 100% rename from components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.module.css rename to components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.module.css diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts b/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts similarity index 65% rename from components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts rename to components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts index 5d77f6560..151a18860 100644 --- a/components/HotelReservation/SelectRate/Payment/PaymentOption/paymentOption.ts +++ b/components/HotelReservation/EnterDetails/Payment/PaymentOption/paymentOption.ts @@ -1,10 +1,10 @@ import { RegisterOptions } from "react-hook-form" -import { PaymentMethodEnum } from "@/constants/booking" - export interface PaymentOptionProps { name: string - value: PaymentMethodEnum + value: string label: string + cardNumber?: string registerOptions?: RegisterOptions + onChange?: () => void } diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx new file mode 100644 index 000000000..0da2b79e2 --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -0,0 +1,278 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter, useSearchParams } from "next/navigation" +import { useEffect, useMemo, useState } from "react" +import { Label as AriaLabel } from "react-aria-components" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { + BookingStatusEnum, + PAYMENT_METHOD_TITLES, + PaymentMethodEnum, +} from "@/constants/booking" +import { + bookingTermsAndConditions, + privacyPolicy, +} from "@/constants/currentWebHrefs" +import { env } from "@/env/client" +import { trpc } from "@/lib/trpc/client" +import { useEnterDetailsStore } from "@/stores/enter-details" + +import LoadingSpinner from "@/components/LoadingSpinner" +import Button from "@/components/TempDesignSystem/Button" +import Checkbox from "@/components/TempDesignSystem/Checkbox" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import { toast } from "@/components/TempDesignSystem/Toasts" +import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" +import useLang from "@/hooks/useLang" + +import PaymentOption from "./PaymentOption" +import { PaymentFormData, paymentSchema } from "./schema" + +import styles from "./payment.module.css" + +import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" + +const maxRetries = 40 +const retryInterval = 2000 + +function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { + return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum) +} + +export default function Payment({ + hotelId, + otherPaymentOptions, + savedCreditCards, +}: PaymentProps) { + const router = useRouter() + const lang = useLang() + const intl = useIntl() + const queryParams = useSearchParams() + const { firstName, lastName, email, phoneNumber, countryCode } = + useEnterDetailsStore((state) => state.data) + const [confirmationNumber, setConfirmationNumber] = useState("") + + const methods = useForm({ + defaultValues: { + paymentMethod: savedCreditCards?.length + ? savedCreditCards[0].id + : PaymentMethodEnum.card, + smsConfirmation: false, + termsAndConditions: false, + }, + mode: "all", + reValidateMode: "onChange", + resolver: zodResolver(paymentSchema), + }) + + const initiateBooking = trpc.booking.create.useMutation({ + onSuccess: (result) => { + if (result?.confirmationNumber) { + setConfirmationNumber(result.confirmationNumber) + } else { + // TODO: add proper error message + toast.error("Failed to create booking") + } + }, + onError: (error) => { + console.error("Error", error) + // TODO: add proper error message + toast.error("Failed to create booking") + }, + }) + + const bookingStatus = useHandleBookingStatus( + confirmationNumber, + BookingStatusEnum.PaymentRegistered, + maxRetries, + retryInterval + ) + + useEffect(() => { + if (bookingStatus?.data?.paymentUrl) { + router.push(bookingStatus.data.paymentUrl) + } + }, [bookingStatus, router]) + + function handleSubmit(data: PaymentFormData) { + const allQueryParams = + queryParams.size > 0 ? `?${queryParams.toString()}` : "" + + // set payment method to card if saved card is submitted + const paymentMethod = isPaymentMethodEnum(data.paymentMethod) + ? data.paymentMethod + : PaymentMethodEnum.card + + const savedCreditCard = savedCreditCards?.find( + (card) => card.id === data.paymentMethod + ) + + initiateBooking.mutate({ + hotelId: hotelId, + checkInDate: "2024-12-10", + checkOutDate: "2024-12-11", + rooms: [ + { + adults: 1, + childrenAges: [], + rateCode: "SAVEEU", + roomTypeCode: "QC", + guest: { + title: "Mr", // TODO: do we need title? + firstName, + lastName, + email, + phoneCountryCodePrefix: phoneNumber.slice(0, 3), + phoneNumber: phoneNumber.slice(3), + countryCode, + }, + packages: { + breakfast: true, + allergyFriendly: true, + petFriendly: true, + accessibility: true, + }, + smsConfirmationRequested: data.smsConfirmation, + }, + ], + payment: { + paymentMethod, + card: savedCreditCard + ? { + alias: savedCreditCard.alias, + expiryDate: savedCreditCard.expirationDate, + cardType: savedCreditCard.cardType, + } + : undefined, + cardHolder: { + email: "test.user@scandichotels.com", + name: "Test User", + phoneCountryCode: "", + phoneSubscriber: "", + }, + success: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/success`, + error: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/error${allQueryParams}`, + cancel: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/cancel${allQueryParams}`, + }, + }) + } + + if ( + initiateBooking.isPending || + (confirmationNumber && !bookingStatus.data?.paymentUrl) + ) { + return + } + + return ( + +
+ {savedCreditCards?.length ? ( +
+ + {intl.formatMessage({ id: "MY SAVED CARDS" })} + +
+ {savedCreditCards?.map((savedCreditCard) => ( + + ))} +
+
+ ) : null} +
+ {savedCreditCards?.length ? ( + + {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} + + ) : null} +
+ + {otherPaymentOptions.map((paymentMethod) => ( + + ))} +
+
+
+ + + {intl.formatMessage({ + id: "I would like to get my booking confirmation via sms", + })} + + + + + + + {intl.formatMessage( + { + id: "booking.terms", + }, + { + termsLink: (str) => ( + + {str} + + ), + privacyLink: (str) => ( + + {str} + + ), + } + )} + + +
+ +
+
+ ) +} diff --git a/components/HotelReservation/SelectRate/Payment/payment.module.css b/components/HotelReservation/EnterDetails/Payment/payment.module.css similarity index 80% rename from components/HotelReservation/SelectRate/Payment/payment.module.css rename to components/HotelReservation/EnterDetails/Payment/payment.module.css index d55cfd359..7f4f2899a 100644 --- a/components/HotelReservation/SelectRate/Payment/payment.module.css +++ b/components/HotelReservation/EnterDetails/Payment/payment.module.css @@ -1,10 +1,16 @@ .paymentContainer { display: flex; flex-direction: column; - gap: var(--Spacing-x3); + gap: var(--Spacing-x4); max-width: 480px; } +.section { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + .paymentOptionContainer { display: flex; flex-direction: column; diff --git a/components/HotelReservation/SelectRate/Payment/schema.ts b/components/HotelReservation/EnterDetails/Payment/schema.ts similarity index 74% rename from components/HotelReservation/SelectRate/Payment/schema.ts rename to components/HotelReservation/EnterDetails/Payment/schema.ts index ccade33c5..539406a8d 100644 --- a/components/HotelReservation/SelectRate/Payment/schema.ts +++ b/components/HotelReservation/EnterDetails/Payment/schema.ts @@ -1,9 +1,7 @@ import { z } from "zod" -import { PaymentMethodEnum } from "@/constants/booking" - export const paymentSchema = z.object({ - paymentMethod: z.nativeEnum(PaymentMethodEnum), + paymentMethod: z.string(), smsConfirmation: z.boolean(), termsAndConditions: z.boolean().refine((value) => value === true, { message: "You must accept the terms and conditions", diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx index 52aa11685..a508974c5 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx +++ b/components/HotelReservation/EnterDetails/SectionAccordion/index.tsx @@ -1,5 +1,5 @@ "use client" -import { useEffect, useRef, useState } from "react" +import { useEffect, useState } from "react" import { useIntl } from "react-intl" import { useEnterDetailsStore } from "@/stores/enter-details" diff --git a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css index 3680f5628..93398378d 100644 --- a/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css +++ b/components/HotelReservation/EnterDetails/SectionAccordion/sectionAccordion.module.css @@ -27,7 +27,6 @@ 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; } @@ -79,16 +78,3 @@ .content { overflow: hidden; } - -@keyframes allowOverflow { - 0% { - overflow: hidden; - } - 100% { - overflow: visible; - } -} - -.wrapper[data-open="true"] .content { - animation: allowOverflow 0.4s 0.4s ease; -} diff --git a/components/HotelReservation/EnterDetails/SidePeek/index.tsx b/components/HotelReservation/EnterDetails/SidePeek/index.tsx index abbad2dec..f7d280fed 100644 --- a/components/HotelReservation/EnterDetails/SidePeek/index.tsx +++ b/components/HotelReservation/EnterDetails/SidePeek/index.tsx @@ -33,8 +33,12 @@ export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
{hotel.hotelContent.texts.descriptions.medium} - - {hotel.hotelContent.texts.facilityInformation} + {hotel.hotelContent.texts.facilityInformation + .split(/[\n\r]/g) + .filter((p) => p) + .map((paragraph, idx) => ( + {paragraph} + ))}
diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 0ffa5b150..587feb29a 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -14,15 +14,16 @@ .imageContainer { grid-area: image; + position: relative; + height: 100%; + width: 116px; } .tripAdvisor { display: none; } -.image { - height: 100%; - width: 116px; +.imageContainer img { object-fit: cover; } @@ -77,6 +78,8 @@ .imageContainer { position: relative; + min-height: 200px; + width: 518px; } .tripAdvisor { @@ -86,10 +89,6 @@ top: 7px; } - .image { - width: 518px; - } - .hotelInformation { padding-top: var(--Spacing-x2); padding-right: var(--Spacing-x2); diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 99feb01ab..2bea7c895 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -11,10 +11,11 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" import ReadMore from "../ReadMore" +import ImageGallery from "../SelectRate/ImageGallery" import styles from "./hotelCard.module.css" -import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" +import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" export default async function HotelCard({ hotel }: HotelCardProps) { const intl = await getIntl() @@ -22,20 +23,20 @@ export default async function HotelCard({ hotel }: HotelCardProps) { const { hotelData } = hotel const { price } = hotel - const sortedAmenities = hotelData.detailedFacilities - .sort((a, b) => b.sortOrder - a.sortOrder) - .slice(0, 5) + const amenities = hotelData.detailedFacilities.slice(0, 5) return (
- {hotelData.hotelContent.images.metaData.altText} + {hotelData.gallery && ( + + )}
@@ -57,8 +58,8 @@ export default async function HotelCard({ hotel }: HotelCardProps) {
- {sortedAmenities.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.name) + {amenities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) return (
{IconComponent && } @@ -67,7 +68,11 @@ export default async function HotelCard({ hotel }: HotelCardProps) { ) })}
- +
@@ -100,7 +105,11 @@ export default async function HotelCard({ hotel }: HotelCardProps) { className={styles.button} > {/* TODO: Localize link and also use correct search params */} - + {intl.formatMessage({ id: "See rooms" })} diff --git a/components/HotelReservation/ReadMore/index.tsx b/components/HotelReservation/ReadMore/index.tsx index 5814eae54..c8d39fcbe 100644 --- a/components/HotelReservation/ReadMore/index.tsx +++ b/components/HotelReservation/ReadMore/index.tsx @@ -34,7 +34,7 @@ function getAmenitiesList(hotel: Hotel) { return [...detailedAmenities, ...simpleAmenities] } -export default function ReadMore({ hotel, hotelId }: ReadMoreProps) { +export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) { const intl = useIntl() const [sidePeekOpen, setSidePeekOpen] = useState(false) @@ -46,11 +46,12 @@ export default function ReadMore({ hotel, hotelId }: ReadMoreProps) { onPress={() => { setSidePeekOpen(true) }} - intent={"text"} - color="burgundy" + intent="text" + theme="base" + wrapping className={styles.detailsButton} > - {intl.formatMessage({ id: "See hotel details" })} + {label} b.sortOrder - a.sortOrder) + .slice(0, 5) + + return ( +
+ {hotelAttributes && ( +
+
+ {hotelAttributes.ratings?.tripAdvisor && ( +
+ + + {hotelAttributes.ratings.tripAdvisor.rating} + +
+ )} + {hotelAttributes.gallery && ( + + )} +
+
+
+ + {hotelAttributes.name} + + + {`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + + + {hotelAttributes.hotelContent.texts.descriptions.medium} + +
+ +
+
+ + {intl.formatMessage({ id: "At the hotel" })} + + {sortedFacilities?.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) + return ( +
+ {IconComponent && ( + + )} + + {facility.name} + +
+ ) + })} +
+ +
+
+
+ )} +
+ ) +} diff --git a/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css b/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css new file mode 100644 index 000000000..5d156f77a --- /dev/null +++ b/components/HotelReservation/SelectRate/ImageGallery/imageGallery.module.css @@ -0,0 +1,17 @@ +.galleryIcon { + position: absolute; + bottom: 16px; + right: 16px; + max-height: 32px; + width: 48px; + background-color: rgba(0, 0, 0, 0.6); + padding: var(--Spacing-x-quarter) var(--Spacing-x-half); + border-radius: var(--Corner-radius-Small); + display: flex; + align-items: center; + gap: var(--Spacing-x-quarter); +} + +.triggerArea { + cursor: pointer; +} diff --git a/components/HotelReservation/SelectRate/ImageGallery/index.tsx b/components/HotelReservation/SelectRate/ImageGallery/index.tsx new file mode 100644 index 000000000..778c2d45e --- /dev/null +++ b/components/HotelReservation/SelectRate/ImageGallery/index.tsx @@ -0,0 +1,36 @@ +import { GalleryIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Lightbox from "@/components/Lightbox" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" + +import styles from "./imageGallery.module.css" + +import type { ImageGalleryProps } from "@/types/components/hotelReservation/selectRate/imageGallery" + +export default function ImageGallery({ images, title }: ImageGalleryProps) { + return ( + ({ + url: image.imageSizes.small, + alt: image.metaData.altText, + title: image.metaData.title, + }))} + dialogTitle={title} + > +
+ {images[0].metaData.altText} +
+ + + {images.length} + +
+
+
+ ) +} diff --git a/components/HotelReservation/SelectRate/Payment/PaymentOption/index.tsx b/components/HotelReservation/SelectRate/Payment/PaymentOption/index.tsx deleted file mode 100644 index a44501ae4..000000000 --- a/components/HotelReservation/SelectRate/Payment/PaymentOption/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import Image from "next/image" -import { useFormContext } from "react-hook-form" - -import { PAYMENT_METHOD_ICONS } from "@/constants/booking" - -import Body from "@/components/TempDesignSystem/Text/Body" - -import { PaymentOptionProps } from "./paymentOption" - -import styles from "./paymentOption.module.css" - -export default function PaymentOption({ - name, - value, - label, -}: PaymentOptionProps) { - const { register } = useFormContext() - return ( - - ) -} diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/SelectRate/Payment/index.tsx deleted file mode 100644 index b1bddd84c..000000000 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ /dev/null @@ -1,217 +0,0 @@ -"use client" - -import { zodResolver } from "@hookform/resolvers/zod" -import { useRouter } from "next/navigation" -import { useEffect, useState } from "react" -import { Label as AriaLabel } from "react-aria-components" -import { FormProvider, useForm } from "react-hook-form" -import { useIntl } from "react-intl" - -import { - BOOKING_CONFIRMATION_NUMBER, - BookingStatusEnum, - PAYMENT_METHOD_TITLES, - PaymentMethodEnum, -} from "@/constants/booking" -import { - bookingTermsAndConditions, - privacyPolicy, -} from "@/constants/currentWebHrefs" -import { trpc } from "@/lib/trpc/client" - -import LoadingSpinner from "@/components/LoadingSpinner" -import Button from "@/components/TempDesignSystem/Button" -import Checkbox from "@/components/TempDesignSystem/Checkbox" -import Link from "@/components/TempDesignSystem/Link" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import { toast } from "@/components/TempDesignSystem/Toasts" -import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" -import useLang from "@/hooks/useLang" - -import PaymentOption from "./PaymentOption" -import { PaymentFormData, paymentSchema } from "./schema" - -import styles from "./payment.module.css" - -import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section" - -const maxRetries = 40 -const retryInterval = 2000 - -export default function Payment({ hotel }: PaymentProps) { - const router = useRouter() - const lang = useLang() - const intl = useIntl() - const [confirmationNumber, setConfirmationNumber] = useState("") - - const methods = useForm({ - defaultValues: { - paymentMethod: PaymentMethodEnum.card, - smsConfirmation: false, - termsAndConditions: false, - }, - mode: "all", - reValidateMode: "onChange", - resolver: zodResolver(paymentSchema), - }) - - const initiateBooking = trpc.booking.booking.create.useMutation({ - onSuccess: (result) => { - if (result?.confirmationNumber) { - setConfirmationNumber(result.confirmationNumber) - } else { - // TODO: add proper error message - toast.error("Failed to create booking") - } - }, - onError: (error) => { - console.error("Error", error) - // TODO: add proper error message - toast.error("Failed to create booking") - }, - }) - - const bookingStatus = useHandleBookingStatus( - confirmationNumber, - BookingStatusEnum.PaymentRegistered, - maxRetries, - retryInterval - ) - - useEffect(() => { - if (confirmationNumber && bookingStatus?.data?.paymentUrl) { - // Planet doesn't support query params so we have to store values in session storage - sessionStorage.setItem(BOOKING_CONFIRMATION_NUMBER, confirmationNumber) - router.push(bookingStatus.data.paymentUrl) - } - }, [confirmationNumber, bookingStatus, router]) - - function handleSubmit(data: PaymentFormData) { - initiateBooking.mutate({ - hotelId: hotel.operaId, - checkInDate: "2024-12-10", - checkOutDate: "2024-12-11", - rooms: [ - { - adults: 1, - childrenAges: [], - rateCode: "SAVEEU", - roomTypeCode: "QC", - guest: { - title: "Mr", - firstName: "Test", - lastName: "User", - email: "test.user@scandichotels.com", - phoneCountryCodePrefix: "string", - phoneNumber: "string", - countryCode: "string", - }, - packages: { - breakfast: true, - allergyFriendly: true, - petFriendly: true, - accessibility: true, - }, - smsConfirmationRequested: data.smsConfirmation, - }, - ], - payment: { - paymentMethod: data.paymentMethod, - cardHolder: { - email: "test.user@scandichotels.com", - name: "Test User", - phoneCountryCode: "", - phoneSubscriber: "", - }, - success: `api/web/payment-callback/${lang}/success`, - error: `api/web/payment-callback/${lang}/error`, - cancel: `api/web/payment-callback/${lang}/cancel`, - }, - }) - } - - if ( - initiateBooking.isPending || - (confirmationNumber && !bookingStatus.data?.paymentUrl) - ) { - return - } - - return ( - -
-
- - {hotel.merchantInformationData.alternatePaymentOptions.map( - (paymentMethod) => ( - - ) - )} -
- - - {intl.formatMessage({ - id: "I would like to get my booking confirmation via sms", - })} - - - - - - - {intl.formatMessage( - { - id: "booking.terms", - }, - { - termsLink: (str) => ( - - {str} - - ), - privacyLink: (str) => ( - - {str} - - ), - } - )} - - - -
-
- ) -} diff --git a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx index 3086a5c2b..a523305ae 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption/index.tsx @@ -18,6 +18,7 @@ export default function FlexibilityOption({ paymentTerm, priceInformation, roomType, + roomTypeCode, handleSelectRate, }: FlexibilityOptionProps) { const [rootDiv, setRootDiv] = useState(undefined) @@ -46,7 +47,8 @@ export default function FlexibilityOption({ function onChange() { const rate = { - roomType: roomType, + roomTypeCode, + roomType, priceName: name, public: publicPrice, member: memberPrice, diff --git a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx index 29d4b0740..b929bfe76 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RateSummary/index.tsx @@ -24,17 +24,15 @@ export default function RateSummary({
- <> - - {priceToShow?.localPrice.pricePerStay}{" "} - {priceToShow?.localPrice.currency} - - - {intl.formatMessage({ id: "Approx." })}{" "} - {priceToShow?.requestedPrice?.pricePerStay}{" "} - {priceToShow?.requestedPrice?.currency} - - + + {priceToShow?.localPrice.pricePerStay}{" "} + {priceToShow?.localPrice.currency} + + + {intl.formatMessage({ id: "Approx." })}{" "} + {priceToShow?.requestedPrice?.pricePerStay}{" "} + {priceToShow?.requestedPrice?.currency} +
@@ -150,26 +152,8 @@ export default function RoomCard({ )} {/*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. */} - {mainImage.metaData.altText} {images && ( - ({ - url: image.imageSizes.small, - alt: image.metaData.altText, - title: image.metaData.title, - }))} - dialogTitle={roomConfiguration.roomType} - > -
- - {images.length} -
-
+ )} )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css index 25add23b5..ef5d9b8fc 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/roomCard.module.css @@ -77,17 +77,3 @@ min-height: 185px; position: relative; } - -.galleryIcon { - position: absolute; - bottom: 16px; - right: 16px; - height: 24px; - background-color: rgba(64, 57, 55, 0.9); - padding: 0 var(--Spacing-x-half); - border-radius: var(--Corner-radius-Small); - cursor: pointer; - display: flex; - align-items: center; - gap: var(--Spacing-x-quarter); -} diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index 7d1c15d6d..c4c5e2e87 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -4,6 +4,7 @@ import { useState } from "react" import RateSummary from "./RateSummary" import RoomCard from "./RoomCard" +import getHotelReservationQueryParams from "./utils" import styles from "./roomSelection.module.css" @@ -19,12 +20,29 @@ export default function RoomSelection({ const router = useRouter() const searchParams = useSearchParams() + const isUserLoggedIn = !!user function handleSubmit(e: React.FormEvent) { e.preventDefault() + const searchParamsObject = getHotelReservationQueryParams(searchParams) + const queryParams = new URLSearchParams(searchParams) - queryParams.set("roomClass", e.currentTarget.roomClass?.value) - queryParams.set("flexibility", e.currentTarget.flexibility?.value) + + searchParamsObject.room.forEach((item, index) => { + if (rateSummary?.roomTypeCode) { + queryParams.set(`room[${index}].roomtype`, rateSummary.roomTypeCode) + } + if (rateSummary?.public?.rateCode) { + queryParams.set(`room[${index}].ratecode`, rateSummary.public.rateCode) + } + if (rateSummary?.member?.rateCode) { + queryParams.set( + `room[${index}].counterratecode`, + rateSummary.member.rateCode + ) + } + }) + router.push(`select-bed?${queryParams}`) } @@ -48,7 +66,10 @@ export default function RoomSelection({ ))} {rateSummary && ( - + )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts new file mode 100644 index 000000000..e47a0da70 --- /dev/null +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -0,0 +1,28 @@ +import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" + +function getHotelReservationQueryParams(searchParams: URLSearchParams) { + const searchParamsObject: Record = Array.from( + searchParams.entries() + ).reduce>( + (acc, [key, value]) => { + const keys = key.replace(/\]/g, "").split(/\[|\./) // Split keys by '[' or '.' + keys.reduce((nestedAcc, k, i) => { + if (i === keys.length - 1) { + // Convert value to number if the key is 'adults' or 'age' + ;(nestedAcc as Record)[k] = + k === "adults" || k === "age" ? Number(value) : value + } else { + if (!nestedAcc[k]) { + nestedAcc[k] = isNaN(Number(keys[i + 1])) ? {} : [] // Initialize as object or array + } + } + return nestedAcc[k] as Record + }, acc) + return acc + }, + {} as Record + ) + return searchParamsObject as SelectRateSearchParams +} + +export default getHotelReservationQueryParams diff --git a/components/Icons/Accesories.tsx b/components/Icons/Accesories.tsx new file mode 100644 index 000000000..9aaf0c894 --- /dev/null +++ b/components/Icons/Accesories.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AccesoriesIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Air.tsx b/components/Icons/Air.tsx new file mode 100644 index 000000000..239566515 --- /dev/null +++ b/components/Icons/Air.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function AirIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Coffee.tsx b/components/Icons/CoffeeAlt.tsx similarity index 96% rename from components/Icons/Coffee.tsx rename to components/Icons/CoffeeAlt.tsx index 840f78b0d..e76da5126 100644 --- a/components/Icons/Coffee.tsx +++ b/components/Icons/CoffeeAlt.tsx @@ -2,7 +2,11 @@ import { iconVariants } from "./variants" import type { IconProps } from "@/types/components/icon" -export default function CoffeeIcon({ className, color, ...props }: IconProps) { +export default function CoffeeAltIcon({ + className, + color, + ...props +}: IconProps) { const classNames = iconVariants({ className, color }) return ( + + + + + + + + ) +} diff --git a/components/Icons/Cool.tsx b/components/Icons/Cool.tsx new file mode 100644 index 000000000..efa5ae67f --- /dev/null +++ b/components/Icons/Cool.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function CoolIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/DoorOpen.tsx b/components/Icons/DoorOpen.tsx index f70c29859..93bc2caf4 100644 --- a/components/Icons/DoorOpen.tsx +++ b/components/Icons/DoorOpen.tsx @@ -2,7 +2,11 @@ import { iconVariants } from "./variants" import type { IconProps } from "@/types/components/icon" -export default function CoffeeIcon({ className, color, ...props }: IconProps) { +export default function DoorOpenIcon({ + className, + color, + ...props +}: IconProps) { const classNames = iconVariants({ className, color }) return ( + + + + + + + + ) +} diff --git a/components/Icons/Dresser.tsx b/components/Icons/Dresser.tsx new file mode 100644 index 000000000..b81f2bece --- /dev/null +++ b/components/Icons/Dresser.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function DresserIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/ElectricCar.tsx b/components/Icons/ElectricCar.tsx new file mode 100644 index 000000000..1f9500b64 --- /dev/null +++ b/components/Icons/ElectricCar.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ElectricCarIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Fan.tsx b/components/Icons/Fan.tsx new file mode 100644 index 000000000..128200104 --- /dev/null +++ b/components/Icons/Fan.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function FanIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Footstool.tsx b/components/Icons/Footstool.tsx new file mode 100644 index 000000000..becb0fd80 --- /dev/null +++ b/components/Icons/Footstool.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function FootstoolIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Garage.tsx b/components/Icons/Garage.tsx new file mode 100644 index 000000000..b715532cc --- /dev/null +++ b/components/Icons/Garage.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GarageIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Golf.tsx b/components/Icons/Golf.tsx new file mode 100644 index 000000000..027c906d7 --- /dev/null +++ b/components/Icons/Golf.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GolfIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Groceries.tsx b/components/Icons/Groceries.tsx new file mode 100644 index 000000000..404f09f7b --- /dev/null +++ b/components/Icons/Groceries.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function GroceriesIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Hanger.tsx b/components/Icons/Hanger.tsx new file mode 100644 index 000000000..63c869619 --- /dev/null +++ b/components/Icons/Hanger.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HangerIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/HangerAlt.tsx b/components/Icons/HangerAlt.tsx new file mode 100644 index 000000000..732656e60 --- /dev/null +++ b/components/Icons/HangerAlt.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HangerAltIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Heat.tsx b/components/Icons/Heat.tsx new file mode 100644 index 000000000..ef093e955 --- /dev/null +++ b/components/Icons/Heat.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function HeatIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Kayaking.tsx b/components/Icons/Kayaking.tsx new file mode 100644 index 000000000..1d4061a3a --- /dev/null +++ b/components/Icons/Kayaking.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function KayakingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Kettle.tsx b/components/Icons/Kettle.tsx new file mode 100644 index 000000000..28713719a --- /dev/null +++ b/components/Icons/Kettle.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function KettleIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Lamp.tsx b/components/Icons/Lamp.tsx new file mode 100644 index 000000000..bde8af3dd --- /dev/null +++ b/components/Icons/Lamp.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function LampIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/LaundryMachine.tsx b/components/Icons/LaundryMachine.tsx new file mode 100644 index 000000000..b4225197d --- /dev/null +++ b/components/Icons/LaundryMachine.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function LaundryMachineIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/LocalBar.tsx b/components/Icons/LocalBar.tsx new file mode 100644 index 000000000..7a5b3bb51 --- /dev/null +++ b/components/Icons/LocalBar.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function LocalBarIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Nature.tsx b/components/Icons/Nature.tsx new file mode 100644 index 000000000..4c48c377f --- /dev/null +++ b/components/Icons/Nature.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NatureIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Nightlife.tsx b/components/Icons/Nightlife.tsx new file mode 100644 index 000000000..093099cb9 --- /dev/null +++ b/components/Icons/Nightlife.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NightlifeIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/NoSmoking.tsx b/components/Icons/NoSmoking.tsx new file mode 100644 index 000000000..bdaa7d3f3 --- /dev/null +++ b/components/Icons/NoSmoking.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function NoSmokingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/OutdoorFurniture.tsx b/components/Icons/OutdoorFurniture.tsx new file mode 100644 index 000000000..3ddac9f38 --- /dev/null +++ b/components/Icons/OutdoorFurniture.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function OutdoorFurnitureIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Printer.tsx b/components/Icons/Printer.tsx new file mode 100644 index 000000000..d703940da --- /dev/null +++ b/components/Icons/Printer.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function PrinterIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/RoomService.tsx b/components/Icons/RoomService.tsx new file mode 100644 index 000000000..00aadd7d0 --- /dev/null +++ b/components/Icons/RoomService.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function RoomServiceIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Skateboarding.tsx b/components/Icons/Skateboarding.tsx new file mode 100644 index 000000000..6c0106ff3 --- /dev/null +++ b/components/Icons/Skateboarding.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SkateboardingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Smoking.tsx b/components/Icons/Smoking.tsx new file mode 100644 index 000000000..58c30abce --- /dev/null +++ b/components/Icons/Smoking.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SmokingIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Spa.tsx b/components/Icons/Spa.tsx new file mode 100644 index 000000000..f3141a4e4 --- /dev/null +++ b/components/Icons/Spa.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SpaIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Street.tsx b/components/Icons/Street.tsx new file mode 100644 index 000000000..414df197c --- /dev/null +++ b/components/Icons/Street.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function StreetIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Swim.tsx b/components/Icons/Swim.tsx new file mode 100644 index 000000000..abd2bd29a --- /dev/null +++ b/components/Icons/Swim.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function SwimIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Thermostat.tsx b/components/Icons/Thermostat.tsx new file mode 100644 index 000000000..2fd3ebe97 --- /dev/null +++ b/components/Icons/Thermostat.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function ThermostatIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/Tshirt.tsx b/components/Icons/Tshirt.tsx new file mode 100644 index 000000000..e6643725b --- /dev/null +++ b/components/Icons/Tshirt.tsx @@ -0,0 +1,36 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function TshirtIcon({ className, color, ...props }: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/TvCasting.tsx b/components/Icons/TvCasting.tsx new file mode 100644 index 000000000..ea6e7b90d --- /dev/null +++ b/components/Icons/TvCasting.tsx @@ -0,0 +1,40 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function TvCastingIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + + + + + ) +} diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts index 15752b50c..f5410421f 100644 --- a/components/Icons/get-icon-by-icon-name.ts +++ b/components/Icons/get-icon-by-icon-name.ts @@ -4,8 +4,10 @@ import FacebookIcon from "./Facebook" import InstagramIcon from "./Instagram" import TripAdvisorIcon from "./TripAdvisor" import { + AccesoriesIcon, AccessibilityIcon, AccountCircleIcon, + AirIcon, AirplaneIcon, ArrowRightIcon, BarIcon, @@ -22,27 +24,48 @@ import { ChevronRightSmallIcon, CloseIcon, CloseLargeIcon, - CoffeeIcon, + CoffeeAltIcon, ConciergeIcon, + ConvenienceStore24hIcon, + CoolIcon, CrossCircle, CulturalIcon, DoorOpenIcon, + DresserIcon, ElectricBikeIcon, + ElectricCarIcon, EmailIcon, EyeHideIcon, EyeShowIcon, + FanIcon, FitnessIcon, + FootstoolIcon, GalleryIcon, + GarageIcon, GiftIcon, GlobeIcon, + GolfIcon, + GroceriesIcon, + HangerAltIcon, + HangerIcon, + HeatIcon, HouseIcon, ImageIcon, InfoCircleIcon, + KayakingIcon, + KettleIcon, + LampIcon, + LaundryMachineIcon, + LocalBarIcon, LocationIcon, LockIcon, MapIcon, MinusIcon, MuseumIcon, + NatureIcon, + NightlifeIcon, + NoSmokingIcon, + OutdoorFurnitureIcon, ParkingIcon, People2Icon, PersonIcon, @@ -51,14 +74,23 @@ import { PlusCircleIcon, PlusIcon, RestaurantIcon, + RoomServiceIcon, SaunaIcon, SearchIcon, ServiceIcon, ShoppingIcon, + SkateboardingIcon, + SmokingIcon, SnowflakeIcon, + SpaIcon, StarFilledIcon, + StreetIcon, + SwimIcon, + ThermostatIcon, TrainIcon, + TshirtIcon, TshirtWashIcon, + TvCastingIcon, WarningTriangle, WifiIcon, } from "." @@ -67,10 +99,14 @@ import { IconName, IconProps } from "@/types/components/icon" export function getIconByIconName(icon?: IconName): FC | null { switch (icon) { + case IconName.Accesories: + return AccesoriesIcon case IconName.Accessibility: return AccessibilityIcon case IconName.AccountCircle: return AccountCircleIcon + case IconName.Air: + return AirIcon case IconName.Airplane: return AirplaneIcon case IconName.ArrowRight: @@ -105,32 +141,56 @@ export function getIconByIconName(icon?: IconName): FC | null { return CloseIcon case IconName.CloseLarge: return CloseLargeIcon - case IconName.Coffee: - return CoffeeIcon + case IconName.ConvenienceStore24h: + return ConvenienceStore24hIcon + case IconName.Cool: + return CoolIcon + case IconName.CoffeeAlt: + return CoffeeAltIcon case IconName.Concierge: return ConciergeIcon case IconName.Cultural: return CulturalIcon case IconName.DoorOpen: return DoorOpenIcon + case IconName.Dresser: + return DresserIcon case IconName.ElectricBike: return ElectricBikeIcon + case IconName.ElectricCar: + return ElectricCarIcon case IconName.Email: return EmailIcon case IconName.EyeHide: return EyeHideIcon case IconName.EyeShow: return EyeShowIcon + case IconName.Fan: + return FanIcon case IconName.Facebook: return FacebookIcon case IconName.Fitness: return FitnessIcon + case IconName.Footstool: + return FootstoolIcon case IconName.Gallery: return GalleryIcon + case IconName.Garage: + return GarageIcon case IconName.Gift: return GiftIcon case IconName.Globe: return GlobeIcon + case IconName.Golf: + return GolfIcon + case IconName.Groceries: + return GroceriesIcon + case IconName.Hanger: + return HangerIcon + case IconName.HangerAlt: + return HangerAltIcon + case IconName.Heat: + return HeatIcon case IconName.House: return HouseIcon case IconName.Image: @@ -139,6 +199,16 @@ export function getIconByIconName(icon?: IconName): FC | null { return InfoCircleIcon case IconName.Instagram: return InstagramIcon + case IconName.Kayaking: + return KayakingIcon + case IconName.Kettle: + return KettleIcon + case IconName.Lamp: + return LampIcon + case IconName.LaundryMachine: + return LaundryMachineIcon + case IconName.LocalBar: + return LocalBarIcon case IconName.Location: return LocationIcon case IconName.Lock: @@ -149,6 +219,14 @@ export function getIconByIconName(icon?: IconName): FC | null { return MinusIcon case IconName.Museum: return MuseumIcon + case IconName.Nature: + return NatureIcon + case IconName.Nightlife: + return NightlifeIcon + case IconName.NoSmoking: + return NoSmokingIcon + case IconName.OutdoorFurniture: + return OutdoorFurnitureIcon case IconName.Parking: return ParkingIcon case IconName.Person: @@ -165,6 +243,12 @@ export function getIconByIconName(icon?: IconName): FC | null { return PlusCircleIcon case IconName.Restaurant: return RestaurantIcon + case IconName.RoomService: + return RoomServiceIcon + case IconName.Smoking: + return SmokingIcon + case IconName.Spa: + return SpaIcon case IconName.Sauna: return SaunaIcon case IconName.Search: @@ -173,16 +257,28 @@ export function getIconByIconName(icon?: IconName): FC | null { return ServiceIcon case IconName.Shopping: return ShoppingIcon + case IconName.Skateboarding: + return SkateboardingIcon case IconName.Snowflake: return SnowflakeIcon case IconName.StarFilled: return StarFilledIcon + case IconName.Street: + return StreetIcon + case IconName.Swim: + return SwimIcon + case IconName.Thermostat: + return ThermostatIcon + case IconName.Tshirt: + return TshirtIcon case IconName.Train: return TrainIcon case IconName.Tripadvisor: return TripAdvisorIcon case IconName.TshirtWash: return TshirtWashIcon + case IconName.TvCasting: + return TvCastingIcon case IconName.WarningTriangle: return WarningTriangle case IconName.Wifi: diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index e8a0b6433..e7547b178 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -1,5 +1,7 @@ +export { default as AccesoriesIcon } from "./Accesories" export { default as AccessibilityIcon } from "./Accessibility" export { default as AccountCircleIcon } from "./AccountCircle" +export { default as AirIcon } from "./Air" export { default as AirplaneIcon } from "./Airplane" export { default as ArrowRightIcon } from "./ArrowRight" export { default as BarIcon } from "./Bar" @@ -17,34 +19,56 @@ export { default as ChevronRightIcon } from "./ChevronRight" export { default as ChevronRightSmallIcon } from "./ChevronRightSmall" export { default as CloseIcon } from "./Close" export { default as CloseLargeIcon } from "./CloseLarge" -export { default as CoffeeIcon } from "./Coffee" +export { default as CoffeeAltIcon } from "./CoffeeAlt" export { default as ConciergeIcon } from "./Concierge" +export { default as ConvenienceStore24hIcon } from "./ConvenienceStore24h" +export { default as CoolIcon } from "./Cool" export { default as CreditCard } from "./CreditCard" export { default as CrossCircle } from "./CrossCircle" export { default as CulturalIcon } from "./Cultural" export { default as DeleteIcon } from "./Delete" export { default as DoorOpenIcon } from "./DoorOpen" +export { default as DownloadIcon } from "./Download" +export { default as DresserIcon } from "./Dresser" export { default as EditIcon } from "./Edit" export { default as ElectricBikeIcon } from "./ElectricBike" +export { default as ElectricCarIcon } from "./ElectricCar" export { default as EmailIcon } from "./Email" export { default as ErrorCircleIcon } from "./ErrorCircle" export { default as EyeHideIcon } from "./EyeHide" export { default as EyeShowIcon } from "./EyeShow" +export { default as FanIcon } from "./Fan" export { default as FitnessIcon } from "./Fitness" +export { default as FootstoolIcon } from "./Footstool" export { default as GalleryIcon } from "./Gallery" +export { default as GarageIcon } from "./Garage" export { default as GiftIcon } from "./Gift" export { default as GlobeIcon } from "./Globe" +export { default as GolfIcon } from "./Golf" +export { default as GroceriesIcon } from "./Groceries" +export { default as HangerIcon } from "./Hanger" +export { default as HangerAltIcon } from "./HangerAlt" export { default as HeartIcon } from "./Heart" +export { default as HeatIcon } from "./Heat" export { default as HouseIcon } from "./House" export { default as ImageIcon } from "./Image" export { default as InfoCircleIcon } from "./InfoCircle" +export { default as KayakingIcon } from "./Kayaking" +export { default as KettleIcon } from "./Kettle" export { default as KingBedIcon } from "./KingBed" +export { default as LampIcon } from "./Lamp" +export { default as LaundryMachineIcon } from "./LaundryMachine" +export { default as LocalBarIcon } from "./LocalBar" export { default as LocationIcon } from "./Location" export { default as LockIcon } from "./Lock" export { default as MapIcon } from "./Map" export { default as MinusIcon } from "./Minus" export { default as MuseumIcon } from "./Museum" +export { default as NatureIcon } from "./Nature" +export { default as NightlifeIcon } from "./Nightlife" export { default as NoBreakfastIcon } from "./NoBreakfast" +export { default as NoSmokingIcon } from "./NoSmoking" +export { default as OutdoorFurnitureIcon } from "./OutdoorFurniture" export { default as ParkingIcon } from "./Parking" export { default as People2Icon } from "./People2" export { default as PersonIcon } from "./Person" @@ -53,15 +77,25 @@ export { default as PhoneIcon } from "./Phone" export { default as PlusIcon } from "./Plus" export { default as PlusCircleIcon } from "./PlusCircle" export { default as PriceTagIcon } from "./PriceTag" +export { default as PrinterIcon } from "./Printer" export { default as RestaurantIcon } from "./Restaurant" +export { default as RoomServiceIcon } from "./RoomService" export { default as SaunaIcon } from "./Sauna" export { default as ScandicLogoIcon } from "./ScandicLogo" export { default as SearchIcon } from "./Search" export { default as ServiceIcon } from "./Service" export { default as ShoppingIcon } from "./Shopping" +export { default as SkateboardingIcon } from "./Skateboarding" +export { default as SmokingIcon } from "./Smoking" export { default as SnowflakeIcon } from "./Snowflake" +export { default as SpaIcon } from "./Spa" export { default as StarFilledIcon } from "./StarFilled" +export { default as StreetIcon } from "./Street" +export { default as SwimIcon } from "./Swim" +export { default as ThermostatIcon } from "./Thermostat" export { default as TrainIcon } from "./Train" +export { default as TshirtIcon } from "./Tshirt" export { default as TshirtWashIcon } from "./TshirtWash" +export { default as TvCastingIcon } from "./TvCasting" export { default as WarningTriangle } from "./WarningTriangle" export { default as WifiIcon } from "./Wifi" diff --git a/components/Image.tsx b/components/Image.tsx index cf88c6e01..419259fc3 100644 --- a/components/Image.tsx +++ b/components/Image.tsx @@ -2,7 +2,10 @@ import NextImage from "next/image" -import type { ImageLoaderProps, ImageProps } from "next/image" +import type { ImageLoaderProps } from "next/image" +import type { CSSProperties } from "react" + +import type { ImageProps } from "@/types/components/image" function imageLoader({ quality, src, width }: ImageLoaderProps) { const hasQS = src.indexOf("?") !== -1 @@ -10,6 +13,14 @@ function imageLoader({ quality, src, width }: ImageLoaderProps) { } // Next/Image adds & instead of ? before the params -export default function Image(props: ImageProps) { - return +export default function Image({ focalPoint, style, ...props }: ImageProps) { + const styles: CSSProperties = focalPoint + ? { + objectFit: "cover", + objectPosition: `${focalPoint.x}% ${focalPoint.y}%`, + ...style, + } + : { ...style } + + return } diff --git a/components/ImageContainer/index.tsx b/components/ImageContainer/index.tsx index c7e23d621..390aaa81b 100644 --- a/components/ImageContainer/index.tsx +++ b/components/ImageContainer/index.tsx @@ -18,6 +18,7 @@ export default function ImageContainer({ height={365} width={600} alt={leftImage.meta.alt || leftImage.title} + focalPoint={leftImage.focalPoint} /> {leftImage.meta.caption}
@@ -28,6 +29,7 @@ export default function ImageContainer({ height={365} width={600} alt={rightImage.meta.alt || rightImage.title} + focalPoint={rightImage.focalPoint} /> {leftImage.meta.caption} diff --git a/components/JsonToHtml/renderOptions.tsx b/components/JsonToHtml/renderOptions.tsx index ce7a325b5..219981529 100644 --- a/components/JsonToHtml/renderOptions.tsx +++ b/components/JsonToHtml/renderOptions.tsx @@ -396,6 +396,7 @@ export const renderOptions: RenderOptions = { height={365} src={image.url} width={width} + focalPoint={image.focalPoint} {...props} /> {image.meta.caption} diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx index 3e0f59637..02e317e54 100644 --- a/components/Sidebar/index.tsx +++ b/components/Sidebar/index.tsx @@ -1,5 +1,8 @@ import JsonToHtml from "@/components/JsonToHtml" +import ShortcutsList from "../Blocks/ShortcutsList" +import Card from "../TempDesignSystem/Card" +import TeaserCard from "../TempDesignSystem/TeaserCard" import JoinLoyaltyContact from "./JoinLoyalty" import MyPagesNavigation from "./MyPagesNavigation" @@ -40,6 +43,35 @@ export default function Sidebar({ blocks }: SidebarProps) { key={`${block.typename}-${idx}`} /> ) + case SidebarEnums.blocks.ScriptedCard: + return ( + + ) + case SidebarEnums.blocks.TeaserCard: + return ( + + ) + case SidebarEnums.blocks.QuickLinks: + return + default: return null } diff --git a/components/Sidebar/sidebar.module.css b/components/Sidebar/sidebar.module.css index 5ae1ce5d4..f30a1ea39 100644 --- a/components/Sidebar/sidebar.module.css +++ b/components/Sidebar/sidebar.module.css @@ -2,6 +2,10 @@ display: grid; container-name: sidebar; container-type: inline-size; + gap: var(--Spacing-x3); + + border-top: 1px solid var(--Base-Border-Subtle); + padding-top: var(--Spacing-x4); } .content { @@ -11,8 +15,10 @@ @media screen and (min-width: 1367px) { .aside { align-content: flex-start; - display: grid; gap: var(--Spacing-x4); + + border-top: 0; + padding-top: 0; } } diff --git a/components/SitewideAlert/index.tsx b/components/SitewideAlert/index.tsx new file mode 100644 index 000000000..2ce624119 --- /dev/null +++ b/components/SitewideAlert/index.tsx @@ -0,0 +1,33 @@ +import { getSiteConfig } from "@/lib/trpc/memoizedRequests" + +import Alert from "../TempDesignSystem/Alert" + +import styles from "./sitewideAlert.module.css" + +export function preload() { + void getSiteConfig() +} + +export default async function SitewideAlert() { + const siteConfig = await getSiteConfig() + + if (!siteConfig?.sitewideAlert) { + return null + } + + const { sitewideAlert } = siteConfig + return ( +
+ +
+ ) +} diff --git a/components/SitewideAlert/sitewideAlert.module.css b/components/SitewideAlert/sitewideAlert.module.css new file mode 100644 index 000000000..f91bf3db5 --- /dev/null +++ b/components/SitewideAlert/sitewideAlert.module.css @@ -0,0 +1,9 @@ +.sitewideAlert { + width: 100%; +} + +.alarm { + position: sticky; + top: 0; + z-index: calc(var(--header-z-index) + 1); +} diff --git a/components/TempDesignSystem/Alert/Sidepeek/index.tsx b/components/TempDesignSystem/Alert/Sidepeek/index.tsx new file mode 100644 index 000000000..db69cf563 --- /dev/null +++ b/components/TempDesignSystem/Alert/Sidepeek/index.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useState } from "react" + +import { ChevronRightIcon } from "@/components/Icons" +import JsonToHtml from "@/components/JsonToHtml" +import Button from "@/components/TempDesignSystem/Button" + +import SidePeek from "../../SidePeek" + +import styles from "./sidepeek.module.css" + +import type { AlertSidepeekProps } from "./sidepeek" + +export default function AlertSidepeek({ + ctaText, + sidePeekContent, +}: AlertSidepeekProps) { + const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false) + const { heading, content } = sidePeekContent + + return ( +
+ + {sidePeekIsOpen ? ( + setSidePeekIsOpen(false)} + > + + + ) : null} +
+ ) +} diff --git a/components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css new file mode 100644 index 000000000..fccd935aa --- /dev/null +++ b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.module.css @@ -0,0 +1,3 @@ +.alertSidepeek { + flex-shrink: 0; +} diff --git a/components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts new file mode 100644 index 000000000..acd7ce35a --- /dev/null +++ b/components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts @@ -0,0 +1,6 @@ +import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig" + +export interface AlertSidepeekProps { + ctaText: string + sidePeekContent: NonNullable +} diff --git a/components/TempDesignSystem/Alert/alert.module.css b/components/TempDesignSystem/Alert/alert.module.css new file mode 100644 index 000000000..728f7d22d --- /dev/null +++ b/components/TempDesignSystem/Alert/alert.module.css @@ -0,0 +1,101 @@ +.alert { + overflow: hidden; +} + +.iconWrapper { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.content { + width: 100%; + max-width: var(--max-width-navigation); + margin: 0 auto; + display: flex; + gap: var(--Spacing-x2); +} + +.innerContent { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2) 0; + flex-grow: 1; +} + +.textWrapper { + display: grid; + gap: var(--Spacing-x-half); +} + +/* Intent: inline */ +.inline { + border-radius: var(--Corner-radius-Large); + border: 1px solid var(--Base-Border-Subtle); + background-color: var(--Base-Surface-Primary-light-Normal); +} +.inline .innerContent { + padding-right: var(--Spacing-x3); +} +.inline .iconWrapper { + padding: var(--Spacing-x-one-and-half); +} +.inline.alarm .iconWrapper { + background-color: var(--UI-Semantic-Error); +} +.inline.warning .iconWrapper { + background-color: var(--UI-Semantic-Warning); +} +.inline.info .iconWrapper { + background-color: var(--UI-Semantic-Information); +} +.inline .icon, +.inline .icon * { + fill: var(--Base-Surface-Primary-light-Normal); +} + +/* Intent: banner */ +.banner { + padding: 0 var(--Spacing-x3); + border-left-width: 6px; + border-left-style: solid; +} +.banner.alarm { + border-left-color: var(--UI-Semantic-Error); + background-color: var(--Scandic-Red-00); +} +.banner.warning { + border-left-color: var(--UI-Semantic-Warning); + background-color: var(--Scandic-Yellow-00); +} +.banner.info { + border-left-color: var(--UI-Semantic-Information); + background-color: var(--Scandic-Blue-00); +} +.banner.alarm .icon, +.banner.alarm .icon * { + fill: var(--UI-Semantic-Error); +} +.banner.warning .icon, +.banner.warning .icon * { + fill: var(--UI-Semantic-Warning); +} +.banner.info .icon, +.banner.info .icon * { + fill: var(--UI-Semantic-Information); +} + +@media screen and (min-width: 768px) { + .banner { + padding: 0 var(--Spacing-x5); + } + .innerContent { + flex-direction: row; + align-items: center; + gap: var(--Spacing-x2); + } +} diff --git a/components/TempDesignSystem/Alert/alert.ts b/components/TempDesignSystem/Alert/alert.ts new file mode 100644 index 000000000..e63873c66 --- /dev/null +++ b/components/TempDesignSystem/Alert/alert.ts @@ -0,0 +1,24 @@ +import { alertVariants } from "./variants" + +import type { VariantProps } from "class-variance-authority" + +import { AlertTypeEnum } from "@/types/enums/alert" +import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig" + +export interface AlertProps extends VariantProps { + className?: string + type: AlertTypeEnum + heading?: string | null + text?: string | null + phoneContact?: { + displayText: string + phoneNumber?: string + footnote?: string | null + } | null + sidepeekContent?: SidepeekContent | null + sidepeekCtaText?: string | null + link?: { + url: string + title: string + } | null +} diff --git a/components/TempDesignSystem/Alert/index.tsx b/components/TempDesignSystem/Alert/index.tsx new file mode 100644 index 000000000..2f1e97b6a --- /dev/null +++ b/components/TempDesignSystem/Alert/index.tsx @@ -0,0 +1,80 @@ +import Body from "@/components/TempDesignSystem/Text/Body" + +import Link from "../Link" +import AlertSidepeek from "./Sidepeek" +import { getIconByAlertType } from "./utils" +import { alertVariants } from "./variants" + +import styles from "./alert.module.css" + +import type { AlertProps } from "./alert" + +export default function Alert({ + className, + variant, + type, + heading, + text, + link, + phoneContact, + sidepeekCtaText, + sidepeekContent, +}: AlertProps) { + const classNames = alertVariants({ + className, + variant, + type, + }) + const Icon = getIconByAlertType(type) + + if (!text && !heading) { + return null + } + + return ( +
+
+ + + +
+
+ {heading ? ( + +

{heading}

+ + ) : null} + + {text} + {phoneContact?.phoneNumber ? ( + <> + {phoneContact.displayText} + + {phoneContact.phoneNumber} + + {phoneContact.footnote ? ( + . ({phoneContact.footnote}) + ) : null} + + ) : null} + +
+ {link ? ( + + {link.title} + + ) : null} + {!link && sidepeekCtaText && sidepeekContent ? ( + + ) : null} +
+
+
+ ) +} diff --git a/components/TempDesignSystem/Alert/utils.ts b/components/TempDesignSystem/Alert/utils.ts new file mode 100644 index 000000000..06fc9bcfd --- /dev/null +++ b/components/TempDesignSystem/Alert/utils.ts @@ -0,0 +1,17 @@ +import { InfoCircleIcon } from "@/components/Icons" +import CrossCircleIcon from "@/components/Icons/CrossCircle" +import WarningTriangleIcon from "@/components/Icons/WarningTriangle" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export function getIconByAlertType(alertType: AlertTypeEnum) { + switch (alertType) { + case AlertTypeEnum.Alarm: + return CrossCircleIcon + case AlertTypeEnum.Warning: + return WarningTriangleIcon + case AlertTypeEnum.Info: + default: + return InfoCircleIcon + } +} diff --git a/components/TempDesignSystem/Alert/variants.ts b/components/TempDesignSystem/Alert/variants.ts new file mode 100644 index 000000000..7397ac1e9 --- /dev/null +++ b/components/TempDesignSystem/Alert/variants.ts @@ -0,0 +1,23 @@ +import { cva } from "class-variance-authority" + +import styles from "./alert.module.css" + +import { AlertTypeEnum } from "@/types/enums/alert" + +export const alertVariants = cva(styles.alert, { + variants: { + variant: { + inline: styles.inline, + banner: styles.banner, + }, + type: { + [AlertTypeEnum.Info]: styles.info, + [AlertTypeEnum.Warning]: styles.warning, + [AlertTypeEnum.Alarm]: styles.alarm, + }, + }, + defaultVariants: { + variant: "inline", + type: AlertTypeEnum.Info, + }, +}) diff --git a/components/TempDesignSystem/Card/CardImage/index.tsx b/components/TempDesignSystem/Card/CardImage/index.tsx index 9c3233e87..bcdbd265a 100644 --- a/components/TempDesignSystem/Card/CardImage/index.tsx +++ b/components/TempDesignSystem/Card/CardImage/index.tsx @@ -24,6 +24,7 @@ export default function CardImage({ alt={backgroundImage.title} width={180} height={180} + focalPoint={backgroundImage.focalPoint} /> ) )} diff --git a/components/TempDesignSystem/Card/index.tsx b/components/TempDesignSystem/Card/index.tsx index 547ab5bea..4a2ab61a9 100644 --- a/components/TempDesignSystem/Card/index.tsx +++ b/components/TempDesignSystem/Card/index.tsx @@ -34,7 +34,7 @@ export default function Card({ imageWidth = imageWidth || - (backgroundImage && "dimensions" in backgroundImage + (backgroundImage?.dimensions ? backgroundImage.dimensions.aspectRatio * imageHeight : 420) @@ -53,6 +53,7 @@ export default function Card({ alt={backgroundImage.meta.alt || backgroundImage.title} width={imageWidth} height={imageHeight} + focalPoint={backgroundImage.focalPoint} /> )} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css index 0afc51797..99077e212 100644 --- a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -21,7 +21,6 @@ } .checkbox { - flex-grow: 1; width: 24px; height: 24px; min-width: 24px; diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index 501d43449..c57d56cc4 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -28,7 +28,7 @@ export default function Card({ const { register } = useFormContext() return ( -