Merged in feat/SW-454-select-room-api (pull request #648)
Feat/SW-454 Create select rate page foundation * Extract select-rate page to its own, fixed route * Rename availability to hotelsAvailability * Update availability hotels response * Number to string Approved-by: Pontus Dreij
This commit is contained in:
@@ -8,7 +8,6 @@ import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
|||||||
import Details from "@/components/HotelReservation/EnterDetails/Details"
|
import Details from "@/components/HotelReservation/EnterDetails/Details"
|
||||||
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
|
||||||
import Payment from "@/components/HotelReservation/SelectRate/Payment"
|
import Payment from "@/components/HotelReservation/SelectRate/Payment"
|
||||||
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
|
||||||
import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion"
|
import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion"
|
||||||
import Summary from "@/components/HotelReservation/SelectRate/Summary"
|
import Summary from "@/components/HotelReservation/SelectRate/Summary"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
@@ -138,18 +137,7 @@ export default async function SectionsPage({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
path={`select-rate?${currentSearchParams}`}
|
path={`select-rate?${currentSearchParams}`}
|
||||||
>
|
></SectionAccordion>
|
||||||
{params.section === "select-rate" && (
|
|
||||||
<RoomSelection
|
|
||||||
alternatives={rooms}
|
|
||||||
nextPath="select-bed"
|
|
||||||
// TODO: Get real value
|
|
||||||
nrOfNights={1}
|
|
||||||
// TODO: Get real value
|
|
||||||
nrOfAdults={1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SectionAccordion>
|
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header={intl.formatMessage({ id: "Bed type" })}
|
header={intl.formatMessage({ id: "Bed type" })}
|
||||||
selection={selectedBed}
|
selection={selectedBed}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFil
|
|||||||
export async function fetchAvailableHotels(
|
export async function fetchAvailableHotels(
|
||||||
input: AvailabilityInput
|
input: AvailabilityInput
|
||||||
): Promise<HotelData[]> {
|
): Promise<HotelData[]> {
|
||||||
const availableHotels = await serverClient().hotel.availability.get(input)
|
const availableHotels = await serverClient().hotel.availability.hotels(input)
|
||||||
|
|
||||||
if (!availableHotels) throw new Error()
|
if (!availableHotels) throw new Error()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
.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: 1134px;
|
||||||
|
margin-top: var(--Spacing-x5);
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--Spacing-x7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
|
||||||
|
|
||||||
|
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
|
import styles from "./page.module.css"
|
||||||
|
|
||||||
|
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
|
export default async function SelectRatePage({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
|
||||||
|
setLang(params.lang)
|
||||||
|
|
||||||
|
// TODO: Use real endpoint.
|
||||||
|
const hotel = tempHotelData.data.attributes
|
||||||
|
|
||||||
|
const rates = await serverClient().hotel.rates.get({
|
||||||
|
// TODO: pass the correct hotel ID and all other parameters that should be included in the search
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
})
|
||||||
|
|
||||||
|
// const rates = await serverClient().hotel.availability.getForHotel({
|
||||||
|
// hotelId: 811,
|
||||||
|
// roomStayStartDate: "2024-11-02",
|
||||||
|
// roomStayEndDate: "2024-11-03",
|
||||||
|
// adults: 1,
|
||||||
|
// })
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* TODO: Add Hotel Listing Card */}
|
||||||
|
<div>Hotel Listing Card TBI</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.main}>
|
||||||
|
<RoomSelection
|
||||||
|
rates={rates}
|
||||||
|
// TODO: Get real value
|
||||||
|
nrOfNights={1}
|
||||||
|
// TODO: Get real value
|
||||||
|
nrOfAdults={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,11 +5,10 @@ import RoomCard from "./RoomCard"
|
|||||||
|
|
||||||
import styles from "./roomSelection.module.css"
|
import styles from "./roomSelection.module.css"
|
||||||
|
|
||||||
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/section"
|
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||||
|
|
||||||
export default function RoomSelection({
|
export default function RoomSelection({
|
||||||
alternatives,
|
rates,
|
||||||
nextPath,
|
|
||||||
nrOfNights,
|
nrOfNights,
|
||||||
nrOfAdults,
|
nrOfAdults,
|
||||||
}: RoomSelectionProps) {
|
}: RoomSelectionProps) {
|
||||||
@@ -21,17 +20,17 @@ export default function RoomSelection({
|
|||||||
const queryParams = new URLSearchParams(searchParams)
|
const queryParams = new URLSearchParams(searchParams)
|
||||||
queryParams.set("roomClass", e.currentTarget.roomClass?.value)
|
queryParams.set("roomClass", e.currentTarget.roomClass?.value)
|
||||||
queryParams.set("flexibility", e.currentTarget.flexibility?.value)
|
queryParams.set("flexibility", e.currentTarget.flexibility?.value)
|
||||||
router.push(`${nextPath}?${queryParams}`)
|
router.push(`select-bed?${queryParams}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<ul className={styles.roomList}>
|
<ul className={styles.roomList}>
|
||||||
{alternatives.map((room) => (
|
{rates.map((room) => (
|
||||||
<li key={room.id}>
|
<li key={room.id}>
|
||||||
<form
|
<form
|
||||||
method="GET"
|
method="GET"
|
||||||
action={`${nextPath}?${searchParams}`}
|
action={`select-bed?${searchParams}`}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -50,6 +49,7 @@ export default function RoomSelection({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div className={styles.summary}>This is summary</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,10 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export namespace endpoints {
|
|||||||
profile = "profile/v0/Profile",
|
profile = "profile/v0/Profile",
|
||||||
}
|
}
|
||||||
export const enum v1 {
|
export const enum v1 {
|
||||||
availability = "availability/v1/availabilities/city",
|
hotelsAvailability = "availability/v1/availabilities/city",
|
||||||
profile = "profile/v1/Profile",
|
profile = "profile/v1/Profile",
|
||||||
booking = "booking/v1/Bookings",
|
booking = "booking/v1/Bookings",
|
||||||
creditCards = `${profile}/creditCards`,
|
creditCards = `${profile}/creditCards`,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const getHotelInputSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getAvailabilityInputSchema = z.object({
|
export const getHotelsAvailabilityInputSchema = z.object({
|
||||||
cityId: z.string(),
|
cityId: z.string(),
|
||||||
roomStayStartDate: z.string(),
|
roomStayStartDate: z.string(),
|
||||||
roomStayEndDate: z.string(),
|
roomStayEndDate: z.string(),
|
||||||
|
|||||||
@@ -525,26 +525,18 @@ const occupancySchema = z.object({
|
|||||||
|
|
||||||
const bestPricePerStaySchema = z.object({
|
const bestPricePerStaySchema = z.object({
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
amount: z.number(),
|
// TODO: remove optional when API is ready
|
||||||
regularAmount: z.number(),
|
regularAmount: z.string().optional(),
|
||||||
memberAmount: z.number(),
|
// TODO: remove optional when API is ready
|
||||||
discountRate: z.number(),
|
memberAmount: z.string().optional(),
|
||||||
discountAmount: z.number(),
|
|
||||||
points: z.number(),
|
|
||||||
numberOfVouchers: z.number(),
|
|
||||||
numberOfBonusCheques: z.number(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const bestPricePerNightSchema = z.object({
|
const bestPricePerNightSchema = z.object({
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
amount: z.number(),
|
// TODO: remove optional when API is ready
|
||||||
regularAmount: z.number(),
|
regularAmount: z.string().optional(),
|
||||||
memberAmount: z.number(),
|
// TODO: remove optional when API is ready
|
||||||
discountRate: z.number(),
|
memberAmount: z.string().optional(),
|
||||||
discountAmount: z.number(),
|
|
||||||
points: z.number(),
|
|
||||||
numberOfVouchers: z.number(),
|
|
||||||
numberOfBonusCheques: z.number(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const linksSchema = z.object({
|
const linksSchema = z.object({
|
||||||
@@ -556,7 +548,7 @@ const linksSchema = z.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
const availabilitySchema = z.object({
|
const hotelsAvailabilitySchema = z.object({
|
||||||
data: z.array(
|
data: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
attributes: z.object({
|
attributes: z.object({
|
||||||
@@ -575,10 +567,10 @@ const availabilitySchema = z.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getAvailabilitySchema = availabilitySchema
|
export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema
|
||||||
export type Availability = z.infer<typeof availabilitySchema>
|
export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
|
||||||
export type AvailabilityPrices =
|
export type HotelsAvailabilityPrices =
|
||||||
Availability["data"][number]["attributes"]["bestPricePerNight"]
|
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
|
||||||
|
|
||||||
const flexibilityPrice = z.object({
|
const flexibilityPrice = z.object({
|
||||||
standard: z.number(),
|
standard: z.number(),
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ import { toApiLang } from "@/server/utils"
|
|||||||
|
|
||||||
import { hotelPageSchema } from "../contentstack/hotelPage/output"
|
import { hotelPageSchema } from "../contentstack/hotelPage/output"
|
||||||
import {
|
import {
|
||||||
getAvailabilityInputSchema,
|
|
||||||
getHotelInputSchema,
|
getHotelInputSchema,
|
||||||
|
getHotelsAvailabilityInputSchema,
|
||||||
getlHotelDataInputSchema,
|
getlHotelDataInputSchema,
|
||||||
getRatesInputSchema,
|
getRatesInputSchema,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
import {
|
import {
|
||||||
getAvailabilitySchema,
|
|
||||||
getHotelDataSchema,
|
getHotelDataSchema,
|
||||||
|
getHotelsAvailabilitySchema,
|
||||||
getRatesSchema,
|
getRatesSchema,
|
||||||
roomSchema,
|
roomSchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
@@ -51,12 +51,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 availabilityCounter = meter.createCounter("trpc.hotel.availability")
|
const hotelsAvailabilityCounter = meter.createCounter(
|
||||||
const availabilitySuccessCounter = meter.createCounter(
|
"trpc.hotel.availability.hotels"
|
||||||
"trpc.hotel.availability-success"
|
|
||||||
)
|
)
|
||||||
const availabilityFailCounter = meter.createCounter(
|
const hotelsAvailabilitySuccessCounter = meter.createCounter(
|
||||||
"trpc.hotel.availability-fail"
|
"trpc.hotel.availability.hotels-success"
|
||||||
|
)
|
||||||
|
const hotelsAvailabilityFailCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.hotels-fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
async function getContentstackData(
|
async function getContentstackData(
|
||||||
@@ -250,8 +252,8 @@ export const hotelQueryRouter = router({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
availability: router({
|
availability: router({
|
||||||
get: hotelServiceProcedure
|
hotels: hotelServiceProcedure
|
||||||
.input(getAvailabilityInputSchema)
|
.input(getHotelsAvailabilityInputSchema)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const {
|
const {
|
||||||
cityId,
|
cityId,
|
||||||
@@ -274,7 +276,7 @@ export const hotelQueryRouter = router({
|
|||||||
attachedProfileId,
|
attachedProfileId,
|
||||||
}
|
}
|
||||||
|
|
||||||
availabilityCounter.add(1, {
|
hotelsAvailabilityCounter.add(1, {
|
||||||
cityId,
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
@@ -284,11 +286,11 @@ export const hotelQueryRouter = router({
|
|||||||
reservationProfileType,
|
reservationProfileType,
|
||||||
})
|
})
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotels.availability start",
|
"api.hotels.hotelsAvailability start",
|
||||||
JSON.stringify({ query: { cityId, params } })
|
JSON.stringify({ query: { cityId, params } })
|
||||||
)
|
)
|
||||||
const apiResponse = await api.get(
|
const apiResponse = await api.get(
|
||||||
`${api.endpoints.v1.availability}/${cityId}`,
|
`${api.endpoints.v1.hotelsAvailability}/${cityId}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
@@ -298,7 +300,7 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
const text = await apiResponse.text()
|
const text = await apiResponse.text()
|
||||||
availabilityFailCounter.add(1, {
|
hotelsAvailabilityFailCounter.add(1, {
|
||||||
cityId,
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
@@ -314,7 +316,7 @@ export const hotelQueryRouter = router({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"api.hotels.availability error",
|
"api.hotels.hotelsAvailability error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { cityId, params },
|
query: { cityId, params },
|
||||||
error: {
|
error: {
|
||||||
@@ -328,9 +330,9 @@ export const hotelQueryRouter = router({
|
|||||||
}
|
}
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const validateAvailabilityData =
|
const validateAvailabilityData =
|
||||||
getAvailabilitySchema.safeParse(apiJson)
|
getHotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
if (!validateAvailabilityData.success) {
|
if (!validateAvailabilityData.success) {
|
||||||
availabilityFailCounter.add(1, {
|
hotelsAvailabilityFailCounter.add(1, {
|
||||||
cityId,
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
@@ -342,7 +344,7 @@ export const hotelQueryRouter = router({
|
|||||||
error: JSON.stringify(validateAvailabilityData.error),
|
error: JSON.stringify(validateAvailabilityData.error),
|
||||||
})
|
})
|
||||||
console.error(
|
console.error(
|
||||||
"api.hotels.availability validation error",
|
"api.hotels.hotelsAvailability validation error",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { cityId, params },
|
query: { cityId, params },
|
||||||
error: validateAvailabilityData.error,
|
error: validateAvailabilityData.error,
|
||||||
@@ -350,7 +352,7 @@ export const hotelQueryRouter = router({
|
|||||||
)
|
)
|
||||||
throw badRequestError()
|
throw badRequestError()
|
||||||
}
|
}
|
||||||
availabilitySuccessCounter.add(1, {
|
hotelsAvailabilitySuccessCounter.add(1, {
|
||||||
cityId,
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
@@ -360,7 +362,7 @@ export const hotelQueryRouter = router({
|
|||||||
reservationProfileType,
|
reservationProfileType,
|
||||||
})
|
})
|
||||||
console.info(
|
console.info(
|
||||||
"api.hotels.availability success",
|
"api.hotels.hotelsAvailability success",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
query: { cityId, params: params },
|
query: { cityId, params: params },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AvailabilityPrices } from "@/server/routers/hotels/output"
|
import { HotelsAvailabilityPrices } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
import { Hotel } from "@/types/hotel"
|
import { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
@@ -8,5 +8,5 @@ export type HotelCardListingProps = {
|
|||||||
|
|
||||||
export type HotelData = {
|
export type HotelData = {
|
||||||
hotelData: Hotel
|
hotelData: Hotel
|
||||||
price: AvailabilityPrices
|
price: HotelsAvailabilityPrices
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Rate } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
|
export interface RoomSelectionProps {
|
||||||
|
rates: Rate[]
|
||||||
|
nrOfAdults: number
|
||||||
|
nrOfNights: number
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { Rate } from "@/server/routers/hotels/output"
|
|
||||||
|
|
||||||
import { Hotel } from "@/types/hotel"
|
import { Hotel } from "@/types/hotel"
|
||||||
|
|
||||||
export interface SectionProps {
|
export interface SectionProps {
|
||||||
@@ -27,12 +25,6 @@ export interface BreakfastSelectionProps extends SectionProps {
|
|||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomSelectionProps extends SectionProps {
|
|
||||||
alternatives: Rate[]
|
|
||||||
nrOfAdults: number
|
|
||||||
nrOfNights: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DetailsProps extends SectionProps {}
|
export interface DetailsProps extends SectionProps {}
|
||||||
|
|
||||||
export interface PaymentProps {
|
export interface PaymentProps {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SelectRateSearchParams {
|
||||||
|
fromDate: string
|
||||||
|
toDate: string
|
||||||
|
hotel: string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user