Merge remote-tracking branch 'origin' into feat/tracking-payment
This commit is contained in:
@@ -10,3 +10,8 @@
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
width: min(600px, 100%);
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
|
||||
import {
|
||||
BED_TYPE_ICONS,
|
||||
type BedTypeEnum,
|
||||
type ExtraBedTypeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import { KingBedIcon } from "@/components/Icons"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||
|
||||
import BedTypeInfo from "./BedTypeInfo"
|
||||
@@ -18,6 +22,7 @@ import type {
|
||||
BedTypeFormSchema,
|
||||
BedTypeProps,
|
||||
} from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
const initialBedType = useEnterDetailsStore(
|
||||
@@ -74,8 +79,13 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
return (
|
||||
<RadioCard
|
||||
key={roomType.value}
|
||||
Icon={KingBedIcon}
|
||||
iconWidth={46}
|
||||
Icon={(props) => (
|
||||
<BedIconRenderer
|
||||
mainBedType={roomType.type}
|
||||
extraBedType={roomType.extraBed?.type}
|
||||
props={props}
|
||||
/>
|
||||
)}
|
||||
id={roomType.value}
|
||||
name="bedType"
|
||||
subtitle={width}
|
||||
@@ -89,3 +99,25 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function BedIconRenderer({
|
||||
mainBedType,
|
||||
extraBedType,
|
||||
props,
|
||||
}: {
|
||||
mainBedType: BedTypeEnum
|
||||
extraBedType: ExtraBedTypeEnum | undefined
|
||||
props: IconProps
|
||||
}) {
|
||||
const MainBedIcon = BED_TYPE_ICONS[mainBedType]
|
||||
const ExtraBedIcon = extraBedType ? BED_TYPE_ICONS[extraBedType] : null
|
||||
|
||||
return (
|
||||
<div className={`${props.className} ${styles.iconContainer}`}>
|
||||
<MainBedIcon height={32} color="uiTextMediumContrast" />
|
||||
{ExtraBedIcon && (
|
||||
<ExtraBedIcon height={32} color="uiTextMediumContrast" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import { useIntl } from "react-intl"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import { MagicWandIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import Modal from "../../Modal"
|
||||
|
||||
import styles from "./modal.module.css"
|
||||
|
||||
import type { Dispatch, SetStateAction } from "react"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
// stringMatcher regex is copied from current web as specified by requirements.
|
||||
@@ -78,7 +80,18 @@ export const joinDetailsSchema = baseDetailsSchema.merge(
|
||||
z.object({
|
||||
join: z.literal<boolean>(true),
|
||||
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
||||
dateOfBirth: z.string().min(1, { message: "Date of birth is required" }),
|
||||
dateOfBirth: z
|
||||
.string()
|
||||
.min(1, { message: "Date of birth is required" })
|
||||
.refine(
|
||||
(date) => {
|
||||
const today = dt()
|
||||
const dob = dt(date)
|
||||
const age = today.diff(dob, "year")
|
||||
return age >= 18
|
||||
},
|
||||
{ message: "Must be at least 18 years of age to continue" }
|
||||
),
|
||||
membershipNo: z.string().default(""),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { type PropsWithChildren, useEffect, useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Modal as AriaModal,
|
||||
ModalOverlay,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import {
|
||||
type AnimationState,
|
||||
AnimationStateEnum,
|
||||
type InnerModalProps,
|
||||
type ModalProps,
|
||||
} from "./modal"
|
||||
import { fade, slideInOut } from "./motionVariants"
|
||||
|
||||
import styles from "./modal.module.css"
|
||||
|
||||
const MotionOverlay = motion(ModalOverlay)
|
||||
const MotionModal = motion(AriaModal)
|
||||
|
||||
function InnerModal({
|
||||
animation,
|
||||
onAnimationComplete = () => undefined,
|
||||
setAnimation,
|
||||
onToggle,
|
||||
isOpen,
|
||||
children,
|
||||
title,
|
||||
}: PropsWithChildren<InnerModalProps>) {
|
||||
const intl = useIntl()
|
||||
function modalStateHandler(newAnimationState: AnimationState) {
|
||||
setAnimation((currentAnimationState) =>
|
||||
newAnimationState === AnimationStateEnum.hidden &&
|
||||
currentAnimationState === AnimationStateEnum.hidden
|
||||
? AnimationStateEnum.unmounted
|
||||
: currentAnimationState
|
||||
)
|
||||
if (newAnimationState === AnimationStateEnum.visible) {
|
||||
onAnimationComplete()
|
||||
}
|
||||
}
|
||||
|
||||
function onOpenChange(state: boolean) {
|
||||
onToggle!(state)
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionOverlay
|
||||
animate={animation}
|
||||
className={styles.overlay}
|
||||
initial={"hidden"}
|
||||
isDismissable
|
||||
isExiting={animation === AnimationStateEnum.hidden}
|
||||
onAnimationComplete={modalStateHandler}
|
||||
variants={fade}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
variants={slideInOut}
|
||||
animate={animation}
|
||||
initial={"hidden"}
|
||||
>
|
||||
<Dialog className={styles.dialog} aria-label="Dialog">
|
||||
{({ close }) => (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
{title && (
|
||||
<Subtitle type="one" color="uiTextHighContrast">
|
||||
{title}
|
||||
</Subtitle>
|
||||
)}
|
||||
|
||||
<button onClick={close} type="button" className={styles.close}>
|
||||
<CloseLargeIcon color="uiTextMediumContrast" />
|
||||
</button>
|
||||
</header>
|
||||
<section className={styles.content}>{children}</section>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
</MotionOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
onAnimationComplete = () => undefined,
|
||||
trigger,
|
||||
isOpen,
|
||||
onToggle,
|
||||
title,
|
||||
children,
|
||||
}: PropsWithChildren<ModalProps>) {
|
||||
const [animation, setAnimation] = useState<AnimationState>(
|
||||
AnimationStateEnum.visible
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof isOpen === "boolean") {
|
||||
setAnimation(
|
||||
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
||||
)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
if (!trigger) {
|
||||
return (
|
||||
<InnerModal
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
animation={animation}
|
||||
setAnimation={setAnimation}
|
||||
onToggle={onToggle}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</InnerModal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogTrigger
|
||||
onOpenChange={(isOpen) =>
|
||||
setAnimation(
|
||||
isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden
|
||||
)
|
||||
}
|
||||
>
|
||||
{trigger}
|
||||
<InnerModal
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
animation={animation}
|
||||
setAnimation={setAnimation}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</InnerModal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: var(--default-modal-overlay-z-index);
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium) var(--Corner-radius-Medium) 0 0;
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--default-modal-z-index);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* for supporting animations within content */
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
--button-dimension: 32px;
|
||||
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--button-dimension);
|
||||
position: relative;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x3) var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: var(--Spacing-x2);
|
||||
width: var(--button-dimension);
|
||||
height: var(--button-dimension);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: auto;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Dispatch, SetStateAction } from "react"
|
||||
|
||||
export enum AnimationStateEnum {
|
||||
unmounted = "unmounted",
|
||||
hidden = "hidden",
|
||||
visible = "visible",
|
||||
}
|
||||
|
||||
export type AnimationState = keyof typeof AnimationStateEnum
|
||||
|
||||
export type ModalProps = {
|
||||
onAnimationComplete?: VoidFunction
|
||||
title?: string
|
||||
} & (
|
||||
| { trigger: JSX.Element; isOpen?: never; onToggle?: never }
|
||||
| {
|
||||
trigger?: never
|
||||
isOpen: boolean
|
||||
onToggle: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
)
|
||||
|
||||
export type InnerModalProps = Omit<ModalProps, "trigger"> & {
|
||||
animation: AnimationState
|
||||
setAnimation: Dispatch<SetStateAction<AnimationState>>
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
export const fade = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
}
|
||||
|
||||
export const slideInOut = {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 32,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
}
|
||||
@@ -8,19 +8,19 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
CheckIcon,
|
||||
ChevronDownSmallIcon,
|
||||
ChevronRightSmallIcon,
|
||||
} from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
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 { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import Modal from "../../Modal"
|
||||
import PriceDetailsTable from "../PriceDetailsTable"
|
||||
|
||||
import styles from "./ui.module.css"
|
||||
@@ -51,6 +51,7 @@ export default function SummaryUI({
|
||||
isMember,
|
||||
rateDetails,
|
||||
roomType,
|
||||
breakfastIncluded,
|
||||
}: SummaryProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
@@ -169,23 +170,34 @@ export default function SummaryUI({
|
||||
}`}
|
||||
</Caption>
|
||||
<Caption color="uiTextMediumContrast">{cancellationText}</Caption>
|
||||
<Popover
|
||||
placement="bottom left"
|
||||
triggerContent={
|
||||
<Caption color="burgundy" type="underline">
|
||||
{intl.formatMessage({ id: "Rate details" })}
|
||||
</Caption>
|
||||
<Modal
|
||||
trigger={
|
||||
<Button intent="text">
|
||||
<Caption color="burgundy" type="underline">
|
||||
{intl.formatMessage({ id: "Rate details" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
}
|
||||
title={cancellationText}
|
||||
>
|
||||
<aside className={styles.rateDetailsPopover}>
|
||||
<header>
|
||||
<Caption type="bold">{cancellationText}</Caption>
|
||||
</header>
|
||||
{rateDetails?.map((detail, idx) => (
|
||||
<Caption key={`rateDetails-${idx}`}>{detail}</Caption>
|
||||
<div className={styles.terms}>
|
||||
{rateDetails?.map((info) => (
|
||||
<Body
|
||||
key={info}
|
||||
color="uiTextHighContrast"
|
||||
className={styles.termsText}
|
||||
>
|
||||
<CheckIcon
|
||||
color="uiSemanticSuccess"
|
||||
width={20}
|
||||
height={20}
|
||||
className={styles.termsIcon}
|
||||
></CheckIcon>
|
||||
{info}
|
||||
</Body>
|
||||
))}
|
||||
</aside>
|
||||
</Popover>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
{packages
|
||||
? packages.map((roomPackage) => (
|
||||
@@ -242,7 +254,13 @@ export default function SummaryUI({
|
||||
</Body>
|
||||
</div>
|
||||
) : null}
|
||||
{breakfast === false ? (
|
||||
{breakfastIncluded ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast included" })}
|
||||
</Body>
|
||||
</div>
|
||||
) : breakfast === false ? (
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "No breakfast" })}
|
||||
|
||||
@@ -72,6 +72,19 @@
|
||||
width: 560px;
|
||||
}
|
||||
|
||||
.terms {
|
||||
margin-top: var(--Spacing-x3);
|
||||
margin-bottom: var(--Spacing-x3);
|
||||
}
|
||||
.termsText:nth-child(n) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--Spacing-x1);
|
||||
}
|
||||
.terms .termsIcon {
|
||||
margin-right: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.bottomDivider {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user