feat(sw-453): implemented filter from packages
This commit is contained in:
@@ -1,24 +0,0 @@
|
|||||||
.page {
|
|
||||||
min-height: 100dvh;
|
|
||||||
padding-top: var(--Spacing-x6);
|
|
||||||
padding-left: var(--Spacing-x2);
|
|
||||||
padding-right: var(--Spacing-x2);
|
|
||||||
background-color: var(--Scandic-Brand-Warm-White);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
max-width: var(--max-width);
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x7);
|
|
||||||
padding: var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
max-width: 340px;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages"
|
||||||
|
|
||||||
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||||
import RoomFilter from "@/components/HotelReservation/SelectRate/RoomFilter"
|
import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
|
||||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
|
||||||
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
|
||||||
|
|
||||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
@@ -24,7 +22,7 @@ export default async function SelectRatePage({
|
|||||||
const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms
|
const adults = selectRoomParamsObject.room?.[0].adults // TODO: Handle multiple rooms
|
||||||
const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms
|
const children = selectRoomParamsObject.room?.[0].child?.length // TODO: Handle multiple rooms
|
||||||
|
|
||||||
const [hotelData, roomsAvailability, user] = await Promise.all([
|
const [hotelData, roomsAvailability, packages, user] = await Promise.all([
|
||||||
serverClient().hotel.hotelData.get({
|
serverClient().hotel.hotelData.get({
|
||||||
hotelId: searchParams.hotel,
|
hotelId: searchParams.hotel,
|
||||||
language: params.lang,
|
language: params.lang,
|
||||||
@@ -37,6 +35,18 @@ export default async function SelectRatePage({
|
|||||||
adults,
|
adults,
|
||||||
children,
|
children,
|
||||||
}),
|
}),
|
||||||
|
serverClient().hotel.packages.get({
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
startDate: searchParams.fromDate,
|
||||||
|
endDate: searchParams.toDate,
|
||||||
|
adults: adults,
|
||||||
|
children: children,
|
||||||
|
packageCodes: [
|
||||||
|
RoomPackageCode.ACCE,
|
||||||
|
RoomPackageCode.PETR,
|
||||||
|
RoomPackageCode.ALLG,
|
||||||
|
],
|
||||||
|
}),
|
||||||
getProfileSafely(),
|
getProfileSafely(),
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -51,20 +61,14 @@ export default async function SelectRatePage({
|
|||||||
const roomCategories = hotelData?.included
|
const roomCategories = hotelData?.included
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<HotelInfoCard hotelData={hotelData} />
|
<HotelInfoCard hotelData={hotelData} />
|
||||||
<div className={styles.content}>
|
<Rooms
|
||||||
<div className={styles.main}>
|
roomsAvailability={roomsAvailability}
|
||||||
<RoomFilter
|
roomCategories={roomCategories ?? []}
|
||||||
numberOfRooms={roomsAvailability.roomConfigurations.length}
|
user={user}
|
||||||
/>
|
packages={packages ?? []}
|
||||||
<RoomSelection
|
/>
|
||||||
roomsAvailability={roomsAvailability}
|
</>
|
||||||
roomCategories={roomCategories ?? []}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
|||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|||||||
@@ -1,49 +1,60 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRef } from "react"
|
import { useCallback, useEffect, useMemo } from "react"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
import { roomFilterSchema } from "@/server/routers/hotels/schemas/room"
|
import { RoomPackageCode } from "@/server/routers/hotels/schemas/packages"
|
||||||
|
|
||||||
|
import Chip from "@/components/TempDesignSystem/Chip"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import styles from "./roomFilter.module.css"
|
import styles from "./roomFilter.module.css"
|
||||||
|
|
||||||
import {
|
import { RoomFilterProps } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
RoomFilterFormData,
|
|
||||||
RoomFilterProps,
|
export default function RoomFilter({
|
||||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
numberOfRooms,
|
||||||
|
onFilter,
|
||||||
|
filterOptions,
|
||||||
|
}: RoomFilterProps) {
|
||||||
|
const initialFilterValues = useMemo(
|
||||||
|
() =>
|
||||||
|
filterOptions.reduce(
|
||||||
|
(acc, option) => {
|
||||||
|
acc[option.code] = false
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, boolean | undefined>
|
||||||
|
),
|
||||||
|
[filterOptions]
|
||||||
|
)
|
||||||
|
|
||||||
function RoomFilter({ numberOfRooms }: RoomFilterProps) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const methods = useForm<RoomFilterFormData>({
|
const methods = useForm<Record<string, boolean | undefined>>({
|
||||||
defaultValues: {
|
defaultValues: initialFilterValues,
|
||||||
allergyFriendly: false,
|
|
||||||
petFriendly: false,
|
|
||||||
accessibility: false,
|
|
||||||
},
|
|
||||||
mode: "all",
|
mode: "all",
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
resolver: zodResolver(roomFilterSchema),
|
resolver: zodResolver(z.object({})),
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement | null>(null)
|
const { watch, getValues, handleSubmit } = methods
|
||||||
const { watch, setValue } = methods
|
const petFriendly = watch(RoomPackageCode.PETR)
|
||||||
const petFriendly = watch("petFriendly")
|
const allergyFriendly = watch(RoomPackageCode.ALLG)
|
||||||
const allergyFriendly = watch("allergyFriendly")
|
|
||||||
|
|
||||||
const onSubmit = (data: RoomFilterFormData) => {
|
const submitFilter = useCallback(() => {
|
||||||
if (data.petFriendly) {
|
const data = getValues()
|
||||||
setValue("allergyFriendly", false)
|
onFilter(data)
|
||||||
} else if (data.allergyFriendly) {
|
}, [onFilter, getValues])
|
||||||
setValue("petFriendly", false)
|
|
||||||
}
|
useEffect(() => {
|
||||||
console.log("Form submitted with data:", data)
|
const subscription = watch(() => handleSubmit(submitFilter)())
|
||||||
}
|
return () => subscription.unsubscribe()
|
||||||
|
}, [handleSubmit, watch, submitFilter])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -51,45 +62,27 @@ function RoomFilter({ numberOfRooms }: RoomFilterProps) {
|
|||||||
{intl.formatMessage({ id: "Room types available" }, { numberOfRooms })}
|
{intl.formatMessage({ id: "Room types available" }, { numberOfRooms })}
|
||||||
</Body>
|
</Body>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form ref={formRef} onSubmit={methods.handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(submitFilter)}>
|
||||||
<div className={styles.roomsFilter}>
|
<div className={styles.roomsFilter}>
|
||||||
<Checkbox
|
{filterOptions.map((option) => (
|
||||||
name="accessibility"
|
<Checkbox
|
||||||
onChange={() => formRef.current?.requestSubmit()}
|
name={option.code}
|
||||||
>
|
key={option.code}
|
||||||
<Caption color="uiTextHighContrast">
|
registerOptions={{
|
||||||
{intl.formatMessage({ id: "Accessibility room" })}
|
required: false,
|
||||||
</Caption>
|
disabled:
|
||||||
</Checkbox>
|
(option.code === RoomPackageCode.PETR && allergyFriendly) ||
|
||||||
<Checkbox
|
(option.code === RoomPackageCode.ALLG && petFriendly),
|
||||||
name="petFriendly"
|
}}
|
||||||
onChange={() => {
|
>
|
||||||
setValue("petFriendly", !petFriendly)
|
<Caption color="uiTextHighContrast">
|
||||||
formRef.current?.requestSubmit()
|
{intl.formatMessage({ id: option.description })}
|
||||||
}}
|
</Caption>
|
||||||
registerOptions={{ disabled: allergyFriendly }}
|
</Checkbox>
|
||||||
>
|
))}
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({ id: "Pet room" })}
|
|
||||||
</Caption>
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox
|
|
||||||
name="allergyFriendly"
|
|
||||||
onChange={() => {
|
|
||||||
setValue("allergyFriendly", !allergyFriendly)
|
|
||||||
formRef.current?.requestSubmit()
|
|
||||||
}}
|
|
||||||
registerOptions={{ disabled: petFriendly }}
|
|
||||||
>
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage({ id: "Allergy room" })}
|
|
||||||
</Caption>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RoomFilter
|
|
||||||
|
|||||||
53
components/HotelReservation/SelectRate/Rooms/index.tsx
Normal file
53
components/HotelReservation/SelectRate/Rooms/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { RoomsAvailability } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
|
import RoomFilter from "../RoomFilter"
|
||||||
|
import RoomSelection from "../RoomSelection"
|
||||||
|
|
||||||
|
import styles from "./rooms.module.css"
|
||||||
|
|
||||||
|
import { RoomProps } from "@/types/components/hotelReservation/selectRate/room"
|
||||||
|
import { RoomPackageCodes } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
|
export default function Rooms({
|
||||||
|
roomsAvailability,
|
||||||
|
roomCategories = [],
|
||||||
|
user,
|
||||||
|
packages,
|
||||||
|
}: RoomProps) {
|
||||||
|
const [rooms, setRooms] = useState<RoomsAvailability>(roomsAvailability)
|
||||||
|
|
||||||
|
function handleFilter(filter: Record<string, boolean | undefined>) {
|
||||||
|
const selectedCodes = Object.keys(filter).filter((key) => filter[key])
|
||||||
|
|
||||||
|
if (selectedCodes.length === 0) {
|
||||||
|
setRooms(roomsAvailability)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRooms = roomsAvailability.roomConfigurations.filter((room) =>
|
||||||
|
room.features.some((feature) =>
|
||||||
|
selectedCodes.includes(feature.code as RoomPackageCodes)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setRooms({ ...roomsAvailability, roomConfigurations: filteredRooms })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<RoomFilter
|
||||||
|
numberOfRooms={rooms.roomConfigurations.length}
|
||||||
|
onFilter={handleFilter}
|
||||||
|
filterOptions={packages}
|
||||||
|
/>
|
||||||
|
<RoomSelection
|
||||||
|
roomsAvailability={rooms}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.content {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x7);
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"A photo of the room": "Et foto af værelset",
|
"A photo of the room": "Et foto af værelset",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
"About the hotel": "About the hotel",
|
"About the hotel": "About the hotel",
|
||||||
"Accessibility room": "Handicapvenligt værelse",
|
"Accessible Room": "Tilgængelighedsrum",
|
||||||
"Activities": "Aktiviteter",
|
"Activities": "Aktiviteter",
|
||||||
"Add code": "Tilføj kode",
|
"Add code": "Tilføj kode",
|
||||||
"Add new card": "Tilføj nyt kort",
|
"Add new card": "Tilføj nyt kort",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"Adults": "voksne",
|
"Adults": "voksne",
|
||||||
"Airport": "Lufthavn",
|
"Airport": "Lufthavn",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle vores morgenmadsbuffeter tilbyder glutenfrie, veganske og allergivenlige muligheder.",
|
||||||
"Allergy room": "Allergivenligt værelse",
|
"Allergy Room": "Allergirum",
|
||||||
"Already a friend?": "Allerede en ven?",
|
"Already a friend?": "Allerede en ven?",
|
||||||
"Amenities": "Faciliteter",
|
"Amenities": "Faciliteter",
|
||||||
"Amusement park": "Forlystelsespark",
|
"Amusement park": "Forlystelsespark",
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
"Pay later": "Betal senere",
|
"Pay later": "Betal senere",
|
||||||
"Pay now": "Betal nu",
|
"Pay now": "Betal nu",
|
||||||
"Payment info": "Betalingsoplysninger",
|
"Payment info": "Betalingsoplysninger",
|
||||||
"Pet room": "Kæledyrsrum",
|
"Pet Room": "Kæledyrsrum",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefonnummer er påkrævet",
|
"Phone is required": "Telefonnummer er påkrævet",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"A photo of the room": "Ein Foto des Zimmers",
|
"A photo of the room": "Ein Foto des Zimmers",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
"About the hotel": "Über das Hotel",
|
"About the hotel": "Über das Hotel",
|
||||||
"Accessibility room": "Barrierefreies Zimmer",
|
"Accessible Room": "Barrierefreies Zimmer",
|
||||||
"Activities": "Aktivitäten",
|
"Activities": "Aktivitäten",
|
||||||
"Add code": "Code hinzufügen",
|
"Add code": "Code hinzufügen",
|
||||||
"Add new card": "Neue Karte hinzufügen",
|
"Add new card": "Neue Karte hinzufügen",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"Adults": "Erwachsene",
|
"Adults": "Erwachsene",
|
||||||
"Airport": "Flughafen",
|
"Airport": "Flughafen",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle unsere Frühstücksbuffets bieten glutenfreie, vegane und allergikerfreundliche Speisen.",
|
||||||
"Allergy room": "Allergiefreundliches Zimmer",
|
"Allergy Room": "Allergikerzimmer",
|
||||||
"Already a friend?": "Sind wir schon Freunde?",
|
"Already a friend?": "Sind wir schon Freunde?",
|
||||||
"Amenities": "Annehmlichkeiten",
|
"Amenities": "Annehmlichkeiten",
|
||||||
"Amusement park": "Vergnügungspark",
|
"Amusement park": "Vergnügungspark",
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
"Pay later": "Später bezahlen",
|
"Pay later": "Später bezahlen",
|
||||||
"Pay now": "Jetzt bezahlen",
|
"Pay now": "Jetzt bezahlen",
|
||||||
"Payment info": "Zahlungsinformationen",
|
"Payment info": "Zahlungsinformationen",
|
||||||
"Pet room": "Haustierfreundliches Zimmer",
|
"Pet Room": "Haustierzimmer",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefon ist erforderlich",
|
"Phone is required": "Telefon ist erforderlich",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"A photo of the room": "A photo of the room",
|
"A photo of the room": "A photo of the room",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
"About the hotel": "About the hotel",
|
"About the hotel": "About the hotel",
|
||||||
"Accessibility room": "Accessibility room",
|
"Accessible Room": "Accessibility room",
|
||||||
"Activities": "Activities",
|
"Activities": "Activities",
|
||||||
"Add Room": "Add room",
|
"Add Room": "Add room",
|
||||||
"Add code": "Add code",
|
"Add code": "Add code",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"Age": "Age",
|
"Age": "Age",
|
||||||
"Airport": "Airport",
|
"Airport": "Airport",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||||
"Allergy room": "Allergy room",
|
"Allergy Room": "Allergy room",
|
||||||
"Already a friend?": "Already a friend?",
|
"Already a friend?": "Already a friend?",
|
||||||
"Amenities": "Amenities",
|
"Amenities": "Amenities",
|
||||||
"Amusement park": "Amusement park",
|
"Amusement park": "Amusement park",
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
"Pay now": "Pay now",
|
"Pay now": "Pay now",
|
||||||
"Payment info": "Payment info",
|
"Payment info": "Payment info",
|
||||||
"Payment received": "Payment received",
|
"Payment received": "Payment received",
|
||||||
"Pet room": "Pet room",
|
"Pet Room": "Pet room",
|
||||||
"Phone": "Phone",
|
"Phone": "Phone",
|
||||||
"Phone is required": "Phone is required",
|
"Phone is required": "Phone is required",
|
||||||
"Phone number": "Phone number",
|
"Phone number": "Phone number",
|
||||||
@@ -258,6 +258,7 @@
|
|||||||
"Restaurant & Bar": "Restaurant & Bar",
|
"Restaurant & Bar": "Restaurant & Bar",
|
||||||
"Restaurants & Bars": "Restaurants & Bars",
|
"Restaurants & Bars": "Restaurants & Bars",
|
||||||
"Retype new password": "Retype new password",
|
"Retype new password": "Retype new password",
|
||||||
|
"Room": "Room",
|
||||||
"Room & Terms": "Room & Terms",
|
"Room & Terms": "Room & Terms",
|
||||||
"Room facilities": "Room facilities",
|
"Room facilities": "Room facilities",
|
||||||
"Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
"Room types available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"A photo of the room": "Kuva huoneesta",
|
"A photo of the room": "Kuva huoneesta",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
"About the hotel": "Tietoja hotellista",
|
"About the hotel": "Tietoja hotellista",
|
||||||
"Accessibility room": "Esteettömyyshuone",
|
"Accessible Room": "Esteetön huone",
|
||||||
"Activities": "Aktiviteetit",
|
"Activities": "Aktiviteetit",
|
||||||
"Add code": "Lisää koodi",
|
"Add code": "Lisää koodi",
|
||||||
"Add new card": "Lisää uusi kortti",
|
"Add new card": "Lisää uusi kortti",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"Adults": "Aikuista",
|
"Adults": "Aikuista",
|
||||||
"Airport": "Lentokenttä",
|
"Airport": "Lentokenttä",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Kaikki aamiaisbuffettimme tarjoavat gluteenittomia, vegaanisia ja allergiaystävällisiä vaihtoehtoja.",
|
||||||
"Allergy room": "Allergiahuone",
|
"Allergy Room": "Allergiahuone",
|
||||||
"Already a friend?": "Oletko jo ystävä?",
|
"Already a friend?": "Oletko jo ystävä?",
|
||||||
"Amenities": "Mukavuudet",
|
"Amenities": "Mukavuudet",
|
||||||
"Amusement park": "Huvipuisto",
|
"Amusement park": "Huvipuisto",
|
||||||
@@ -217,7 +217,7 @@
|
|||||||
"Pay later": "Maksa myöhemmin",
|
"Pay later": "Maksa myöhemmin",
|
||||||
"Pay now": "Maksa nyt",
|
"Pay now": "Maksa nyt",
|
||||||
"Payment info": "Maksutiedot",
|
"Payment info": "Maksutiedot",
|
||||||
"Pet room": "Lemmikkihuone",
|
"Pet Room": "Lemmikkihuone",
|
||||||
"Phone": "Puhelin",
|
"Phone": "Puhelin",
|
||||||
"Phone is required": "Puhelin vaaditaan",
|
"Phone is required": "Puhelin vaaditaan",
|
||||||
"Phone number": "Puhelinnumero",
|
"Phone number": "Puhelinnumero",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"A photo of the room": "Et bilde av rommet",
|
"A photo of the room": "Et bilde av rommet",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
"About the hotel": "Om hotellet",
|
"About the hotel": "Om hotellet",
|
||||||
"Accessibility room": "Tilgjengelighetsrom",
|
"Accessible Room": "Tilgjengelighetsrom",
|
||||||
"Activities": "Aktiviteter",
|
"Activities": "Aktiviteter",
|
||||||
"Add code": "Legg til kode",
|
"Add code": "Legg til kode",
|
||||||
"Add new card": "Legg til nytt kort",
|
"Add new card": "Legg til nytt kort",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"Adults": "Voksne",
|
"Adults": "Voksne",
|
||||||
"Airport": "Flyplass",
|
"Airport": "Flyplass",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
|
||||||
"Allergy room": "Allergihus",
|
"Allergy Room": "Allergirom",
|
||||||
"Already a friend?": "Allerede Friend?",
|
"Already a friend?": "Allerede Friend?",
|
||||||
"Amenities": "Fasiliteter",
|
"Amenities": "Fasiliteter",
|
||||||
"Amusement park": "Tivoli",
|
"Amusement park": "Tivoli",
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
"Pay later": "Betal senere",
|
"Pay later": "Betal senere",
|
||||||
"Pay now": "Betal nå",
|
"Pay now": "Betal nå",
|
||||||
"Payment info": "Betalingsinformasjon",
|
"Payment info": "Betalingsinformasjon",
|
||||||
"Pet room": "Kjæledyrrom",
|
"Pet Room": "Kjæledyrsrom",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefon kreves",
|
"Phone is required": "Telefon kreves",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"A photo of the room": "Ett foto av rummet",
|
"A photo of the room": "Ett foto av rummet",
|
||||||
"About meetings & conferences": "About meetings & conferences",
|
"About meetings & conferences": "About meetings & conferences",
|
||||||
"About the hotel": "Om hotellet",
|
"About the hotel": "Om hotellet",
|
||||||
"Accessibility room": "Tillgänglighetsrum",
|
"Accessible Room": "Tillgänglighetsrum",
|
||||||
"Activities": "Aktiviteter",
|
"Activities": "Aktiviteter",
|
||||||
"Add code": "Lägg till kod",
|
"Add code": "Lägg till kod",
|
||||||
"Add new card": "Lägg till nytt kort",
|
"Add new card": "Lägg till nytt kort",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"Adults": "Vuxna",
|
"Adults": "Vuxna",
|
||||||
"Airport": "Flygplats",
|
"Airport": "Flygplats",
|
||||||
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.",
|
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alla våra frukostbufféer erbjuder glutenfria, veganska och allergivänliga alternativ.",
|
||||||
"Allergy room": "Allergirum",
|
"Allergy Room": "Allergirum",
|
||||||
"Already a friend?": "Är du redan en vän?",
|
"Already a friend?": "Är du redan en vän?",
|
||||||
"Amenities": "Bekvämligheter",
|
"Amenities": "Bekvämligheter",
|
||||||
"Amusement park": "Nöjespark",
|
"Amusement park": "Nöjespark",
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
"Pay later": "Betala senare",
|
"Pay later": "Betala senare",
|
||||||
"Pay now": "Betala nu",
|
"Pay now": "Betala nu",
|
||||||
"Payment info": "Betalningsinformation",
|
"Payment info": "Betalningsinformation",
|
||||||
"Pet room": "Husdjursrum",
|
"Pet Room": "Husdjursrum",
|
||||||
"Phone": "Telefon",
|
"Phone": "Telefon",
|
||||||
"Phone is required": "Telefonnummer är obligatorisk",
|
"Phone is required": "Telefonnummer är obligatorisk",
|
||||||
"Phone number": "Telefonnummer",
|
"Phone number": "Telefonnummer",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export namespace endpoints {
|
|||||||
rewards = `${profile}/reward`,
|
rewards = `${profile}/reward`,
|
||||||
tierRewards = `${profile}/TierRewards`,
|
tierRewards = `${profile}/TierRewards`,
|
||||||
subscriberId = `${profile}/SubscriberId`,
|
subscriberId = `${profile}/SubscriberId`,
|
||||||
|
packages = "package/v1/packages/hotel",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function get(
|
|||||||
const searchParams = new URLSearchParams(params)
|
const searchParams = new URLSearchParams(params)
|
||||||
if (searchParams.size) {
|
if (searchParams.size) {
|
||||||
searchParams.forEach((value, key) => {
|
searchParams.forEach((value, key) => {
|
||||||
url.searchParams.set(key, value)
|
url.searchParams.append(key, value)
|
||||||
})
|
})
|
||||||
url.searchParams.sort()
|
url.searchParams.sort()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import {
|
|||||||
getHotelPageCounter,
|
getHotelPageCounter,
|
||||||
validateHotelPageRefs,
|
validateHotelPageRefs,
|
||||||
} from "../contentstack/hotelPage/utils"
|
} from "../contentstack/hotelPage/utils"
|
||||||
|
import {
|
||||||
|
getRoomPackagesInputSchema,
|
||||||
|
getRoomPackagesSchema,
|
||||||
|
} from "./schemas/packages"
|
||||||
import {
|
import {
|
||||||
getHotelInputSchema,
|
getHotelInputSchema,
|
||||||
getHotelsAvailabilityInputSchema,
|
getHotelsAvailabilityInputSchema,
|
||||||
@@ -57,6 +61,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
|||||||
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
|
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
|
||||||
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
|
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
|
||||||
|
|
||||||
|
const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get")
|
||||||
|
const getPackagesSuccessCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.packages.get-success"
|
||||||
|
)
|
||||||
|
const getPackagesFailCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.packages.get-fail"
|
||||||
|
)
|
||||||
|
|
||||||
const hotelsAvailabilityCounter = meter.createCounter(
|
const hotelsAvailabilityCounter = meter.createCounter(
|
||||||
"trpc.hotel.availability.hotels"
|
"trpc.hotel.availability.hotels"
|
||||||
)
|
)
|
||||||
@@ -694,4 +706,89 @@ export const hotelQueryRouter = router({
|
|||||||
return locations
|
return locations
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
packages: router({
|
||||||
|
get: serviceProcedure
|
||||||
|
.input(getRoomPackagesInputSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const { hotelId, startDate, endDate, adults, children, packageCodes } =
|
||||||
|
input
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
adults: adults.toString(),
|
||||||
|
children: children.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
packageCodes.forEach((code) => {
|
||||||
|
searchParams.append("packageCodes", code)
|
||||||
|
})
|
||||||
|
|
||||||
|
const params = searchParams.toString()
|
||||||
|
|
||||||
|
getPackagesCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.packages start",
|
||||||
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
`${api.endpoints.v1.packages}/${hotelId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
getPackagesFailCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.packages error",
|
||||||
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
|
)
|
||||||
|
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson)
|
||||||
|
|
||||||
|
if (!validatedPackagesData.success) {
|
||||||
|
getHotelFailCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validatedPackagesData.error),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"api.hotels.packages validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: validatedPackagesData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
getPackagesSuccessCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.packages success",
|
||||||
|
JSON.stringify({ query: { hotelId, params: params } })
|
||||||
|
)
|
||||||
|
|
||||||
|
return validatedPackagesData.data
|
||||||
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
59
server/routers/hotels/schemas/packages.ts
Normal file
59
server/routers/hotels/schemas/packages.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export enum RoomPackageCode {
|
||||||
|
PETR = "PETR",
|
||||||
|
ALLG = "ALLG",
|
||||||
|
ACCE = "ACCE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRoomPackagesInputSchema = z.object({
|
||||||
|
hotelId: z.string(),
|
||||||
|
startDate: z.string(),
|
||||||
|
endDate: z.string(),
|
||||||
|
adults: z.number(),
|
||||||
|
children: z.number().optional().default(0),
|
||||||
|
packageCodes: z.array(z.string()).optional().default([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
const packagesSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
code: z.enum([
|
||||||
|
RoomPackageCode.PETR,
|
||||||
|
RoomPackageCode.ALLG,
|
||||||
|
RoomPackageCode.ACCE,
|
||||||
|
]),
|
||||||
|
itemCode: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
currency: z.string(),
|
||||||
|
calculatedPrice: z.number(),
|
||||||
|
inventories: z.array(
|
||||||
|
z.object({
|
||||||
|
date: z.string(),
|
||||||
|
total: z.number(),
|
||||||
|
available: z.number(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const getRoomPackagesSchema = z
|
||||||
|
.object({
|
||||||
|
data: z.object({
|
||||||
|
attributes: z.object({
|
||||||
|
hotelId: z.number(),
|
||||||
|
packages: packagesSchema,
|
||||||
|
}),
|
||||||
|
relationships: z
|
||||||
|
.object({
|
||||||
|
links: z.array(
|
||||||
|
z.object({
|
||||||
|
url: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
type: z.string(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.transform((data) => data.data.attributes.packages)
|
||||||
@@ -92,9 +92,3 @@ export const roomSchema = z
|
|||||||
roomFacilities: data.attributes.roomFacilities,
|
roomFacilities: data.attributes.roomFacilities,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const roomFilterSchema = z.object({
|
|
||||||
accessibility: z.boolean(),
|
|
||||||
petFriendly: z.boolean(),
|
|
||||||
allergyFriendly: z.boolean(),
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export async function getServiceToken() {
|
|||||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||||
scopes = ["profile"]
|
scopes = ["profile"]
|
||||||
} else {
|
} else {
|
||||||
scopes = ["profile", "hotel", "booking"]
|
scopes = ["profile", "hotel", "booking", "package"]
|
||||||
}
|
}
|
||||||
const tag = generateServiceTokenTag(scopes)
|
const tag = generateServiceTokenTag(scopes)
|
||||||
const getCachedJwt = unstable_cache(
|
const getCachedJwt = unstable_cache(
|
||||||
|
|||||||
6
types/components/hotelReservation/selectRate/room.ts
Normal file
6
types/components/hotelReservation/selectRate/room.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { RoomPackageData } from "./roomFilter"
|
||||||
|
import { RoomSelectionProps } from "./roomSelection"
|
||||||
|
|
||||||
|
export interface RoomProps extends RoomSelectionProps {
|
||||||
|
packages: RoomPackageData
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { roomFilterSchema } from "@/server/routers/hotels/schemas/room"
|
import { getRoomPackagesSchema } from "@/server/routers/hotels/schemas/packages"
|
||||||
|
|
||||||
export interface RoomFilterProps {
|
export interface RoomFilterProps {
|
||||||
numberOfRooms: number
|
numberOfRooms: number
|
||||||
|
onFilter: (filter: Record<string, boolean | undefined>) => void
|
||||||
|
filterOptions: RoomPackageData
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomFilterFormData extends z.output<typeof roomFilterSchema> {}
|
export interface RoomPackageData
|
||||||
|
extends z.output<typeof getRoomPackagesSchema> {}
|
||||||
|
|
||||||
|
export type RoomPackageCodes = RoomPackageData[number]["code"]
|
||||||
|
|||||||
Reference in New Issue
Block a user