Merge branch 'develop' into feat/sw-222-staycard-link
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.column {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x4);
|
||||
}
|
||||
@media screen and (min-width: 767px) {
|
||||
.grid {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
display: grid;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2) 0;
|
||||
gap: var(--Spacing-x4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
|
||||
@@ -44,6 +44,7 @@ export default async function ContentPage() {
|
||||
<Hero
|
||||
alt={hero_image.meta.alt || hero_image.meta.caption || ""}
|
||||
src={hero_image.url}
|
||||
focalPoint={hero_image.focalPoint}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -397,6 +397,7 @@ export const renderOptions: RenderOptions = {
|
||||
height={365}
|
||||
src={image.url}
|
||||
width={width}
|
||||
focalPoint={image.focalPoint}
|
||||
{...props}
|
||||
/>
|
||||
<Caption>{image.meta.caption}</Caption>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { FocalPoint } from "@/types/components/image"
|
||||
|
||||
export interface HeroProps {
|
||||
alt: string
|
||||
src: string
|
||||
focalPoint?: FocalPoint
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HeroProps } from "./hero"
|
||||
|
||||
import styles from "./hero.module.css"
|
||||
|
||||
export default async function Hero({ alt, src }: HeroProps) {
|
||||
export default async function Hero({ alt, src, focalPoint }: HeroProps) {
|
||||
return (
|
||||
<Image
|
||||
className={styles.hero}
|
||||
@@ -12,6 +12,7 @@ export default async function Hero({ alt, src }: HeroProps) {
|
||||
height={480}
|
||||
width={1196}
|
||||
src={src}
|
||||
focalPoint={focalPoint}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import Image from "next/image"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
|
||||
import { PAYMENT_METHOD_ICONS, PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import { PaymentOptionProps } from "./paymentOption"
|
||||
|
||||
import styles from "./paymentOption.module.css"
|
||||
|
||||
export default function PaymentOption({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
cardNumber,
|
||||
registerOptions = {},
|
||||
}: PaymentOptionProps) {
|
||||
const { register } = useFormContext()
|
||||
|
||||
return (
|
||||
<label key={value} className={styles.paymentOption}>
|
||||
<div className={styles.titleContainer}>
|
||||
<input
|
||||
aria-hidden
|
||||
hidden
|
||||
type="radio"
|
||||
id={value}
|
||||
value={value}
|
||||
{...register(name, registerOptions)}
|
||||
/>
|
||||
<span className={styles.radio} />
|
||||
<Body>{label}</Body>
|
||||
</div>
|
||||
{cardNumber ? (
|
||||
<Caption color="uiTextMediumContrast">•••• {cardNumber}</Caption>
|
||||
) : (
|
||||
<Image
|
||||
className={styles.paymentOptionIcon}
|
||||
src={PAYMENT_METHOD_ICONS[value as PaymentMethodEnum]}
|
||||
alt={label}
|
||||
width={48}
|
||||
height={32}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RegisterOptions } from "react-hook-form"
|
||||
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
export interface PaymentOptionProps {
|
||||
name: string
|
||||
value: PaymentMethodEnum
|
||||
value: string
|
||||
label: string
|
||||
cardNumber?: string
|
||||
registerOptions?: RegisterOptions
|
||||
onChange?: () => void
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Checkbox from "@/components/TempDesignSystem/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||
@@ -38,7 +39,15 @@ import { PaymentProps } from "@/types/components/hotelReservation/selectRate/sec
|
||||
const maxRetries = 40
|
||||
const retryInterval = 2000
|
||||
|
||||
export default function Payment({ hotel }: PaymentProps) {
|
||||
function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum)
|
||||
}
|
||||
|
||||
export default function Payment({
|
||||
hotelId,
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
}: PaymentProps) {
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
@@ -46,7 +55,9 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
|
||||
const methods = useForm<PaymentFormData>({
|
||||
defaultValues: {
|
||||
paymentMethod: PaymentMethodEnum.card,
|
||||
paymentMethod: savedCreditCards?.length
|
||||
? savedCreditCards[0].id
|
||||
: PaymentMethodEnum.card,
|
||||
smsConfirmation: false,
|
||||
termsAndConditions: false,
|
||||
},
|
||||
@@ -87,8 +98,17 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
}, [confirmationNumber, bookingStatus, router])
|
||||
|
||||
function handleSubmit(data: PaymentFormData) {
|
||||
// set payment method to card if saved card is submitted
|
||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||
? data.paymentMethod
|
||||
: PaymentMethodEnum.card
|
||||
|
||||
const savedCreditCard = savedCreditCards?.find(
|
||||
(card) => card.id === data.paymentMethod
|
||||
)
|
||||
|
||||
initiateBooking.mutate({
|
||||
hotelId: hotel.operaId,
|
||||
hotelId: hotelId,
|
||||
checkInDate: "2024-12-10",
|
||||
checkOutDate: "2024-12-11",
|
||||
rooms: [
|
||||
@@ -116,7 +136,14 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
},
|
||||
],
|
||||
payment: {
|
||||
paymentMethod: data.paymentMethod,
|
||||
paymentMethod,
|
||||
card: savedCreditCard
|
||||
? {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
}
|
||||
: undefined,
|
||||
cardHolder: {
|
||||
email: "test.user@scandichotels.com",
|
||||
name: "Test User",
|
||||
@@ -143,65 +170,94 @@ export default function Payment({ hotel }: PaymentProps) {
|
||||
className={styles.paymentContainer}
|
||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
<PaymentOption
|
||||
name="paymentMethod"
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
/>
|
||||
{hotel.merchantInformationData.alternatePaymentOptions.map(
|
||||
(paymentMethod) => (
|
||||
{savedCreditCards?.length ? (
|
||||
<section className={styles.section}>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "MY SAVED CARDS" })}
|
||||
</Body>
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
{savedCreditCards?.map((savedCreditCard) => (
|
||||
<PaymentOption
|
||||
key={savedCreditCard.id}
|
||||
name="paymentMethod"
|
||||
value={savedCreditCard.id}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[
|
||||
savedCreditCard.cardType as PaymentMethodEnum
|
||||
]
|
||||
}
|
||||
cardNumber={savedCreditCard.truncatedNumber}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
<section className={styles.section}>
|
||||
{savedCreditCards?.length ? (
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "OTHER PAYMENT METHODS" })}
|
||||
</Body>
|
||||
) : null}
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
<PaymentOption
|
||||
name="paymentMethod"
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
/>
|
||||
{otherPaymentOptions.map((paymentMethod) => (
|
||||
<PaymentOption
|
||||
key={paymentMethod}
|
||||
name="paymentMethod"
|
||||
value={paymentMethod as PaymentMethodEnum}
|
||||
value={paymentMethod}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[paymentMethod as PaymentMethodEnum]
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className={styles.section}>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
|
||||
<AriaLabel className={styles.terms}>
|
||||
<Checkbox name="termsAndConditions" />
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "booking.terms",
|
||||
},
|
||||
{
|
||||
termsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</AriaLabel>
|
||||
<AriaLabel className={styles.terms}>
|
||||
<Checkbox name="termsAndConditions" />
|
||||
<Caption>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "booking.terms",
|
||||
},
|
||||
{
|
||||
termsLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={bookingTermsAndConditions[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyLink: (str) => (
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="underscored"
|
||||
href={privacyPolicy[lang]}
|
||||
target="_blank"
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</AriaLabel>
|
||||
</section>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.submitButton}
|
||||
@@ -1,10 +1,16 @@
|
||||
.paymentContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
gap: var(--Spacing-x4);
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.paymentOptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1,9 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.nativeEnum(PaymentMethodEnum),
|
||||
paymentMethod: z.string(),
|
||||
smsConfirmation: z.boolean(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import Image from "next/image"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
|
||||
import { PAYMENT_METHOD_ICONS } from "@/constants/booking"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import { PaymentOptionProps } from "./paymentOption"
|
||||
|
||||
import styles from "./paymentOption.module.css"
|
||||
|
||||
export default function PaymentOption({
|
||||
name,
|
||||
value,
|
||||
label,
|
||||
}: PaymentOptionProps) {
|
||||
const { register } = useFormContext()
|
||||
return (
|
||||
<label key={value} className={styles.paymentOption} htmlFor={value}>
|
||||
<div className={styles.titleContainer}>
|
||||
<input
|
||||
aria-hidden
|
||||
hidden
|
||||
type="radio"
|
||||
id={value}
|
||||
value={value}
|
||||
{...register(name)}
|
||||
/>
|
||||
<span className={styles.radio} />
|
||||
<Body asChild>
|
||||
<label htmlFor={value}>{label}</label>
|
||||
</Body>
|
||||
</div>
|
||||
<Image
|
||||
className={styles.paymentOptionIcon}
|
||||
src={PAYMENT_METHOD_ICONS[value]}
|
||||
alt={label}
|
||||
width={48}
|
||||
height={32}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import NextImage from "next/image"
|
||||
|
||||
import type { ImageLoaderProps, ImageProps } from "next/image"
|
||||
import type { ImageLoaderProps } from "next/image"
|
||||
import type { CSSProperties } from "react"
|
||||
|
||||
import type { ImageProps } from "@/types/components/image"
|
||||
|
||||
function imageLoader({ quality, src, width }: ImageLoaderProps) {
|
||||
const hasQS = src.indexOf("?") !== -1
|
||||
@@ -10,6 +13,14 @@ function imageLoader({ quality, src, width }: ImageLoaderProps) {
|
||||
}
|
||||
|
||||
// Next/Image adds & instead of ? before the params
|
||||
export default function Image(props: ImageProps) {
|
||||
return <NextImage {...props} loader={imageLoader} />
|
||||
export default function Image({ focalPoint, style, ...props }: ImageProps) {
|
||||
const styles: CSSProperties = focalPoint
|
||||
? {
|
||||
objectFit: "cover",
|
||||
objectPosition: `${focalPoint.x}% ${focalPoint.y}%`,
|
||||
...style,
|
||||
}
|
||||
: { ...style }
|
||||
|
||||
return <NextImage {...props} style={styles} loader={imageLoader} />
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export default function ImageContainer({
|
||||
height={365}
|
||||
width={600}
|
||||
alt={leftImage.meta.alt || leftImage.title}
|
||||
focalPoint={leftImage.focalPoint}
|
||||
/>
|
||||
<Caption>{leftImage.meta.caption}</Caption>
|
||||
</article>
|
||||
@@ -28,6 +29,7 @@ export default function ImageContainer({
|
||||
height={365}
|
||||
width={600}
|
||||
alt={rightImage.meta.alt || rightImage.title}
|
||||
focalPoint={rightImage.focalPoint}
|
||||
/>
|
||||
<Caption>{leftImage.meta.caption}</Caption>
|
||||
</article>
|
||||
|
||||
@@ -396,6 +396,7 @@ export const renderOptions: RenderOptions = {
|
||||
height={365}
|
||||
src={image.url}
|
||||
width={width}
|
||||
focalPoint={image.focalPoint}
|
||||
{...props}
|
||||
/>
|
||||
<Caption>{image.meta.caption}</Caption>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import JsonToHtml from "@/components/JsonToHtml"
|
||||
|
||||
import ShortcutsList from "../Blocks/ShortcutsList"
|
||||
import Card from "../TempDesignSystem/Card"
|
||||
import TeaserCard from "../TempDesignSystem/TeaserCard"
|
||||
import JoinLoyaltyContact from "./JoinLoyalty"
|
||||
import MyPagesNavigation from "./MyPagesNavigation"
|
||||
|
||||
@@ -40,6 +43,35 @@ export default function Sidebar({ blocks }: SidebarProps) {
|
||||
key={`${block.typename}-${idx}`}
|
||||
/>
|
||||
)
|
||||
case SidebarEnums.blocks.ScriptedCard:
|
||||
return (
|
||||
<Card
|
||||
key={block.scripted_card.system.uid}
|
||||
heading={block.scripted_card.heading}
|
||||
secondaryButton={block.scripted_card.secondaryButton}
|
||||
primaryButton={block.scripted_card.primaryButton}
|
||||
bodyText={block.scripted_card.body_text}
|
||||
scriptedTopTitle={block.scripted_card.scripted_top_title}
|
||||
theme={block.scripted_card.theme ?? "image"}
|
||||
/>
|
||||
)
|
||||
case SidebarEnums.blocks.TeaserCard:
|
||||
return (
|
||||
<TeaserCard
|
||||
title={block.teaser_card.heading}
|
||||
description={block.teaser_card.body_text}
|
||||
style={block.teaser_card.theme}
|
||||
key={block.teaser_card.system.uid}
|
||||
primaryButton={block.teaser_card.primaryButton}
|
||||
secondaryButton={block.teaser_card.secondaryButton}
|
||||
sidePeekButton={block.teaser_card.sidePeekButton}
|
||||
sidePeekContent={block.teaser_card.sidePeekContent}
|
||||
image={block.teaser_card.image}
|
||||
/>
|
||||
)
|
||||
case SidebarEnums.blocks.QuickLinks:
|
||||
return <ShortcutsList {...block.shortcuts} hasTwoColumns={false} />
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
display: grid;
|
||||
container-name: sidebar;
|
||||
container-type: inline-size;
|
||||
gap: var(--Spacing-x3);
|
||||
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
padding-top: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -11,8 +15,10 @@
|
||||
@media screen and (min-width: 1367px) {
|
||||
.aside {
|
||||
align-content: flex-start;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function CardImage({
|
||||
alt={backgroundImage.title}
|
||||
width={180}
|
||||
height={180}
|
||||
focalPoint={backgroundImage.focalPoint}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function Card({
|
||||
|
||||
imageWidth =
|
||||
imageWidth ||
|
||||
(backgroundImage && "dimensions" in backgroundImage
|
||||
(backgroundImage?.dimensions
|
||||
? backgroundImage.dimensions.aspectRatio * imageHeight
|
||||
: 420)
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function Card({
|
||||
alt={backgroundImage.meta.alt || backgroundImage.title}
|
||||
width={imageWidth}
|
||||
height={imageHeight}
|
||||
focalPoint={backgroundImage.focalPoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Card({
|
||||
const { register } = useFormContext()
|
||||
|
||||
return (
|
||||
<label className={styles.label} data-declined={declined}>
|
||||
<label className={styles.label} data-declined={declined} tabIndex={0}>
|
||||
<Caption className={styles.title} type="label" uppercase>
|
||||
{title}
|
||||
</Caption>
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function LoyaltyCard({
|
||||
height={160}
|
||||
className={styles.image}
|
||||
alt={image.meta.alt || image.title}
|
||||
focalPoint={image.focalPoint}
|
||||
/>
|
||||
) : null}
|
||||
<Title as="h5" level="h3" textAlign="center">
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function TeaserCard({
|
||||
className={styles.backgroundImage}
|
||||
width={399}
|
||||
height={201}
|
||||
focalPoint={image.focalPoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 399px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user