Merge branch 'develop' into feat/sw-222-staycard-link

This commit is contained in:
Linus Flood
2024-10-21 15:17:09 +02:00
65 changed files with 1729 additions and 582 deletions

View File

@@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
padding: var(--Spacing-x3) var(--Spacing-x4);
}
.column {

View File

@@ -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 {

View File

@@ -37,6 +37,7 @@
display: grid;
padding: var(--Spacing-x4) var(--Spacing-x2) 0;
gap: var(--Spacing-x4);
align-items: start;
}
.mainContent {

View File

@@ -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}

View File

@@ -397,6 +397,7 @@ export const renderOptions: RenderOptions = {
height={365}
src={image.url}
width={width}
focalPoint={image.focalPoint}
{...props}
/>
<Caption>{image.meta.caption}</Caption>

View File

@@ -1,4 +1,7 @@
import type { FocalPoint } from "@/types/components/image"
export interface HeroProps {
alt: string
src: string
focalPoint?: FocalPoint
}

View File

@@ -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}
/>
)
}

View File

@@ -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>
)
}

View File

@@ -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
}

View File

@@ -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}

View File

@@ -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;

View File

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

View File

@@ -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"

View File

@@ -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>
)
}

View File

@@ -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} />
}

View File

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

View File

@@ -396,6 +396,7 @@ export const renderOptions: RenderOptions = {
height={365}
src={image.url}
width={width}
focalPoint={image.focalPoint}
{...props}
/>
<Caption>{image.meta.caption}</Caption>

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -24,6 +24,7 @@ export default function CardImage({
alt={backgroundImage.title}
width={180}
height={180}
focalPoint={backgroundImage.focalPoint}
/>
)
)}

View File

@@ -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>
)}

View File

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

View File

@@ -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">

View File

@@ -35,6 +35,7 @@ export default function TeaserCard({
className={styles.backgroundImage}
width={399}
height={201}
focalPoint={image.focalPoint}
/>
</div>
)}

View File

@@ -2,7 +2,6 @@
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column;
max-width: 399px;
overflow: hidden;
}