Merged in feat/SW-1890-New-Breakfast-Component (pull request #1666)
Feat/SW-1890 New Breakfast Component Design * refactor(SW-1890): Replace BreakfastChoiceCard with RadioCard component and update styles - Removed BreakfastChoiceCard component and its associated styles. - extemded RadioCard component to additional UI. - Updated breakfast.module.css to adjust container width. - Added new properties for subtitleSecondary and description in RadioCard. - Updated translations for breakfast-related messages in en.json. * feat(SW-1890): Add hover state to RadioCard * chore(SW1890): Update translation for breakfast cost message to clarify age range * chore(SW-1890): Updated breakfast cost display to use formatPrice utility * fix(SW-1890): Set fixed size for CoffeeIcon component * fix(SW-1890): Add missing translations for breakfast-related messages * feat(SW-1890): Introduce new breakfast icons and update Breakfast component - Replaced CoffeeIcon with BreakfastBuffetIcon and NoBreakfastBuffetIcon in the Breakfast component. - Added new BreakfastBuffetIcon and NoBreakfastBuffetIcon components to the design system. - Updated imports in the Breakfast component to reflect the new icons. Approved-by: Christian Andolf
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
.ancillaryChoiceCard:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
|
||||
import { AncillaryCard } from "@/components/TempDesignSystem/AncillaryCard"
|
||||
|
||||
import styles from "./ancillaryChoiceCard.module.css"
|
||||
|
||||
import type { BreakfastChoiceCardProps } from "@/types/components/ancillaryCard"
|
||||
|
||||
export default function BreakfastChoiceCard({
|
||||
name,
|
||||
id,
|
||||
value,
|
||||
ancillary,
|
||||
}: BreakfastChoiceCardProps) {
|
||||
const { register, setValue } = useFormContext()
|
||||
|
||||
function onLabelClick(event: React.MouseEvent) {
|
||||
// Preventing click event on label elements firing twice: https://github.com/facebook/react/issues/14295
|
||||
event.preventDefault()
|
||||
setValue(name, value)
|
||||
}
|
||||
return (
|
||||
<label
|
||||
onClick={onLabelClick}
|
||||
tabIndex={0}
|
||||
className={styles.ancillaryChoiceCard}
|
||||
>
|
||||
<AncillaryCard ancillary={ancillary} />
|
||||
<input
|
||||
{...register(name)}
|
||||
aria-hidden
|
||||
id={id || name}
|
||||
hidden
|
||||
type="radio"
|
||||
value={value}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -8,5 +8,5 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
width: min(600px, 100%);
|
||||
width: min(700px, 100%);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,17 @@ import { useCallback, useEffect } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
BreakfastBuffetIcon,
|
||||
NoBreakfastBuffetIcon,
|
||||
} from "@scandic-hotels/design-system/Icons"
|
||||
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
|
||||
import BreakfastChoiceCard from "@/components/HotelReservation/EnterDetails/Breakfast/BreakfastChoiceCard"
|
||||
import RadioCard from "@/components/TempDesignSystem/Form/RadioCard"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { useRoomContext } from "@/contexts/Details/Room"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import { breakfastFormSchema } from "./schema"
|
||||
|
||||
@@ -26,6 +32,9 @@ export default function Breakfast() {
|
||||
room,
|
||||
} = useRoomContext()
|
||||
|
||||
const hasChildrenInRoom = !!room.childrenInRoom?.length
|
||||
const totalPriceForNoBreakfast = 0
|
||||
|
||||
const breakfastSelection = room?.breakfast
|
||||
? room.breakfast.code
|
||||
: room?.breakfast === false
|
||||
@@ -64,7 +73,7 @@ export default function Breakfast() {
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<div className={styles.container}>
|
||||
{room.childrenInRoom?.length ? (
|
||||
{hasChildrenInRoom ? (
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "Children's breakfast is always free as part of the adult's breakfast.",
|
||||
@@ -73,42 +82,45 @@ export default function Breakfast() {
|
||||
) : null}
|
||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
{packages?.map((pkg) => (
|
||||
<BreakfastChoiceCard
|
||||
<RadioCard
|
||||
key={pkg.code}
|
||||
name="breakfast"
|
||||
ancillary={{
|
||||
title: intl.formatMessage({ id: "Breakfast buffet" }),
|
||||
price: {
|
||||
total: pkg.localPrice.price,
|
||||
currency: pkg.localPrice.currency,
|
||||
included:
|
||||
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST,
|
||||
text: intl.formatMessage({ id: "/night per adult" }),
|
||||
},
|
||||
description: intl.formatMessage({
|
||||
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||
}),
|
||||
imageUrl: "/_static/img/enter-details/breakfast.png", // TODO: Add dynamic image
|
||||
}}
|
||||
value={pkg.code}
|
||||
id={pkg.code}
|
||||
Icon={BreakfastBuffetIcon}
|
||||
title={intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
subtitle={
|
||||
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
? intl.formatMessage({ id: "Included" })
|
||||
: `+ ${formatPrice(intl, pkg.localPrice.price, pkg.localPrice.currency ?? "")}`
|
||||
}
|
||||
subtitleSecondary={intl.formatMessage({ id: "Per adult/night" })}
|
||||
description={
|
||||
hasChildrenInRoom
|
||||
? intl.formatMessage({
|
||||
id: "Free for kids aged 12 and under.",
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
descriptionSecondary={intl.formatMessage({
|
||||
id: "Includes vegan, gluten-free, and other allergy-friendly options.",
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
<BreakfastChoiceCard
|
||||
<RadioCard
|
||||
name="breakfast"
|
||||
ancillary={{
|
||||
title: intl.formatMessage({ id: "No breakfast" }),
|
||||
price: {
|
||||
total: 0,
|
||||
currency: packages?.[0].localPrice.currency ?? "",
|
||||
},
|
||||
description: intl.formatMessage({
|
||||
id: "You can always change your mind later and add breakfast at the hotel.",
|
||||
}),
|
||||
imageUrl: "/_static/img/enter-details/breakfast.png", // TODO: Add dynamic image
|
||||
imageOpacity: 0.1,
|
||||
}}
|
||||
value="false"
|
||||
Icon={NoBreakfastBuffetIcon}
|
||||
title={intl.formatMessage({ id: "No breakfast" })}
|
||||
subtitle={`+ ${formatPrice(intl, totalPriceForNoBreakfast, packages?.[0].localPrice.currency ?? "")}`}
|
||||
descriptionSecondary={
|
||||
hasChildrenInRoom
|
||||
? intl.formatMessage({
|
||||
id: "Breakfast can be added after booking for an extra cost for adults and kids ages 4 and up.",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "Breakfast can be added after booking for an additional fee.",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useFormContext } from "react-hook-form"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import styles from "./radioCard.module.css"
|
||||
|
||||
import type { RadioCardProps } from "./types"
|
||||
@@ -15,10 +17,13 @@ export default function RadioCard({
|
||||
iconHeight = 32,
|
||||
id,
|
||||
name,
|
||||
subtitle,
|
||||
title,
|
||||
subtitleSecondary,
|
||||
subtitle,
|
||||
value,
|
||||
disabled = false,
|
||||
description,
|
||||
descriptionSecondary,
|
||||
}: RadioCardProps) {
|
||||
const { register, setValue } = useFormContext()
|
||||
|
||||
@@ -52,9 +57,44 @@ export default function RadioCard({
|
||||
<Typography variant="Body/Paragraph/mdBold" className={styles.title}>
|
||||
<p>{title}</p>
|
||||
</Typography>
|
||||
{subtitleSecondary ? (
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
className={styles.subtitleSecondary}
|
||||
>
|
||||
<p>{subtitleSecondary}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography variant="Body/Paragraph/mdBold" className={styles.subtitle}>
|
||||
<p>{subtitle}</p>
|
||||
</Typography>
|
||||
|
||||
{description || descriptionSecondary ? (
|
||||
<Divider
|
||||
className={styles.divider}
|
||||
variant="horizontal"
|
||||
color="subtle"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{description ? (
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
className={styles.description}
|
||||
>
|
||||
<p>{description}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
{descriptionSecondary ? (
|
||||
<Typography
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
className={styles.descriptionSecondary}
|
||||
>
|
||||
<p>{descriptionSecondary}</p>
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<input
|
||||
{...register(name)}
|
||||
aria-hidden
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-areas: "icon icon" "title subtitle";
|
||||
grid-auto-rows: min-content;
|
||||
grid-template-areas:
|
||||
"icon subtitleSecondary"
|
||||
"title subtitle";
|
||||
border-radius: var(--Corner-radius-md);
|
||||
border: 1px solid var(--Border-Intense);
|
||||
background: var(--Surface-Primary-Default);
|
||||
padding: var(--Space-x2) var(--Space-x3);
|
||||
gap: var(--Space-x1);
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.label:hover:not(.disabled) {
|
||||
background: var(--Surface-Primary-Hover);
|
||||
}
|
||||
|
||||
.label.disabled {
|
||||
@@ -44,12 +54,34 @@
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
grid-area: subtitle;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.title {
|
||||
grid-area: title;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
grid-area: subtitle;
|
||||
color: var(--Text-Default);
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.subtitleSecondary {
|
||||
grid-area: subtitleSecondary;
|
||||
place-self: end;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.divider {
|
||||
grid-column: 1 / -1;
|
||||
margin: var(--Space-x1) 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.descriptionSecondary {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ export interface RadioCardProps
|
||||
Icon?: (props: IconProps) => JSX.Element
|
||||
iconHeight?: number
|
||||
name: string
|
||||
subtitle?: React.ReactNode
|
||||
subtitle: React.ReactNode
|
||||
subtitleSecondary?: React.ReactNode
|
||||
title: React.ReactNode
|
||||
value?: string
|
||||
disabled?: boolean
|
||||
description?: React.ReactNode
|
||||
descriptionSecondary?: React.ReactNode
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user