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 HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
import Payment from "@/components/HotelReservation/SelectRate/Payment"
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion"
import Summary from "@/components/HotelReservation/SelectRate/Summary"
import { getIntl } from "@/i18n"
@@ -138,18 +137,7 @@ export default async function SectionsPage({
: undefined
}
path={`select-rate?${currentSearchParams}`}
>
{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" })}
selection={selectedBed}

View File

@@ -9,7 +9,7 @@ import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFil
export async function fetchAvailableHotels(
input: AvailabilityInput
): Promise<HotelData[]> {
const availableHotels = await serverClient().hotel.availability.get(input)
const availableHotels = await serverClient().hotel.availability.hotels(input)
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 { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/section"
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export default function RoomSelection({
alternatives,
nextPath,
rates,
nrOfNights,
nrOfAdults,
}: RoomSelectionProps) {
@@ -21,17 +20,17 @@ export default function RoomSelection({
const queryParams = new URLSearchParams(searchParams)
queryParams.set("roomClass", e.currentTarget.roomClass?.value)
queryParams.set("flexibility", e.currentTarget.flexibility?.value)
router.push(`${nextPath}?${queryParams}`)
router.push(`select-bed?${queryParams}`)
}
return (
<div className={styles.wrapper}>
<ul className={styles.roomList}>
{alternatives.map((room) => (
{rates.map((room) => (
<li key={room.id}>
<form
method="GET"
action={`${nextPath}?${searchParams}`}
action={`select-bed?${searchParams}`}
onSubmit={handleSubmit}
>
<input
@@ -50,6 +49,7 @@ export default function RoomSelection({
</li>
))}
</ul>
<div className={styles.summary}>This is summary</div>
</div>
)
}

View File

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

View File

@@ -6,7 +6,7 @@ export namespace endpoints {
profile = "profile/v0/Profile",
}
export const enum v1 {
availability = "availability/v1/availabilities/city",
hotelsAvailability = "availability/v1/availabilities/city",
profile = "profile/v1/Profile",
booking = "booking/v1/Bookings",
creditCards = `${profile}/creditCards`,

View File

@@ -6,7 +6,7 @@ export const getHotelInputSchema = z.object({
.optional(),
})
export const getAvailabilityInputSchema = z.object({
export const getHotelsAvailabilityInputSchema = z.object({
cityId: z.string(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),

View File

@@ -525,26 +525,18 @@ const occupancySchema = z.object({
const bestPricePerStaySchema = z.object({
currency: z.string(),
amount: z.number(),
regularAmount: z.number(),
memberAmount: z.number(),
discountRate: z.number(),
discountAmount: z.number(),
points: z.number(),
numberOfVouchers: z.number(),
numberOfBonusCheques: z.number(),
// TODO: remove optional when API is ready
regularAmount: z.string().optional(),
// TODO: remove optional when API is ready
memberAmount: z.string().optional(),
})
const bestPricePerNightSchema = z.object({
currency: z.string(),
amount: z.number(),
regularAmount: z.number(),
memberAmount: z.number(),
discountRate: z.number(),
discountAmount: z.number(),
points: z.number(),
numberOfVouchers: z.number(),
numberOfBonusCheques: z.number(),
// TODO: remove optional when API is ready
regularAmount: z.string().optional(),
// TODO: remove optional when API is ready
memberAmount: z.string().optional(),
})
const linksSchema = z.object({
@@ -556,7 +548,7 @@ const linksSchema = z.object({
),
})
const availabilitySchema = z.object({
const hotelsAvailabilitySchema = z.object({
data: z.array(
z.object({
attributes: z.object({
@@ -575,10 +567,10 @@ const availabilitySchema = z.object({
),
})
export const getAvailabilitySchema = availabilitySchema
export type Availability = z.infer<typeof availabilitySchema>
export type AvailabilityPrices =
Availability["data"][number]["attributes"]["bestPricePerNight"]
export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema
export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
export type HotelsAvailabilityPrices =
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
const flexibilityPrice = z.object({
standard: z.number(),

View File

@@ -20,14 +20,14 @@ import { toApiLang } from "@/server/utils"
import { hotelPageSchema } from "../contentstack/hotelPage/output"
import {
getAvailabilityInputSchema,
getHotelInputSchema,
getHotelsAvailabilityInputSchema,
getlHotelDataInputSchema,
getRatesInputSchema,
} from "./input"
import {
getAvailabilitySchema,
getHotelDataSchema,
getHotelsAvailabilitySchema,
getRatesSchema,
roomSchema,
} from "./output"
@@ -51,12 +51,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get")
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
const availabilityCounter = meter.createCounter("trpc.hotel.availability")
const availabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability-success"
const hotelsAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.hotels"
)
const availabilityFailCounter = meter.createCounter(
"trpc.hotel.availability-fail"
const hotelsAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.hotels-success"
)
const hotelsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.hotels-fail"
)
async function getContentstackData(
@@ -250,8 +252,8 @@ export const hotelQueryRouter = router({
}
}),
availability: router({
get: hotelServiceProcedure
.input(getAvailabilityInputSchema)
hotels: hotelServiceProcedure
.input(getHotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const {
cityId,
@@ -274,7 +276,7 @@ export const hotelQueryRouter = router({
attachedProfileId,
}
availabilityCounter.add(1, {
hotelsAvailabilityCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
@@ -284,11 +286,11 @@ export const hotelQueryRouter = router({
reservationProfileType,
})
console.info(
"api.hotels.availability start",
"api.hotels.hotelsAvailability start",
JSON.stringify({ query: { cityId, params } })
)
const apiResponse = await api.get(
`${api.endpoints.v1.availability}/${cityId}`,
`${api.endpoints.v1.hotelsAvailability}/${cityId}`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
@@ -298,7 +300,7 @@ export const hotelQueryRouter = router({
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
availabilityFailCounter.add(1, {
hotelsAvailabilityFailCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
@@ -314,7 +316,7 @@ export const hotelQueryRouter = router({
}),
})
console.error(
"api.hotels.availability error",
"api.hotels.hotelsAvailability error",
JSON.stringify({
query: { cityId, params },
error: {
@@ -328,9 +330,9 @@ export const hotelQueryRouter = router({
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
getAvailabilitySchema.safeParse(apiJson)
getHotelsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
availabilityFailCounter.add(1, {
hotelsAvailabilityFailCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
@@ -342,7 +344,7 @@ export const hotelQueryRouter = router({
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.availability validation error",
"api.hotels.hotelsAvailability validation error",
JSON.stringify({
query: { cityId, params },
error: validateAvailabilityData.error,
@@ -350,7 +352,7 @@ export const hotelQueryRouter = router({
)
throw badRequestError()
}
availabilitySuccessCounter.add(1, {
hotelsAvailabilitySuccessCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
@@ -360,7 +362,7 @@ export const hotelQueryRouter = router({
reservationProfileType,
})
console.info(
"api.hotels.availability success",
"api.hotels.hotelsAvailability success",
JSON.stringify({
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"
@@ -8,5 +8,5 @@ export type HotelCardListingProps = {
export type HotelData = {
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"
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 PaymentProps {

View File

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