Merge branch 'develop' into test

This commit is contained in:
Michael Zetterberg
2024-09-20 14:13:40 +02:00
432 changed files with 17638 additions and 1689 deletions

View File

@@ -4,4 +4,5 @@
font-family: var(--typography-Body-Regular-fontFamily);
gap: var(--Spacing-x3);
grid-template-rows: auto 1fr;
position: relative;
}

View File

@@ -1,5 +1,7 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import ContentPage from "@/components/ContentType/ContentPage"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
@@ -21,10 +23,16 @@ export default async function ContentTypePage({
switch (params.contentType) {
case "content-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <ContentPage />
case "loyalty-page":
return <LoyaltyPage />
case "hotel-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <HotelPage />
default:
const type: never = params.contentType

View File

@@ -0,0 +1,27 @@
# Booking flow
The booking flow is the user journey of booking one or more rooms at our
hotels. Everything from choosing the date to payment and confirmation is
part of the booking flow.
## Booking widget
On most of the pages on the website we have a booking widget. This is where
the user starts the booking flow, by filling the form and submit. If they
entered a city as the destination they will land on the select hotel page
and if they entered a specific hotel they will land on the select rate page.
## Select hotel
Lists available hotels based on the search criteria. When the user selects
a hotel they land on the select rate page.
## Select rate, room, breakfast etc
This is a page with an accordion like design, but every accordion is handled
as its own page with its own URL.
## State management
The state, like search parameters and selected alternatives, is kept
throughout the booking flow in the URL.

View File

@@ -1,7 +1,3 @@
.hotelInfo {
margin-bottom: 64px;
}
.page {
min-height: 100dvh;
padding-top: var(--Spacing-x6);
@@ -12,6 +8,18 @@
.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,181 @@
import { serverClient } from "@/lib/trpc/server"
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
import BedSelection from "@/components/HotelReservation/SelectRate/BedSelection"
import BreakfastSelection from "@/components/HotelReservation/SelectRate/BreakfastSelection"
import Details from "@/components/HotelReservation/SelectRate/Details"
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"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { SectionPageProps } from "@/types/components/hotelReservation/selectRate/section"
import { LangParams, PageArgs } from "@/types/params"
const bedAlternatives = [
{
value: "queen",
name: "Queen bed",
payment: "160 cm",
pricePerNight: 0,
membersPricePerNight: 0,
currency: "SEK",
},
{
value: "king",
name: "King bed",
payment: "160 cm",
pricePerNight: 0,
membersPricePerNight: 0,
currency: "SEK",
},
{
value: "twin",
name: "Twin bed",
payment: "90 cm + 90 cm",
pricePerNight: 82,
membersPricePerNight: 67,
currency: "SEK",
},
]
const breakfastAlternatives = [
{
value: "no",
name: "No breakfast",
payment: "Always cheeper to get it online",
pricePerNight: 0,
currency: "SEK",
},
{
value: "buffe",
name: "Breakfast buffé",
payment: "Always cheeper to get it online",
pricePerNight: 150,
currency: "SEK",
},
]
const getFlexibilityMessage = (value: string) => {
switch (value) {
case "non-refundable":
return "Non refundable"
case "free-rebooking":
return "Free rebooking"
case "free-cancellation":
return "Free cancellation"
}
return undefined
}
export default async function SectionsPage({
params,
searchParams,
}: PageArgs<LangParams & { section: string }, SectionPageProps>) {
setLang(params.lang)
// TODO: Use real endpoint.
const hotel = tempHotelData.data.attributes
const rooms = await serverClient().hotel.rates.get({
// TODO: pass the correct hotel ID and all other parameters that should be included in the search
hotelId: "1",
})
const intl = await getIntl()
const selectedBed = searchParams.bed
? bedAlternatives.find((a) => a.value === searchParams.bed)?.name
: undefined
const selectedBreakfast = searchParams.breakfast
? breakfastAlternatives.find((a) => a.value === searchParams.breakfast)
?.name
: undefined
const selectedRoom = searchParams.roomClass
? rooms.find((room) => room.id.toString() === searchParams.roomClass)?.name
: undefined
const selectedFlexibility = searchParams.flexibility
? getFlexibilityMessage(searchParams.flexibility)
: undefined
const currentSearchParams = new URLSearchParams(searchParams).toString()
return (
<div>
{/* TODO: Add Hotel Listing Card */}
<div>Hotel Listing Card TBI</div>
<div className={styles.content}>
<div className={styles.main}>
<SectionAccordion
header={intl.formatMessage({ id: "Room & Terms" })}
selection={
selectedRoom
? [
selectedRoom,
intl.formatMessage({ id: selectedFlexibility }),
]
: 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
header={intl.formatMessage({ id: "Bed type" })}
selection={selectedBed}
path={`select-bed?${currentSearchParams}`}
>
{params.section === "select-bed" && (
<BedSelection
nextPath="breakfast"
alternatives={bedAlternatives}
/>
)}
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Breakfast" })}
selection={selectedBreakfast}
path={`breakfast?${currentSearchParams}`}
>
{params.section === "breakfast" && (
<BreakfastSelection
alternatives={breakfastAlternatives}
nextPath="details"
/>
)}
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Your details" })}
path={`details?${currentSearchParams}`}
>
{params.section === "details" && <Details nextPath="payment" />}
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Payment info" })}
path={`payment?${currentSearchParams}`}
>
{params.section === "payment" && <Payment />}
</SectionAccordion>
</div>
<div className={styles.summary}>
<Summary />
</div>
</div>
</div>
)
}

View File

@@ -1,3 +1,7 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
@@ -5,5 +9,8 @@ import { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
}

View File

@@ -1,19 +1,17 @@
.main {
display: grid;
grid-template-columns: repeat(2, minmax(min-content, max-content));
display: flex;
gap: var(--Spacing-x4);
padding: var(--Spacing-x4) var(--Spacing-x4) 0 var(--Spacing-x4);
height: 100dvh;
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
}
.hotelCards {
display: grid;
gap: var(--Spacing-x4);
.section {
display: flex;
flex-direction: column;
}
.link {
display: flex;
align-items: center;
padding: var(--Spacing-x2) var(--Spacing-x0);
}

View File

@@ -1,7 +1,6 @@
import { serverClient } from "@/lib/trpc/server"
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
import HotelCard from "@/components/HotelReservation/HotelCard"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
@@ -11,45 +10,98 @@ import { getLang, setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import { LangParams, PageArgs } from "@/types/params"
async function getAvailableHotels(
input: AvailabilityInput
): Promise<HotelData[]> {
const getAvailableHotels = await serverClient().hotel.availability.get(input)
if (!getAvailableHotels) throw new Error()
const { availability } = getAvailableHotels
const hotels = availability.map(async (hotel) => {
const hotelData = await serverClient().hotel.hotelData.get({
hotelId: hotel.hotelId.toString(),
language: getLang(),
})
if (!hotelData) throw new Error()
return {
hotelData: hotelData.data.attributes,
price: hotel.bestPricePerNight,
}
})
return await Promise.all(hotels)
}
export default async function SelectHotelPage({
params,
}: PageArgs<LangParams>) {
const intl = await getIntl()
setLang(params.lang)
// TODO: Use real endpoint.
const hotel = tempHotelData.data.attributes
const hotels = [hotel]
const tempSearchTerm = "Stockholm"
const intl = await getIntl()
const hotelFilters = await serverClient().hotel.filters.get({
hotelId: "879",
const hotels = await getAvailableHotels({
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
roomStayStartDate: "2024-11-02",
roomStayEndDate: "2024-11-03",
adults: 1,
})
const tempSearchTerm = "Stockholm"
const filters = hotels.flatMap((data) => data.hotelData.detailedFacilities)
const filterIds = [...new Set(filters.map((data) => data.id))]
const filterList: {
name: string
id: number
applyToAllHotels: boolean
public: boolean
icon: string
sortOrder: number
code?: string
iconName?: string
}[] = filterIds
.map((id) => filters.find((find) => find.id === id))
.filter(
(
filter
): filter is {
name: string
id: number
applyToAllHotels: boolean
public: boolean
icon: string
sortOrder: number
code?: string
iconName?: string
} => filter !== undefined
)
return (
<main className={styles.main}>
<section>
<section className={styles.section}>
<StaticMap
city={tempSearchTerm}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${tempSearchTerm} city center`}
/>
<Link className={styles.link} color="burgundy" href="#">
{intl.formatMessage({ id: "Show map" })}
<ChevronRightIcon color="burgundy" className={styles.icon} />
<ChevronRightIcon color="burgundy" />
</Link>
<HotelFilter filters={hotelFilters} />
</section>
<section className={styles.hotelCards}>
{hotels.map((hotel) => (
<HotelCard key={hotel.name} hotel={hotel} />
))}
<HotelFilter filters={filterList} />
</section>
<HotelCardListing hotelData={hotels} />
</main>
)
}

View File

@@ -1,39 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
import HotelCard from "@/components/HotelReservation/HotelCard"
import BedSelection from "@/components/HotelReservation/SelectRate/BedSelection"
import BreakfastSelection from "@/components/HotelReservation/SelectRate/BreakfastSelection"
import FlexibilitySelection from "@/components/HotelReservation/SelectRate/FlexibilitySelection"
import RoomSelection from "@/components/HotelReservation/SelectRate/RoomSelection"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
export default async function SelectRate({ params }: PageArgs<LangParams>) {
setLang(params.lang)
// TODO: Use real endpoint.
const hotel = tempHotelData.data.attributes
const rooms = await serverClient().hotel.rates.get({
// TODO: pass the correct hotel ID and all other parameters that should be included in the search
hotelId: "1",
})
return (
<div className={styles.page}>
<main className={styles.content}>
<div className={styles.hotelInfo}>
<HotelCard hotel={hotel} />
</div>
<RoomSelection rooms={rooms} />
<FlexibilitySelection />
<BreakfastSelection />
<BedSelection />
</main>
</div>
)
}