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/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 532228434..2d80356ac 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -17,11 +17,11 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) - const selecetRoomParams = new URLSearchParams(searchParams) - const selecetRoomParamsObject = - getHotelReservationQueryParams(selecetRoomParams) - const adults = selecetRoomParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selecetRoomParamsObject.room[0].child.length // TODO: Handle multiple rooms + const 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({ @@ -33,8 +33,8 @@ export default async function SelectRatePage({ hotelId: parseInt(searchParams.hotel, 10), roomStayStartDate: searchParams.fromDate, roomStayEndDate: searchParams.toDate, - adults: adults, - children: children, + adults, + children, }), getProfileSafely(), ]) 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/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/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 771c4e76e..091444fe2 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -29,6 +29,11 @@ display: none; } +.overview { + display: grid; + gap: var(--Spacing-x3); +} + .introContainer { display: flex; flex-wrap: wrap; @@ -37,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: @@ -76,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} - - + +
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/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 67ce49f17..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() @@ -27,13 +28,15 @@ export default async function HotelCard({ hotel }: HotelCardProps) { return (
- {hotelData.hotelContent.images.metaData.altText} + {hotelData.gallery && ( + + )}
@@ -102,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/SelectRate/HotelInfoCard/hotelInfoCard.module.css b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css index 1482bf4c9..aacdf0449 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css +++ b/components/HotelReservation/SelectRate/HotelInfoCard/hotelInfoCard.module.css @@ -12,10 +12,6 @@ gap: var(--Spacing-x2); } -.image { - border-radius: var(--Corner-radius-Medium); -} - .imageWrapper { position: relative; overflow: hidden; @@ -24,6 +20,10 @@ width: 100%; } +.imageWrapper img { + border-radius: var(--Corner-radius-Medium); +} + .tripAdvisor { display: flex; align-items: center; diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 1ec58dd07..120c0385d 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -10,6 +10,7 @@ import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" import ReadMore from "../../ReadMore" +import ImageGallery from "../ImageGallery" import styles from "./hotelInfoCard.module.css" @@ -28,12 +29,6 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) { {hotelAttributes && (
- {hotelAttributes.hotelContent.images.metaData.altText} {hotelAttributes.ratings?.tripAdvisor && (
@@ -42,7 +37,15 @@ export default function HotelInfoCard({ hotelData }: HotelInfoCardProps) {
)} - {/* TODO: gallery icon and image carousel */} + {hotelAttributes.gallery && ( + + )}
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/RoomSelection/RoomCard/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx index 87b0143b5..444968ce1 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/RoomCard/index.tsx @@ -5,18 +5,18 @@ import { useIntl } from "react-intl" import { RateDefinition } from "@/server/routers/hotels/output" import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption" -import { ChevronRightSmallIcon, GalleryIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Lightbox from "@/components/Lightbox" +import { ChevronRightSmallIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import ImageGallery from "../../ImageGallery" + import styles from "./roomCard.module.css" -import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" +import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" export default function RoomCard({ rateDefinitions, @@ -25,7 +25,6 @@ export default function RoomCard({ handleSelectRate, }: RoomCardProps) { const intl = useIntl() - const saveRate = rateDefinitions.find( // TODO: Update string when API has decided (rate) => rate.cancellationRule === "NonCancellable" @@ -153,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/TempDesignSystem/Alert/alert.ts b/components/TempDesignSystem/Alert/alert.ts index 20927d7fc..e63873c66 100644 --- a/components/TempDesignSystem/Alert/alert.ts +++ b/components/TempDesignSystem/Alert/alert.ts @@ -8,8 +8,8 @@ import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConf export interface AlertProps extends VariantProps { className?: string type: AlertTypeEnum - heading?: string - text: string + heading?: string | null + text?: string | null phoneContact?: { displayText: string phoneNumber?: string diff --git a/components/TempDesignSystem/Alert/index.tsx b/components/TempDesignSystem/Alert/index.tsx index 6499c7126..2f1e97b6a 100644 --- a/components/TempDesignSystem/Alert/index.tsx +++ b/components/TempDesignSystem/Alert/index.tsx @@ -27,6 +27,10 @@ export default function Alert({ }) const Icon = getIconByAlertType(type) + if (!text && !heading) { + return null + } + return (
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/Phone/phone.module.css b/components/TempDesignSystem/Form/Phone/phone.module.css index bda78a1af..f2b75136b 100644 --- a/components/TempDesignSystem/Form/Phone/phone.module.css +++ b/components/TempDesignSystem/Form/Phone/phone.module.css @@ -1,7 +1,7 @@ .phone { display: grid; gap: var(--Spacing-x2); - grid-template-columns: max(164px) 1fr; + grid-template-columns: minmax(124px, 164px) 1fr; --react-international-phone-background-color: var(--Main-Grey-White); --react-international-phone-border-color: var(--Scandic-Beige-40); diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index 716221b5b..2c46c8b6f 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -51,9 +51,10 @@ export default function Link({ const router = useRouter() const fullUrl = useMemo(() => { - const search = - keepSearchParams && searchParams.size ? `?${searchParams}` : "" - return `${href}${search}` + if (!keepSearchParams || !searchParams.size) return href + + const delimiter = href.includes("?") ? "&" : "?" + return `${href}${delimiter}${searchParams}` }, [href, searchParams, keepSearchParams]) // TODO: Remove this check (and hook) and only return when current web is deleted diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 89e286185..0f545d673 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,11 +1,13 @@ import { z } from "zod" +import { dt } from "@/lib/dt" import { toLang } from "@/server/utils" import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image" import { roomSchema } from "./schemas/room" import { getPoiGroupByCategoryName } from "./utils" +import { AlertTypeEnum } from "@/types/enums/alert" import { FacilityEnum } from "@/types/enums/facilities" import { PointOfInterestCategoryNameEnum } from "@/types/hotel" @@ -160,6 +162,21 @@ export const facilitySchema = z.object({ ), }) +export const gallerySchema = z.object({ + heroImages: z.array( + z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, + }) + ), + smallerImages: z.array( + z.object({ + metaData: imageMetaDataSchema, + imageSizes: imageSizesSchema, + }) + ), +}) + const healthFacilitySchema = z.object({ type: z.string(), content: z.object({ @@ -321,6 +338,7 @@ const socialMediaSchema = z.object({ const metaSpecialAlertSchema = z.object({ type: z.string(), + title: z.string().optional(), description: z.string().optional(), displayInBookingFlow: z.boolean(), startDate: z.string(), @@ -328,7 +346,23 @@ const metaSpecialAlertSchema = z.object({ }) const metaSchema = z.object({ - specialAlerts: z.array(metaSpecialAlertSchema), + specialAlerts: z + .array(metaSpecialAlertSchema) + .transform((data) => { + const now = dt().utc().format("YYYY-MM-DD") + const filteredAlerts = data.filter((alert) => { + const shouldShowNow = alert.startDate <= now && alert.endDate >= now + const hasText = alert.description || alert.title + return shouldShowNow && hasText + }) + return filteredAlerts.map((alert, idx) => ({ + id: `alert-${alert.type}-${idx}`, + type: AlertTypeEnum.Info, + heading: alert.title || null, + text: alert.description || null, + })) + }) + .default([]), }) const relationshipsSchema = z.object({ @@ -422,6 +456,7 @@ export const getHotelDataSchema = z.object({ conferencesAndMeetings: facilitySchema.optional(), healthAndWellness: facilitySchema.optional(), restaurantImages: facilitySchema.optional(), + gallery: gallerySchema.optional(), }), relationships: relationshipsSchema, }), diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 2fa4cfbe8..b5b5ff34b 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -219,6 +219,7 @@ export const hotelQueryRouter = router({ const hotelAttributes = validatedHotelData.data.data.attributes const images = extractHotelImages(hotelAttributes) + const hotelAlerts = hotelAttributes.meta?.specialAlerts || [] const roomCategories = included ? included.filter((item) => item.type === "roomcategories") @@ -262,6 +263,7 @@ export const hotelQueryRouter = router({ roomCategories, activitiesCard: activities?.upcoming_activities_card, facilities, + alerts: hotelAlerts, faq: contentstackData?.faq, } }), diff --git a/types/components/form/registerForm.ts b/types/components/form/signupForm.ts similarity index 69% rename from types/components/form/registerForm.ts rename to types/components/form/signupForm.ts index 41e783dd5..060c8d860 100644 --- a/types/components/form/registerForm.ts +++ b/types/components/form/signupForm.ts @@ -1,4 +1,4 @@ -export type RegisterFormProps = { +export type SignUpFormProps = { link?: { href: string; text: string } subtitle?: string title: string diff --git a/types/components/hotelReservation/selectRate/imageGallery.ts b/types/components/hotelReservation/selectRate/imageGallery.ts new file mode 100644 index 000000000..333ff2d94 --- /dev/null +++ b/types/components/hotelReservation/selectRate/imageGallery.ts @@ -0,0 +1,3 @@ +import type { GalleryImages } from "@/types/hotel" + +export type ImageGalleryProps = { images: GalleryImages; title: string } diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index 58aa6fbbe..16b3f5dcf 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -7,9 +7,9 @@ interface Child { interface Room { adults: number - roomtypecode: string - ratecode: string - child: Child[] + roomcode?: string + ratecode?: string + child?: Child[] } export interface SelectRateSearchParams { diff --git a/types/hotel.ts b/types/hotel.ts index a60204720..972c6459e 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -2,6 +2,7 @@ import { z } from "zod" import { facilitySchema, + gallerySchema, getHotelDataSchema, parkingSchema, pointOfInterestSchema, @@ -13,7 +14,6 @@ export type HotelData = z.infer export type Hotel = HotelData["data"]["attributes"] export type HotelAddress = HotelData["data"]["attributes"]["address"] export type HotelLocation = HotelData["data"]["attributes"]["location"] - export type Amenities = HotelData["data"]["attributes"]["detailedFacilities"] type HotelRatings = HotelData["data"]["attributes"]["ratings"] @@ -22,6 +22,8 @@ export type HotelTripAdvisor = | undefined export type RoomData = z.infer +export type GallerySchema = z.infer +export type GalleryImages = GallerySchema["heroImages"] export type PointOfInterest = z.output