Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-08 15:13:16 +02:00
178 changed files with 3745 additions and 1291 deletions

View File

@@ -1,4 +1,5 @@
import { ArrowRightIcon } from "@/components/Icons"
import ManagePreferencesButton from "@/components/Profile/ManagePreferencesButton"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -27,12 +28,7 @@ export default async function CommunicationSlot({
})}
</Body>
</article>
<Link href="#" variant="icon">
<ArrowRightIcon color="burgundy" />
<Body color="burgundy" textTransform="underlined">
{formatMessage({ id: "Manage preferences" })}
</Body>
</Link>
<ManagePreferencesButton />
</section>
)
}

View File

@@ -1,4 +1,3 @@
import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import AddCreditCardButton from "@/components/Profile/AddCreditCardButton"
@@ -17,8 +16,6 @@ export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
const { formatMessage } = await getIntl()
const creditCards = await serverClient().user.creditCards()
const { lang } = params
return (
<section className={styles.container}>
<article className={styles.content}>

View File

@@ -15,8 +15,7 @@ export default function ProfileLayout({
{profile}
<Divider color="burgundy" opacity={8} />
{creditCards}
{/* TODO: Implement communication preferences flow. Hidden until decided on where to send user. */}
{/* {communication} */}
{communication}
</section>
</main>
)

View File

@@ -1,182 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import { getHotelDataSchema } from "@/server/routers/hotels/output"
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
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 = getHotelDataSchema.parse(tempHotelData)
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>
<HotelSelectionHeader hotel={hotel.data.attributes} />
<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

@@ -0,0 +1,30 @@
.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);
}
.section {
flex-grow: 1;
}
.summary {
max-width: 340px;
}
.form {
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -0,0 +1,125 @@
"use client"
import { notFound } from "next/navigation"
import { useState } from "react"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
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 SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion"
import Summary from "@/components/HotelReservation/SelectRate/Summary"
import LoadingSpinner from "@/components/LoadingSpinner"
import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params"
enum StepEnum {
selectBed = "select-bed",
breakfast = "breakfast",
details = "details",
payment = "payment",
}
function isValidStep(step: string): step is StepEnum {
return Object.values(StepEnum).includes(step as StepEnum)
}
export default function StepPage({
params,
}: PageArgs<LangParams & { step: StepEnum }>) {
const { step } = params
const [activeStep, setActiveStep] = useState<StepEnum>(step)
const intl = useIntl()
if (!isValidStep(activeStep)) {
return notFound()
}
const { data: hotel, isLoading: loadingHotel } =
trpc.hotel.hotelData.get.useQuery({
hotelId: "811",
language: params.lang,
})
if (loadingHotel) {
return <LoadingSpinner />
}
if (!hotel) {
// TODO: handle case with hotel missing
return notFound()
}
switch (activeStep) {
case StepEnum.breakfast:
//return <div>Select BREAKFAST</div>
case StepEnum.details:
//return <div>Select DETAILS</div>
case StepEnum.payment:
//return <div>Select PAYMENT</div>
case StepEnum.selectBed:
// return <div>Select BED</div>
}
function onNav(step: StepEnum) {
setActiveStep(step)
if (typeof window !== "undefined") {
window.history.pushState({}, "", step)
}
}
return (
<main className={styles.page}>
<HotelSelectionHeader hotel={hotel.data.attributes} />
<div className={styles.content}>
<section className={styles.section}>
<SectionAccordion
header="Select bed"
isCompleted={true}
isOpen={activeStep === StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })}
path="/select-bed"
>
<BedType />
</SectionAccordion>
<SectionAccordion
header="Food options"
isCompleted={true}
isOpen={activeStep === StepEnum.breakfast}
label={intl.formatMessage({ id: "Select breakfast options" })}
path="/breakfast"
>
<Breakfast />
</SectionAccordion>
<SectionAccordion
header="Details"
isCompleted={false}
isOpen={activeStep === StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })}
path="/details"
>
<Details user={null} />
</SectionAccordion>
<SectionAccordion
header="Payment"
isCompleted={false}
isOpen={activeStep === StepEnum.payment}
label={intl.formatMessage({ id: "Select payment method" })}
path="/hotelreservation/select-bed"
>
<Payment hotel={hotel.data.attributes} />
</SectionAccordion>
</section>
<aside className={styles.summary}>
<Summary />
</aside>
</div>
</main>
)
}

View File

@@ -0,0 +1,5 @@
import LoadingSpinner from "@/components/LoadingSpinner"
export default function Loading() {
return <LoadingSpinner />
}

View File

@@ -1,20 +1,67 @@
"use client"
import { useMemo } from "react"
import {
BOOKING_CONFIRMATION_NUMBER,
BookingStatusEnum,
} from "@/constants/booking"
import IntroSection from "@/components/HotelReservation/BookingConfirmation/IntroSection"
import StaySection from "@/components/HotelReservation/BookingConfirmation/StaySection"
import SummarySection from "@/components/HotelReservation/BookingConfirmation/SummarySection"
import { tempConfirmationData } from "@/components/HotelReservation/BookingConfirmation/tempConfirmationData"
import LoadingSpinner from "@/components/LoadingSpinner"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import styles from "./page.module.css"
const maxRetries = 10
const retryInterval = 2000
export default function BookingConfirmationPage() {
const { email, hotel, stay, summary } = tempConfirmationData
return (
<main className={styles.main}>
<section className={styles.section}>
<IntroSection email={email} />
<StaySection hotel={hotel} stay={stay} />
<SummarySection summary={summary} />
</section>
</main>
const confirmationNumber = useMemo(() => {
if (typeof window === "undefined") return ""
const storedConfirmationNumber = sessionStorage.getItem(
BOOKING_CONFIRMATION_NUMBER
)
// TODO: cleanup stored values
// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER)
return storedConfirmationNumber
}, [])
const bookingStatus = useHandleBookingStatus(
confirmationNumber,
BookingStatusEnum.BookingCompleted,
maxRetries,
retryInterval
)
if (
confirmationNumber === null ||
bookingStatus.isError ||
(bookingStatus.isFetched && !bookingStatus.data)
) {
// TODO: handle error
throw new Error("Error fetching booking status")
}
if (
bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted
) {
return (
<main className={styles.main}>
<section className={styles.section}>
<IntroSection email={email} />
<StaySection hotel={hotel} stay={stay} />
<SummarySection summary={summary} />
</section>
</main>
)
}
return <LoadingSpinner />
}

View File

@@ -1,3 +1,4 @@
.layout {
min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
}

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

@@ -0,0 +1,4 @@
.container {
height: 76px;
width: 100%;
}

View File

@@ -0,0 +1,11 @@
import LoadingSpinner from "@/components/LoadingSpinner"
import styles from "./loading.module.css"
export default function LoadingBookingWidget() {
return (
<div className={styles.container}>
<LoadingSpinner />
</div>
)
}