Merged in feat/SW-858 (pull request #1078)

feat(SW-885): ancillary and book next stay, feat(SW-865): link to booking without validation

Approved-by: Tobias Johansson
This commit is contained in:
Simon.Emanuelsson
2024-12-19 13:11:36 +00:00
18 changed files with 183 additions and 94 deletions

View File

@@ -10,14 +10,8 @@ export default async function BookingConfirmationPage({
searchParams, searchParams,
}: PageArgs<LangParams, { confirmationNumber: string }>) { }: PageArgs<LangParams, { confirmationNumber: string }>) {
setLang(params.lang) setLang(params.lang)
const bookingConfirmationPromise = getBookingConfirmation( void getBookingConfirmation(searchParams.confirmationNumber)
searchParams.confirmationNumber
)
return ( return (
<BookingConfirmation <BookingConfirmation confirmationNumber={searchParams.confirmationNumber} />
bookingConfirmationPromise={bookingConfirmationPromise}
lang={params.lang}
/>
) )
} }

View File

@@ -22,7 +22,10 @@ import SearchList from "./SearchList"
import styles from "./search.module.css" import styles from "./search.module.css"
import type { BookingWidgetSchema } from "@/types/components/bookingWidget" import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
import { ActionType } from "@/types/components/form/bookingwidget" import {
ActionType,
type SetStorageData,
} from "@/types/components/form/bookingwidget"
import type { SearchProps } from "@/types/components/search" import type { SearchProps } from "@/types/components/search"
import type { Location } from "@/types/trpc/routers/hotel/locations" import type { Location } from "@/types/trpc/routers/hotel/locations"
@@ -40,7 +43,7 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
: null : null
const [state, dispatch] = useReducer( const [state, dispatch] = useReducer(
reducer, reducer,
{ defaultLocations: locations }, { defaultLocations: locations, initialValue: value },
init init
) )
const handleMatchLocations = useCallback( const handleMatchLocations = useCallback(
@@ -122,30 +125,19 @@ export default function Search({ locations, handlePressEnter }: SearchProps) {
} }
useEffect(() => { useEffect(() => {
const searchData = const searchData = sessionStorage.getItem(sessionStorageKey)
typeof window !== "undefined" const searchHistory = localStorage.getItem(localStorageKey)
? sessionStorage.getItem(sessionStorageKey) const payload: SetStorageData["payload"] = {}
: undefined if (searchData) {
payload.searchData = JSON.parse(searchData)
const searchHistory = }
typeof window !== "undefined" if (searchHistory) {
? localStorage.getItem(localStorageKey) payload.searchHistory = JSON.parse(searchHistory)
: null }
if (searchData || searchHistory) {
dispatch({ dispatch({
payload: { payload,
searchData:
isValidJson(searchData) && searchData
? JSON.parse(searchData)
: undefined,
searchHistory:
isValidJson(searchHistory) && searchHistory
? JSON.parse(searchHistory)
: null,
},
type: ActionType.SET_STORAGE_DATA, type: ActionType.SET_STORAGE_DATA,
}) })
}
}, [dispatch]) }, [dispatch])
const stayType = state.searchData?.type === "cities" ? "city" : "hotel" const stayType = state.searchData?.type === "cities" ? "city" : "hotel"

View File

@@ -10,11 +10,20 @@ export const localStorageKey = "searchHistory"
export const sessionStorageKey = "searchData" export const sessionStorageKey = "searchData"
export function init(initState: InitState): State { export function init(initState: InitState): State {
const locations = []
if (initState.initialValue) {
const location = initState.defaultLocations.find(
(loc) => loc.name.toLowerCase() === initState.initialValue!.toLowerCase()
)
if (location) {
locations.push(location)
}
}
return { return {
defaultLocations: initState.defaultLocations, defaultLocations: initState.defaultLocations,
locations: [], locations,
search: "", search: locations.length ? locations[0].name : "",
searchData: undefined, searchData: locations.length ? locations[0] : undefined,
searchHistory: null, searchHistory: null,
} }
} }
@@ -73,8 +82,12 @@ export function reducer(state: State, action: Action) {
case ActionType.SET_STORAGE_DATA: { case ActionType.SET_STORAGE_DATA: {
return { return {
...state, ...state,
searchData: action.payload.searchData, searchData: action.payload.searchData
searchHistory: action.payload.searchHistory, ? action.payload.searchData
: state.searchData,
searchHistory: action.payload.searchHistory
? action.payload.searchHistory
: state.searchHistory,
} }
} }
default: default:

View File

@@ -1,5 +1,5 @@
"use client" "use client"
import { use, useRef } from "react" import { useRef } from "react"
import Header from "@/components/HotelReservation/BookingConfirmation/Header" import Header from "@/components/HotelReservation/BookingConfirmation/Header"
import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails" import HotelDetails from "@/components/HotelReservation/BookingConfirmation/HotelDetails"
@@ -15,13 +15,11 @@ import styles from "./confirmation.module.css"
import type { ConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation" import type { ConfirmationProps } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
export default function Confirmation({ export default function Confirmation({
bookingConfirmationPromise, booking,
hotel,
room,
}: ConfirmationProps) { }: ConfirmationProps) {
const bookingConfirmation = use(bookingConfirmationPromise)
const mainRef = useRef<HTMLElement | null>(null) const mainRef = useRef<HTMLElement | null>(null)
const { booking, hotel, room } = bookingConfirmation
return ( return (
<main className={styles.main} ref={mainRef}> <main className={styles.main} ref={mainRef}>
<Header booking={booking} hotel={hotel} mainRef={mainRef} /> <Header booking={booking} hotel={hotel} mainRef={mainRef} />
@@ -30,7 +28,11 @@ export default function Confirmation({
<PaymentDetails booking={booking} /> <PaymentDetails booking={booking} />
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
<HotelDetails hotel={hotel} /> <HotelDetails hotel={hotel} />
<Promos /> <Promos
confirmationNumber={booking.confirmationNumber}
hotelId={hotel.operaId}
lastName={booking.guest.lastName}
/>
<div className={styles.mobileReceipt}> <div className={styles.mobileReceipt}>
<Receipt booking={booking} hotel={hotel} room={room} /> <Receipt booking={booking} hotel={hotel} room={room} />
</div> </div>

View File

@@ -1,15 +1,40 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { myBooking } from "@/constants/myBooking"
import { env } from "@/env/client"
import { EditIcon } from "@/components/Icons" import { EditIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
export default function ManageBooking() { import type { ManageBookingProps } from "@/types/components/hotelReservation/bookingConfirmation/actions/manageBooking"
export default function ManageBooking({
confirmationNumber,
lastName,
}: ManageBookingProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang()
const myBookingUrl = myBooking[env.NEXT_PUBLIC_NODE_ENV][lang]
return ( return (
<Button intent="text" size="small" theme="base" variant="icon" wrapping> <Button
asChild
intent="text"
size="small"
theme="base"
variant="icon"
wrapping
>
<Link
color="none"
href={`${myBookingUrl}?bookingId=${confirmationNumber}&lastName=${lastName}`}
weight="bold"
>
<EditIcon /> <EditIcon />
{intl.formatMessage({ id: "Manage booking" })} {intl.formatMessage({ id: "Manage booking" })}
</Link>
</Button> </Button>
) )
} }

View File

@@ -70,7 +70,10 @@ export default function Header({
event={event} event={event}
hotelName={hotel.name} hotelName={hotel.name}
/> />
<ManageBooking /> <ManageBooking
confirmationNumber={booking.confirmationNumber}
lastName={booking.guest.lastName}
/>
<DownloadInvoice mainRef={mainRef} /> <DownloadInvoice mainRef={mainRef} />
</div> </div>
</header> </header>

View File

@@ -1,4 +1,5 @@
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
@@ -6,8 +7,9 @@ import styles from "./promo.module.css"
import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo" import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo"
export default function Promo({ buttonText, text, title }: PromoProps) { export default function Promo({ buttonText, href, text, title }: PromoProps) {
return ( return (
<Link className={styles.link} color="none" href={href}>
<article className={styles.promo}> <article className={styles.promo}>
<Title color="white" level="h4"> <Title color="white" level="h4">
{title} {title}
@@ -15,9 +17,10 @@ export default function Promo({ buttonText, text, title }: PromoProps) {
<Body className={styles.text} color="white" textAlign="center"> <Body className={styles.text} color="white" textAlign="center">
{text} {text}
</Body> </Body>
<Button intent="primary" size="small" theme="primaryStrong"> <Button asChild intent="primary" size="small" theme="primaryStrong">
{buttonText} <div>{buttonText}</div>
</Button> </Button>
</article> </article>
</Link>
) )
} }

View File

@@ -13,24 +13,24 @@
padding: var(--Spacing-x4) var(--Spacing-x3); padding: var(--Spacing-x4) var(--Spacing-x3);
} }
.promo:nth-of-type(1) { .link:nth-of-type(1) .promo {
background-image: linear-gradient( background-image: linear-gradient(
180deg, 180deg,
rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.36) 37.88%, rgba(0, 0, 0, 0.36) 37.88%,
rgba(0, 0, 0, 0.75) 100% rgba(0, 0, 0, 0.75) 100%
); ),
/* , url(""); uncomment and add image once we have it */ url("/_static/img/Scandic_Park_Party_Lipstick.jpg");
} }
.promo:nth-of-type(2) { .link:nth-of-type(2) .promo {
background-image: linear-gradient( background-image: linear-gradient(
180deg, 180deg,
rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.36) 37.88%, rgba(0, 0, 0, 0.36) 37.88%,
rgba(0, 0, 0, 0.75) 100% rgba(0, 0, 0, 0.75) 100%
); ),
/* , url(""); uncomment and add image once we have it */ url("/_static/img/Scandic_Family_Breakfast.jpg");
} }
.text { .text {

View File

@@ -1,16 +1,32 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { homeHrefs } from "@/constants/homeHrefs"
import { myBooking } from "@/constants/myBooking"
import { env } from "@/env/client"
import useLang from "@/hooks/useLang"
import Promo from "./Promo" import Promo from "./Promo"
import styles from "./promos.module.css" import styles from "./promos.module.css"
export default function Promos() { import type { PromosProps } from "@/types/components/hotelReservation/bookingConfirmation/promos"
export default function Promos({
confirmationNumber,
hotelId,
lastName,
}: PromosProps) {
const intl = useIntl() const intl = useIntl()
const lang = useLang()
const homeUrl = homeHrefs[env.NEXT_PUBLIC_NODE_ENV][lang]
const myBookingUrl = myBooking[env.NEXT_PUBLIC_NODE_ENV][lang]
return ( return (
<div className={styles.promos}> <div className={styles.promos}>
<Promo <Promo
buttonText={intl.formatMessage({ id: "View and buy add-ons" })} buttonText={intl.formatMessage({ id: "View and buy add-ons" })}
href={`${myBookingUrl}?bookingId=${confirmationNumber}&lastName=${lastName}`}
text={intl.formatMessage({ text={intl.formatMessage({
id: "Discover the little extra touches to make your upcoming stay even more unforgettable.", id: "Discover the little extra touches to make your upcoming stay even more unforgettable.",
})} })}
@@ -18,6 +34,7 @@ export default function Promos() {
/> />
<Promo <Promo
buttonText={intl.formatMessage({ id: "Book another stay" })} buttonText={intl.formatMessage({ id: "Book another stay" })}
href={`${homeUrl}?hotel=${hotelId}`}
text={intl.formatMessage({ text={intl.formatMessage({
id: "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.", id: "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
})} })}

View File

@@ -1,7 +1,10 @@
import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { Suspense } from "react" import { Suspense } from "react"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import TrackingSDK from "@/components/TrackingSDK" import TrackingSDK from "@/components/TrackingSDK"
import { getLang } from "@/i18n/serverContext"
import Confirmation from "./Confirmation" import Confirmation from "./Confirmation"
@@ -14,12 +17,11 @@ import {
import { CurrencyEnum } from "@/types/enums/currency" import { CurrencyEnum } from "@/types/enums/currency"
export default async function BookingConfirmation({ export default async function BookingConfirmation({
bookingConfirmationPromise, confirmationNumber,
lang,
}: BookingConfirmationProps) { }: BookingConfirmationProps) {
const bookingConfirmation = await bookingConfirmationPromise const lang = getLang()
const { booking, hotel, room } =
const { booking, hotel, room } = bookingConfirmation await getBookingConfirmation(confirmationNumber)
const arrivalDate = new Date(booking.checkInDate) const arrivalDate = new Date(booking.checkInDate)
const departureDate = new Date(booking.checkOutDate) const departureDate = new Date(booking.checkOutDate)
@@ -69,7 +71,7 @@ export default async function BookingConfirmation({
return ( return (
<> <>
<Confirmation bookingConfirmationPromise={bookingConfirmationPromise} /> <Confirmation booking={booking} hotel={hotel} room={room} />
<Suspense fallback={null}> <Suspense fallback={null}>
<TrackingSDK <TrackingSDK
pageData={initialPageTrackingData} pageData={initialPageTrackingData}

26
constants/myBooking.ts Normal file
View File

@@ -0,0 +1,26 @@
export const myBooking = {
development: {
da: "https://stage.scandichotels.dk/hotelreservation/min-booking",
de: "https://stage.scandichotels.de/hotelreservation/my-booking",
en: "https://stage.scandichotels.com/hotelreservation/my-booking",
fi: "https://stage.scandichotels.fi/varaa-hotelli/varauksesi",
no: "https://stage.scandichotels.no/hotelreservation/my-booking",
sv: "https://stage.scandichotels.se/hotelreservation/din-bokning",
},
production: {
da: "https://www.scandichotels.dk/hotelreservation/min-booking",
de: "https://www.scandichotels.de/hotelreservation/my-booking",
en: "https://www.scandichotels.com/hotelreservation/my-booking",
fi: "https://www.scandichotels.fi/varaa-hotelli/varauksesi",
no: "https://www.scandichotels.no/hotelreservation/my-booking",
sv: "https://www.scandichotels.se/hotelreservation/din-bokning",
},
test: {
da: "https://test2.scandichotels.dk/hotelreservation/min-booking",
de: "https://test2.scandichotels.de/hotelreservation/my-booking",
en: "https://test2.scandichotels.com/hotelreservation/my-booking",
fi: "https://test2.scandichotels.fi/varaa-hotelli/varauksesi",
no: "https://test2.scandichotels.no/hotelreservation/my-booking",
sv: "https://test2.scandichotels.se/hotelreservation/din-bokning",
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@@ -44,10 +44,10 @@ interface SetItemAction {
type: ActionType.SELECT_ITEM type: ActionType.SELECT_ITEM
} }
interface SetStorageData { export interface SetStorageData {
payload: { payload: {
searchData: Location searchData?: Location
searchHistory: Locations searchHistory?: Locations
} }
type: ActionType.SET_STORAGE_DATA type: ActionType.SET_STORAGE_DATA
} }
@@ -67,4 +67,6 @@ export interface State {
searchHistory: Locations | null searchHistory: Locations | null
} }
export interface InitState extends Pick<State, "defaultLocations"> {} export interface InitState extends Pick<State, "defaultLocations"> {
initialValue?: string
}

View File

@@ -0,0 +1,5 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface ManageBookingProps
extends Pick<BookingConfirmation["booking"], "confirmationNumber">,
Pick<BookingConfirmation["booking"]["guest"], "lastName"> {}

View File

@@ -1,11 +1,7 @@
import type { Lang } from "@/constants/languages" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { RouterOutput } from "@/lib/trpc/client"
export interface BookingConfirmationProps { export interface BookingConfirmationProps {
lang: Lang confirmationNumber: string
bookingConfirmationPromise: Promise<RouterOutput["booking"]["confirmation"]>
} }
export interface ConfirmationProps { export interface ConfirmationProps extends BookingConfirmation {}
bookingConfirmationPromise: Promise<RouterOutput["booking"]["confirmation"]>
}

View File

@@ -1,5 +1,6 @@
export interface PromoProps { export interface PromoProps {
buttonText: string buttonText: string
href: string
text: string text: string
title: string title: string
} }

View File

@@ -0,0 +1,8 @@
import { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface PromosProps
extends Pick<
BookingConfirmation["booking"],
"confirmationNumber" | "hotelId"
>,
Pick<BookingConfirmation["booking"]["guest"], "lastName"> {}