feat: merge stores, fix auto navigation, split summary
This commit is contained in:
@@ -1,325 +1,57 @@
|
||||
"use client"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ChevronDown } from "react-feather"
|
||||
import { useIntl } from "react-intl"
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
import {
|
||||
getProfileSafely,
|
||||
getSelectedRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { useDetailsStore } from "@/stores/details"
|
||||
import { generateChildrenString } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { ArrowRightIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Popover from "@/components/TempDesignSystem/Popover"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import ClientSummary from "./Client"
|
||||
|
||||
import styles from "./summary.module.css"
|
||||
import type { SummaryPageProps } from "@/types/components/hotelReservation/summary"
|
||||
|
||||
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||
import type { SummaryProps } from "@/types/components/hotelReservation/enterDetails/summary"
|
||||
import type { DetailsState } from "@/types/stores/details"
|
||||
export default async function Summary({
|
||||
adults,
|
||||
fromDate,
|
||||
hotelId,
|
||||
kids,
|
||||
packageCodes,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
toDate,
|
||||
}: SummaryPageProps) {
|
||||
const lang = getLang()
|
||||
|
||||
function storeSelector(state: DetailsState) {
|
||||
return {
|
||||
fromDate: state.data.booking.fromDate,
|
||||
toDate: state.data.booking.toDate,
|
||||
bedType: state.data.bedType,
|
||||
breakfast: state.data.breakfast,
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
setTotalPrice: state.actions.setTotalPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
join: state.data.join,
|
||||
membershipNo: state.data.membershipNo,
|
||||
const availability = await getSelectedRoomAvailability({
|
||||
adults,
|
||||
children: kids ? generateChildrenString(kids) : undefined,
|
||||
hotelId,
|
||||
packageCodes,
|
||||
rateCode,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
roomTypeCode,
|
||||
})
|
||||
const user = await getProfileSafely()
|
||||
|
||||
if (!availability || !availability.selectedRoom) {
|
||||
console.error("No hotel or availability data", availability)
|
||||
// TODO: handle this case
|
||||
redirect(selectRate(lang))
|
||||
}
|
||||
}
|
||||
|
||||
export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
const [chosenBed, setChosenBed] = useState<BedTypeSchema>()
|
||||
const [chosenBreakfast, setChosenBreakfast] = useState<
|
||||
BreakfastPackage | false
|
||||
>()
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const {
|
||||
bedType,
|
||||
breakfast,
|
||||
fromDate,
|
||||
setTotalPrice,
|
||||
toDate,
|
||||
toggleSummaryOpen,
|
||||
totalPrice,
|
||||
join,
|
||||
membershipNo,
|
||||
} = useDetailsStore(storeSelector)
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
|
||||
const nights = intl.formatMessage(
|
||||
{ id: "booking.nights" },
|
||||
{ totalNights: diff }
|
||||
)
|
||||
|
||||
const color = useRef<"uiTextHighContrast" | "red">("uiTextHighContrast")
|
||||
const [price, setPrice] = useState(room.prices.public)
|
||||
|
||||
const additionalPackageCost = room.packages?.reduce(
|
||||
(acc, curr) => {
|
||||
acc.local = acc.local + parseInt(curr.localPrice.totalPrice)
|
||||
acc.euro = acc.euro + parseInt(curr.requestedPrice.totalPrice)
|
||||
return acc
|
||||
},
|
||||
{ local: 0, euro: 0 }
|
||||
) || { local: 0, euro: 0 }
|
||||
|
||||
const roomsPriceLocal = price.local.amount + additionalPackageCost.local
|
||||
const roomsPriceEuro = price.euro
|
||||
? price.euro.amount + additionalPackageCost.euro
|
||||
: undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (showMemberPrice || join || membershipNo) {
|
||||
color.current = "red"
|
||||
if (room.prices.member) {
|
||||
setPrice(room.prices.member)
|
||||
}
|
||||
} else {
|
||||
color.current = "uiTextHighContrast"
|
||||
setPrice(room.prices.public)
|
||||
}
|
||||
}, [showMemberPrice, join, membershipNo, room.prices])
|
||||
|
||||
useEffect(() => {
|
||||
setChosenBed(bedType)
|
||||
|
||||
if (breakfast || breakfast === false) {
|
||||
setChosenBreakfast(breakfast)
|
||||
if (breakfast === false) {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
amount: roomsPriceLocal,
|
||||
currency: price.local.currency,
|
||||
},
|
||||
euro:
|
||||
price.euro && roomsPriceEuro
|
||||
? {
|
||||
amount: roomsPriceEuro,
|
||||
currency: price.euro.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
amount: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: price.local.currency,
|
||||
},
|
||||
euro:
|
||||
price.euro && roomsPriceEuro
|
||||
? {
|
||||
amount:
|
||||
roomsPriceEuro +
|
||||
parseInt(breakfast.requestedPrice.totalPrice),
|
||||
currency: price.euro.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [
|
||||
bedType,
|
||||
breakfast,
|
||||
roomsPriceLocal,
|
||||
price.local.currency,
|
||||
price.euro,
|
||||
roomsPriceEuro,
|
||||
setTotalPrice,
|
||||
])
|
||||
|
||||
return (
|
||||
<section className={styles.summary}>
|
||||
<header className={styles.header}>
|
||||
<Subtitle className={styles.title} type="two">
|
||||
{intl.formatMessage({ id: "Summary" })}
|
||||
</Subtitle>
|
||||
<Body className={styles.date} color="baseTextMediumContrast">
|
||||
{dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||
<ArrowRightIcon color="peach80" height={15} width={15} />
|
||||
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||
</Body>
|
||||
<Button
|
||||
intent="text"
|
||||
size="small"
|
||||
className={styles.chevronButton}
|
||||
onClick={toggleSummaryOpen}
|
||||
>
|
||||
<ChevronDown height="20" width="20" />
|
||||
</Button>
|
||||
</header>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.addOns}>
|
||||
<div>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||
<Caption color={color.current}>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(price.local.amount),
|
||||
currency: price.local.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.adults" },
|
||||
{ totalAdults: room.adults }
|
||||
)}
|
||||
</Caption>
|
||||
{room.children?.length ? (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "booking.children" },
|
||||
{ totalChildren: room.children.length }
|
||||
)}
|
||||
</Caption>
|
||||
) : null}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{room.cancellationText}
|
||||
</Caption>
|
||||
<Popover
|
||||
placement="bottom left"
|
||||
triggerContent={
|
||||
<Caption color="burgundy" type="underline">
|
||||
{intl.formatMessage({ id: "Rate details" })}
|
||||
</Caption>
|
||||
}
|
||||
>
|
||||
<aside className={styles.rateDetailsPopover}>
|
||||
<header>
|
||||
<Caption type="bold">{room.cancellationText}</Caption>
|
||||
</header>
|
||||
{room.rateDetails?.map((detail, idx) => (
|
||||
<Caption key={`rateDetails-${idx}`}>{detail}</Caption>
|
||||
))}
|
||||
</aside>
|
||||
</Popover>
|
||||
</div>
|
||||
{room.packages
|
||||
? room.packages.map((roomPackage) => (
|
||||
<div className={styles.entry} key={roomPackage.code}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">
|
||||
{roomPackage.description}
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: roomPackage.localPrice.price,
|
||||
currency: roomPackage.localPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
{chosenBed ? (
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body color="uiTextHighContrast">{chosenBed.description}</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Based on availability" })}
|
||||
</Caption>
|
||||
</div>
|
||||
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: price.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{chosenBreakfast === false ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No breakfast" })}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: price.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
) : chosenBreakfast?.code ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
</Body>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: chosenBreakfast.localPrice.totalPrice,
|
||||
currency: chosenBreakfast.localPrice.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.total}>
|
||||
<div className={styles.entry}>
|
||||
<div>
|
||||
<Body>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{ id: "<b>Total price</b> (incl VAT)" },
|
||||
{ b: (str) => <b>{str}</b> }
|
||||
)}
|
||||
</Body>
|
||||
<Link color="burgundy" href="#" variant="underscored" size="small">
|
||||
{intl.formatMessage({ id: "Price details" })}
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{totalPrice.local.amount > 0 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.amount),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
)}
|
||||
{totalPrice.euro && totalPrice.euro.amount > 0 && (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.euro.amount),
|
||||
currency: totalPrice.euro.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
|
||||
</div>
|
||||
</section>
|
||||
<ClientSummary
|
||||
adults={adults}
|
||||
cancellationText={availability.cancellationText}
|
||||
isMember={!!user}
|
||||
kids={kids}
|
||||
memberRate={availability.memberRate}
|
||||
rateDetails={availability.rateDetails}
|
||||
roomType={availability.selectedRoom.roomType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user