diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx index 38bcfd49c..2cda658a2 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Campaign.tsx @@ -60,6 +60,24 @@ export default function Campaign({ ) } + const rateTermDetails = product.rateDefinitionMember + ? [ + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + { + title: product.rateDefinitionMember.title, + terms: product.rateDefinition.generalTerms, + }, + ] + : [ + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + ] + const isSelected = isSelectedPriceProduct( product, selectedRate, @@ -149,6 +167,7 @@ export default function Campaign({ } : undefined } + rateTermDetails={rateTermDetails} value={product.public.rateCode} /> ) diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx index 80cf9e7b9..e77c154dd 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Code.tsx @@ -43,6 +43,23 @@ export default function Code({ } else { bannerText = `${bookingCode} ∙ ${intl.formatMessage({ id: "Breakfast excluded" })}` } + const rateTermDetails = product.rateDefinitionMember + ? [ + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + { + title: product.rateDefinitionMember.title, + terms: product.rateDefinition.generalTerms, + }, + ] + : [ + { + title: product.rateDefinition.title, + terms: product.rateDefinition.generalTerms, + }, + ] if ("corporateCheque" in product) { const { localPrice, rateCode } = product.corporateCheque @@ -71,6 +88,7 @@ export default function Code({ unit: localPrice.currency ?? "", }} rateTitle={rateTitles[product.rate].title} + rateTermDetails={rateTermDetails} value={rateCode} /> ) @@ -93,6 +111,7 @@ export default function Code({ unit: intl.formatMessage({ id: "Voucher" }).toUpperCase(), }} rateTitle={rateTitles[product.rate].title} + rateTermDetails={rateTermDetails} value={rateCode} /> ) @@ -151,6 +170,7 @@ export default function Code({ unit: `${localPrice.currency}/${night}`, }} rateTitle={rateTitles[product.rate].title} + rateTermDetails={rateTermDetails} value={rateCode} /> ) diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx index 5c9cfb212..3b1ae8c0a 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/Redemptions.tsx @@ -76,6 +76,13 @@ export default function Redemptions({ ? `${rewardNight} ∙ ${breakfastIncluded}` : `${rewardNight} ∙ ${breakfastExcluded}` + const rateTermDetails = [ + { + title: rateTitles[firstRedemption.rate].title, + terms: firstRedemption.rateDefinition.generalTerms, + }, + ] + return ( ) }) diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx index a379e6c76..2f99c54d9 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomsList/RoomListItem/Rates/index.tsx @@ -57,7 +57,6 @@ export default function Rates({ petRoomPackageSelected && petRoomPackage ? petRoomPackage : undefined, roomTypeCode, } - const showAllRates = selectedFilter === BookingCodeFilterEnum.All const hasBookingCodeRates = !!(campaign.length || code.length) const hasRegularRates = !!regular.length diff --git a/apps/scandic-web/server/routers/hotels/output.ts b/apps/scandic-web/server/routers/hotels/output.ts index 75bd3c625..c922e302f 100644 --- a/apps/scandic-web/server/routers/hotels/output.ts +++ b/apps/scandic-web/server/routers/hotels/output.ts @@ -338,12 +338,15 @@ export const roomsAvailabilitySchema = z ...product, public: null, }) - if (rateDetailsMember) { - breakfastIncludedMember.push( - rateDetailsMember.breakfastIncluded - ) - } + if (rateDetails && rateCode) { + if (rateDetailsMember) { + breakfastIncludedMember.push( + rateDetailsMember.breakfastIncluded + ) + rateDetails.rateDefinitionMember = + rateDetailsMember.rateDefinition + } const rateDefinition = findRateDefintion(rateCode) if (rateDefinition) { switch (rateDefinition.rateType) { diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts index 803fdcf9e..a2d3a336e 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts @@ -18,7 +18,7 @@ const baseProductSchema = z.object({ breakfastIncluded: z.boolean().default(false), // Used to set the rate that we use to chose titles etc. rate: z.enum(["change", "flex", "save"]).default("save"), - rateDefinition: rateDefinitionSchema.nullish().transform((val) => + rateDefinition: rateDefinitionSchema.optional().transform((val) => val ? val : { @@ -36,6 +36,7 @@ const baseProductSchema = z.object({ title: "", } ), + rateDefinitionMember: rateDefinitionSchema.optional(), }) function mapBaseProduct(baseProduct: typeof baseProductSchema._type) { @@ -44,6 +45,7 @@ function mapBaseProduct(baseProduct: typeof baseProductSchema._type) { breakfastIncluded: baseProduct.breakfastIncluded, rate: baseProduct.rate, rateDefinition: baseProduct.rateDefinition, + rateDefinitionMember: baseProduct.rateDefinitionMember, } } @@ -98,12 +100,14 @@ export const redemptionsProduct = z breakfastIncluded, rate, rateDefinition, + rateDefinitionMember, ...redemption }) => ({ bookingCode, breakfastIncluded, rate, rateDefinition, + rateDefinitionMember, redemption, }) ) diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts index 4a0adcec2..2703dea7a 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts @@ -1,6 +1,7 @@ -import { nullableStringValidator } from "@/utils/zod/stringValidator" import { z } from "zod" +import { nullableStringValidator } from "@/utils/zod/stringValidator" + export const rateDefinitionSchema = z.object({ breakfastIncluded: z.boolean(), cancellationRule: z.string(), diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts index cb8cf6a49..aa7b200b4 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/rates.ts @@ -8,11 +8,9 @@ export interface RatesProps { roomConfiguration: RoomConfiguration } -export interface SharedRateCardProps extends Pick< - RoomConfiguration, - "roomTypeCode" -> { +export interface SharedRateCardProps + extends Pick { handleSelectRate: (product: Product) => void nights: number petRoomPackage: NonNullable[number] | undefined -} \ No newline at end of file +} diff --git a/packages/design-system/lib/components/RateCard/Campaign/Campaign.stories.tsx b/packages/design-system/lib/components/RateCard/Campaign/Campaign.stories.tsx index e8c9fcc36..8cb23a627 100644 --- a/packages/design-system/lib/components/RateCard/Campaign/Campaign.stories.tsx +++ b/packages/design-system/lib/components/RateCard/Campaign/Campaign.stories.tsx @@ -20,6 +20,7 @@ const meta: Meta = { omnibusRate: { control: 'object' }, comparisonRate: { control: 'object' }, approximateRate: { control: 'object' }, + rateTermDetails: { control: 'object' }, }, } @@ -47,6 +48,12 @@ export const Default: Story = { label: 'Lowest past price (last 30 days)', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -65,6 +72,12 @@ export const Package: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -88,6 +101,12 @@ export const CampaignLoggedIn: Story = { unit: 'EUR/NIGHT', }, isHighlightedRate: true, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -116,5 +135,11 @@ export const CampaignOmnibus: Story = { label: 'Lowest past price (last 30 days)', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } diff --git a/packages/design-system/lib/components/RateCard/Campaign/index.tsx b/packages/design-system/lib/components/RateCard/Campaign/index.tsx index b205def33..a4b79c0b2 100644 --- a/packages/design-system/lib/components/RateCard/Campaign/index.tsx +++ b/packages/design-system/lib/components/RateCard/Campaign/index.tsx @@ -1,10 +1,11 @@ import { Typography } from '../../Typography' -import { Rate } from '../types' +import { Rate, RateTermDetails } from '../types' import styles from '../rate-card.module.css' import { Button } from '../../Button' import { variants } from '../variants' import { MaterialIcon } from '../../Icons' +import Modal from '../Modal' interface CampaignRateCardProps { name: string @@ -21,6 +22,7 @@ interface CampaignRateCardProps { approximateRate?: Rate handleChange: () => void handleTermsClick?: () => void + rateTermDetails: RateTermDetails[] } export default function CampaignRateCard({ @@ -37,7 +39,7 @@ export default function CampaignRateCard({ bannerText, isHighlightedRate, handleChange, - handleTermsClick, + rateTermDetails, }: CampaignRateCardProps) { const classNames = variants({ variant: 'Campaign', @@ -61,14 +63,40 @@ export default function CampaignRateCard({ - + + + } > - - + {rateTermDetails.map((termGroup) => ( + + + {termGroup.title} + + {termGroup.terms.map((term) => ( + + + + {term} + + + ))} + + ))} + {rateTitle} {` / ${paymentTerm}`} diff --git a/packages/design-system/lib/components/RateCard/Code/Code.stories.tsx b/packages/design-system/lib/components/RateCard/Code/Code.stories.tsx index 2dc2ab1db..e6ba2d733 100644 --- a/packages/design-system/lib/components/RateCard/Code/Code.stories.tsx +++ b/packages/design-system/lib/components/RateCard/Code/Code.stories.tsx @@ -16,6 +16,7 @@ const meta: Meta = { paymentTerm: { control: 'text' }, rate: { control: 'object' }, approximateRate: { control: 'object' }, + rateTermDetails: { contorlr: 'object' }, }, } @@ -38,6 +39,12 @@ export const Default: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -51,6 +58,12 @@ export const Voucher: Story = { price: '1', unit: 'VOUCHER', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -69,6 +82,12 @@ export const CorporateCheck: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -87,6 +106,12 @@ export const DNumberDefault: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -106,6 +131,12 @@ export const DNumberHighlightedRate: Story = { unit: 'EUR', }, isHighlightedRate: true, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -124,6 +155,12 @@ export const LNumberDefault: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -146,5 +183,11 @@ export const LNumberStrikethrough: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } diff --git a/packages/design-system/lib/components/RateCard/Code/index.tsx b/packages/design-system/lib/components/RateCard/Code/index.tsx index 03c3adfc3..8b4212806 100644 --- a/packages/design-system/lib/components/RateCard/Code/index.tsx +++ b/packages/design-system/lib/components/RateCard/Code/index.tsx @@ -1,10 +1,11 @@ -import { Rate } from '../types' +import { Rate, RateTermDetails } from '../types' import styles from '../rate-card.module.css' import { Typography } from '../../Typography' import { Button } from '../../Button' import { variants } from '../variants' import { MaterialIcon } from '../../Icons' +import Modal from '../Modal' interface CodeRateCardProps { name: string @@ -19,6 +20,7 @@ interface CodeRateCardProps { isHighlightedRate?: boolean handleChange: () => void handleTermsClick?: () => void + rateTermDetails: RateTermDetails[] } export default function CodeRateCard({ @@ -33,7 +35,7 @@ export default function CodeRateCard({ bannerText, isHighlightedRate, handleChange, - handleTermsClick, + rateTermDetails, }: CodeRateCardProps) { const classNames = variants({ variant: 'Code', @@ -57,14 +59,40 @@ export default function CodeRateCard({ - + + + } > - - + {rateTermDetails.map((termGroup) => ( + + + {termGroup.title} + + {termGroup.terms.map((term) => ( + + + + {term} + + + ))} + + ))} + {rateTitle} {` / ${paymentTerm}`} diff --git a/packages/design-system/lib/components/RateCard/Modal/index.tsx b/packages/design-system/lib/components/RateCard/Modal/index.tsx new file mode 100644 index 000000000..4b90229d9 --- /dev/null +++ b/packages/design-system/lib/components/RateCard/Modal/index.tsx @@ -0,0 +1,174 @@ +'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 { + type AnimationState, + AnimationStateEnum, + type InnerModalProps, + type ModalProps, +} from './modal' +import { fade, slideInOut } from './motionVariants' + +import styles from './modal.module.css' +import { Typography } from '../../Typography' +import { MaterialIcon } from '../../Icons' + +const MotionOverlay = motion(ModalOverlay) +const MotionModal = motion(AriaModal) + +function InnerModal({ + animation, + onAnimationComplete = () => undefined, + setAnimation, + onToggle, + isOpen, + children, + title, + subtitle, + hideHeader, +}: PropsWithChildren) { + 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 ( + + + + {({ close }) => ( + <> + {!hideHeader && ( + + + {title && ( + + {title} + + )} + {subtitle && ( + + {subtitle} + + )} + + + + + + + )} + + {children} + > + )} + + + + ) +} + +export default function Modal({ + onAnimationComplete = () => undefined, + trigger, + isOpen, + onToggle, + title, + subtitle, + children, + withActions = false, + hideHeader = false, +}: PropsWithChildren) { + const [animation, setAnimation] = useState( + AnimationStateEnum.visible + ) + + useEffect(() => { + if (typeof isOpen === 'boolean') { + setAnimation( + isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden + ) + } + if (isOpen === undefined) { + setAnimation(AnimationStateEnum.unmounted) + } + }, [isOpen]) + + if (!trigger) { + return ( + + {children} + + ) + } + + return ( + + setAnimation( + isOpen ? AnimationStateEnum.visible : AnimationStateEnum.hidden + ) + } + > + {trigger} + + {children} + + + ) +} diff --git a/packages/design-system/lib/components/RateCard/Modal/modal.module.css b/packages/design-system/lib/components/RateCard/Modal/modal.module.css new file mode 100644 index 000000000..e9852a07d --- /dev/null +++ b/packages/design-system/lib/components/RateCard/Modal/modal.module.css @@ -0,0 +1,99 @@ +:root { + --max-width-navigation: 89.5rem; + + --max-width-spacing: calc(var(--Space-x2) * 2); + --max-width-page: min( + calc(100dvw - var(--max-width-spacing)), + var(--max-width-navigation) + ); +} +.overlay { + background: rgba(0, 0, 0, 0.5); + height: var(--visual-viewport-height); + position: fixed; + top: 0; + left: 0; + width: 100vw; + z-index: 100; +} + +.modal { + background-color: var(--Neutral-0); + border-radius: var(--Corner-radius-md) var(--Corner-radius-md) 0 0; + box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); + width: 100%; + position: absolute; + left: 0; + bottom: 0; + z-index: 101; +} + +.dialog { + display: flex; + flex-direction: column; + + /* For removing focus outline when modal opens first time */ + outline: 0 none; + + /* for supporting animations within content */ + position: relative; + overflow: hidden; + + max-height: 100dvh; +} + +.header { + box-sizing: content-box; + display: flex; + align-items: flex-start; + gap: var(--Space-x3); + justify-content: space-between; + min-height: min-content; + position: relative; + padding: var(--Space-x3) var(--Space-x3) 0; +} + +.content { + display: flex; + flex-direction: column; + /* align-items: center; */ + gap: var(--Space-x2); + overflow: auto; + padding: var(--Space-x3); +} + +.close { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +@media screen and (min-width: 768px) { + :root { + --max-width-spacing: calc(var(--Space-x3) * 2); + } + .overlay { + display: flex; + justify-content: center; + align-items: center; + } + + .modal { + left: auto; + bottom: auto; + width: auto; + border-radius: var(--Corner-radius-md); + max-width: var(--max-width-page); + } + + .dialog { + max-height: 90dvh; + } +} + +@media screen and (min-width: 1367px) { + :root { + --max-width-spacing: calc(var(--Space-x5) * 2); + } +} diff --git a/packages/design-system/lib/components/RateCard/Modal/modal.ts b/packages/design-system/lib/components/RateCard/Modal/modal.ts new file mode 100644 index 000000000..1a0fcca6b --- /dev/null +++ b/packages/design-system/lib/components/RateCard/Modal/modal.ts @@ -0,0 +1,29 @@ +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 + subtitle?: string + withActions?: boolean + hideHeader?: boolean +} & ( + | { trigger: JSX.Element; isOpen?: never; onToggle?: never } + | { + trigger?: never + isOpen: boolean + onToggle: (open: boolean) => void + } +) + +export type InnerModalProps = Omit & { + animation: AnimationState + setAnimation: Dispatch> +} diff --git a/packages/design-system/lib/components/RateCard/Modal/motionVariants.ts b/packages/design-system/lib/components/RateCard/Modal/motionVariants.ts new file mode 100644 index 000000000..ba695a0bc --- /dev/null +++ b/packages/design-system/lib/components/RateCard/Modal/motionVariants.ts @@ -0,0 +1,36 @@ +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' }, + }, +} + +export const slideFromTop = { + hidden: { + opacity: 0, + y: -32, + transition: { duration: 0.4, ease: 'easeInOut' }, + }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.4, ease: 'easeInOut' }, + }, +} diff --git a/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx b/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx index b18702f9b..1e0f1cded 100644 --- a/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx +++ b/packages/design-system/lib/components/RateCard/NoRateAvailable/index.tsx @@ -10,7 +10,6 @@ interface NoRateAvailableCardProps { paymentTerm: string bannerText?: string noPricesAvailableText: string - handleTermsClick?: () => void } export default function NoRateAvailableCard({ @@ -19,7 +18,6 @@ export default function NoRateAvailableCard({ paymentTerm, bannerText, noPricesAvailableText, - handleTermsClick, }: NoRateAvailableCardProps) { const classNames = variants({ variant, @@ -36,12 +34,7 @@ export default function NoRateAvailableCard({ - + {`${rateTitle} / ${paymentTerm}`} diff --git a/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx b/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx index 3e73cb05d..af82f4425 100644 --- a/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx +++ b/packages/design-system/lib/components/RateCard/Points/Points.stories.tsx @@ -21,6 +21,7 @@ const meta: Meta = { onRateSelect: { action: 'onRateSelect' }, isNotEnoughPoints: { control: 'boolean' }, notEnoughPointsText: { control: 'text' }, + rateTermDetails: { control: 'object' }, }, } @@ -60,6 +61,12 @@ export const Default: Story = { ], selectedRate: undefined, onRateSelect: (value) => console.log(value), + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -97,6 +104,12 @@ export const WithDisabledRates: Story = { ], selectedRate: '2', onRateSelect: (value) => console.log(value), + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -134,5 +147,11 @@ export const NotEnoughPoints: Story = { isNotEnoughPoints: true, notEnoughPointsText: 'Not enough points', onRateSelect: (value) => console.log(value), + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } diff --git a/packages/design-system/lib/components/RateCard/Points/index.tsx b/packages/design-system/lib/components/RateCard/Points/index.tsx index 147f914f3..94d334bb1 100644 --- a/packages/design-system/lib/components/RateCard/Points/index.tsx +++ b/packages/design-system/lib/components/RateCard/Points/index.tsx @@ -1,5 +1,5 @@ import { Typography } from '../../Typography' -import { RatePointsOption } from '../types' +import { RatePointsOption, RateTermDetails } from '../types' import styles from '../rate-card.module.css' import { Button } from '../../Button' @@ -7,6 +7,7 @@ import { Radio } from '../../Radio' import { RadioGroup } from 'react-aria-components' import { variants } from '../variants' import { MaterialIcon } from '../../Icons' +import Modal from '../Modal' interface PointsRateCardProps { rateTitle: string @@ -17,7 +18,7 @@ interface PointsRateCardProps { onRateSelect: (value: string) => void isNotEnoughPoints?: boolean notEnoughPointsText?: string - handleTermsClick?: () => void + rateTermDetails: RateTermDetails[] } export default function PointsRateCard({ @@ -29,7 +30,7 @@ export default function PointsRateCard({ isNotEnoughPoints, notEnoughPointsText, onRateSelect, - handleTermsClick, + rateTermDetails, }: PointsRateCardProps) { const classNames = variants({ variant: 'Points', @@ -44,14 +45,36 @@ export default function PointsRateCard({ - + + + } > - - + {rateTermDetails.map((termGroup) => ( + + + {termGroup.title} + + {termGroup.terms.map((term) => ( + + + + {term} + + + ))} + + ))} + {rateTitle} {` / ${paymentTerm}`} diff --git a/packages/design-system/lib/components/RateCard/Regular/Regular.stories.tsx b/packages/design-system/lib/components/RateCard/Regular/Regular.stories.tsx index 0e230496c..4fd2f73e8 100644 --- a/packages/design-system/lib/components/RateCard/Regular/Regular.stories.tsx +++ b/packages/design-system/lib/components/RateCard/Regular/Regular.stories.tsx @@ -18,6 +18,7 @@ const meta: Meta = { memberRate: { control: 'object' }, omnibusRate: { control: 'object' }, approximateRate: { control: 'object' }, + rateTermDetails: { control: 'object' }, }, } @@ -51,6 +52,12 @@ export const Default: Story = { price: '169', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -76,6 +83,12 @@ export const Selected: Story = { label: 'Approx.', unit: 'EUR', }, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } @@ -101,5 +114,11 @@ export const HidePublicRate: Story = { unit: 'EUR', }, hidePublicRate: true, + rateTermDetails: [ + { + title: 'Rate definition 1', + terms: ['term 1', 'term 2', 'term 3'], + }, + ], }, } diff --git a/packages/design-system/lib/components/RateCard/Regular/index.tsx b/packages/design-system/lib/components/RateCard/Regular/index.tsx index dd2801a0f..b302f4388 100644 --- a/packages/design-system/lib/components/RateCard/Regular/index.tsx +++ b/packages/design-system/lib/components/RateCard/Regular/index.tsx @@ -1,10 +1,11 @@ -import { Rate } from '../types' +import { Rate, RateTermDetails } from '../types' import styles from '../rate-card.module.css' import { Typography } from '../../Typography' import { Button } from '../../Button' import { variants } from '../variants' import { MaterialIcon } from '../../Icons' +import Modal from '../Modal' interface RegularRateCardProps { name: string @@ -18,7 +19,7 @@ interface RegularRateCardProps { approximateRate?: Rate hidePublicRate?: boolean handleChange: () => void - handleTermsClick?: () => void + rateTermDetails: RateTermDetails[] } export default function RegularRateCard({ @@ -33,7 +34,7 @@ export default function RegularRateCard({ memberRate, hidePublicRate, handleChange, - handleTermsClick, + rateTermDetails, }: RegularRateCardProps) { const classNames = variants({ variant: 'Regular' }) return ( @@ -51,14 +52,40 @@ export default function RegularRateCard({ - + + + } > - - + {rateTermDetails.map((termGroup) => ( + + + {termGroup.title} + + {termGroup.terms.map((term) => ( + + + + {term} + + + ))} + + ))} + {rateTitle} {` / ${paymentTerm}`} diff --git a/packages/design-system/lib/components/RateCard/rate-card.module.css b/packages/design-system/lib/components/RateCard/rate-card.module.css index 42948ff62..497f468cd 100644 --- a/packages/design-system/lib/components/RateCard/rate-card.module.css +++ b/packages/design-system/lib/components/RateCard/rate-card.module.css @@ -134,6 +134,17 @@ gap: var(--Space-x05); } +.terms { + display: flex; + flex-direction: column; + gap: var(--Space-x1); +} + +.term { + display: flex; + align-items: center; + gap: var(--Space-x1); +} .variant-campaign { background-color: var(--Surface-Brand-Primary-1-Default); } diff --git a/packages/design-system/lib/components/RateCard/types.ts b/packages/design-system/lib/components/RateCard/types.ts index e459cb310..ba1a51938 100644 --- a/packages/design-system/lib/components/RateCard/types.ts +++ b/packages/design-system/lib/components/RateCard/types.ts @@ -16,3 +16,7 @@ type AdditionalPrice = { price: string currency: string } +export type RateTermDetails = { + title: string + terms: string[] +} diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 68617b0d5..70c294791 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -78,6 +78,7 @@ "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-storybook": "^0.11.2", + "framer-motion": "^11.3.28", "glob": "^11.0.1", "husky": "^9.1.7", "jiti": "^1", diff --git a/yarn.lock b/yarn.lock index 9ca7a7ad8..c6b896dd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6200,6 +6200,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^5.1.0" eslint-plugin-react-refresh: "npm:^0.4.18" eslint-plugin-storybook: "npm:^0.11.2" + framer-motion: "npm:^11.3.28" glob: "npm:^11.0.1" husky: "npm:^9.1.7" jiti: "npm:^1"
{termGroup.title}
+ + {term} +
{title}