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:
Niclas Edenvin
2024-10-08 09:10:06 +00:00
parent 05222035ff
commit 6e6d14875d
14 changed files with 143 additions and 72 deletions

View File

@@ -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}

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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>
) )
} }

View File

@@ -21,3 +21,10 @@
position: fixed; position: fixed;
width: 0; width: 0;
} }
.summary {
position: fixed;
bottom: 0;
left: 0;
right: 0;
}

View File

@@ -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`,

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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 },
}) })

View File

@@ -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
} }

View File

@@ -0,0 +1,7 @@
import { Rate } from "@/server/routers/hotels/output"
export interface RoomSelectionProps {
rates: Rate[]
nrOfAdults: number
nrOfNights: number
}

View File

@@ -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 {

View File

@@ -0,0 +1,5 @@
export interface SelectRateSearchParams {
fromDate: string
toDate: string
hotel: string
}