feat: add SignupPromo component

This commit is contained in:
Arvid Norlin
2024-12-03 14:56:20 +01:00
parent 963df41ed5
commit 083ba28f9e
15 changed files with 170 additions and 65 deletions

View File

@@ -1,13 +1,30 @@
"use client"
import { useEnterDetailsStore } from "@/stores/enter-details"
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import SummaryUI from "../UI"
import SummaryBottomSheet from "./BottomSheet"
import styles from "./mobile.module.css"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
import { DetailsState } from "@/types/stores/enter-details"
function storeSelector(state: DetailsState) {
return {
join: state.guest.join,
membershipNo: state.guest.membershipNo,
}
}
export default function MobileSummary(props: SummaryProps) {
const { join, membershipNo } = useEnterDetailsStore(storeSelector)
const showPromo = !props.isMember && !join && !membershipNo
return (
<div className={styles.mobileSummary}>
{showPromo ? <SignupPromoMobile /> : null}
<SummaryBottomSheet>
<div className={styles.wrapper}>
<SummaryUI {...props} />

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useEnterDetailsStore } from "@/stores/enter-details"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import { ArrowRightIcon, ChevronDownSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
@@ -17,7 +18,6 @@ import useLang from "@/hooks/useLang"
import styles from "./ui.module.css"
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
import { CurrencyEnum } from "@/types/enums/currency"
import type { DetailsState } from "@/types/stores/enter-details"
export function storeSelector(state: DetailsState) {
@@ -60,10 +60,14 @@ export default function SummaryUI({
const adults = booking.rooms[0].adults
const children = booking.rooms[0].children
const showMemberPrice = !!(
(isMember || join || membershipNo) &&
roomRate.memberRate
)
const memberPrice = roomRate.memberRate
? {
currency: roomRate.memberRate.localPrice.currency,
amount: roomRate.memberRate.localPrice.pricePerStay,
}
: null
const showMemberPrice = !!(isMember || join || membershipNo)
const diff = dt(booking.toDate).diff(booking.fromDate, "days")
@@ -240,6 +244,9 @@ export default function SummaryUI({
</div>
<Divider className={styles.bottomDivider} color="primaryLightSubtle" />
</div>
{!showMemberPrice && memberPrice ? (
<SignupPromoDesktop memberPrice={memberPrice} badgeContent={"✌️"} />
) : null}
</section>
)
}

View File

@@ -3,6 +3,8 @@ import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import SignupPromoDesktop from "@/components/HotelReservation/SignupPromo/Desktop"
import SignupPromoMobile from "@/components/HotelReservation/SignupPromo/Mobile"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
@@ -72,15 +74,7 @@ export default function RateSummary({
return (
<div className={styles.summary} data-visible={isVisible}>
{showMemberDiscountBanner && (
<div className={styles.memberDiscountBannerMobile}>
<Footnote color="burgundy">
{intl.formatMessage({
id: "Join or log in while booking for member pricing.",
})}
</Footnote>
</div>
)}
{showMemberDiscountBanner && <SignupPromoMobile />}
<div className={styles.content}>
<div className={styles.summaryText}>
<Subtitle color="uiTextHighContrast">{roomType}</Subtitle>
@@ -88,23 +82,13 @@ export default function RateSummary({
</div>
<div className={styles.summaryPriceContainer}>
{showMemberDiscountBanner && (
<div className={styles.memberDiscountBannerDesktop}>
<Footnote color="burgundy">
{intl.formatMessage<React.ReactNode>(
{
id: "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.",
},
{
span: (str) => (
<Caption color="red" type="bold" asChild>
<span>{str}</span>
</Caption>
),
amount: member.localPrice.pricePerStay,
currency: member.localPrice.currency,
}
)}
</Footnote>
<div className={styles.promoContainer}>
<SignupPromoDesktop
memberPrice={{
amount: member.localPrice.pricePerStay,
currency: member.localPrice.currency,
}}
/>
</div>
)}
<div className={styles.summaryPriceTextDesktop}>

View File

@@ -33,7 +33,12 @@
width: 100%;
}
.promoContainer {
display: none;
max-width: 264px;
}
.summaryPrice {
align-self: center;
display: flex;
width: 100%;
gap: var(--Spacing-x4);
@@ -50,6 +55,7 @@
}
.summaryPriceTextDesktop {
align-self: center;
display: none;
}
@@ -64,27 +70,6 @@
white-space: nowrap;
}
.memberDiscountBannerDesktop {
display: none;
background: var(--Primary-Light-Surface-Normal);
border-radius: var(--Corner-radius-xLarge) var(--Corner-radius-xLarge) 0px
var(--Corner-radius-xLarge);
flex-direction: row;
align-items: center;
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
gap: var(--Spacing-x2);
max-width: 264px;
}
.memberDiscountBannerMobile {
width: 100%;
background: var(--Primary-Light-Surface-Normal);
padding: var(--Spacing-x-one-and-half);
display: flex;
align-items: center;
justify-content: center;
}
@media (min-width: 768px) {
.summary {
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
@@ -93,15 +78,12 @@
flex-direction: row;
}
.petInfo,
.promoContainer,
.summaryText,
.summaryPriceTextDesktop {
display: block;
}
.memberDiscountBannerDesktop {
display: flex;
}
.summaryPriceTextMobile,
.memberDiscountBannerMobile {
.summaryPriceTextMobile {
display: none;
}
.summaryPrice,

View File

@@ -0,0 +1,43 @@
"use client"
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import styles from "./signupPromo.module.css"
import { SignupPromoProps } from "@/types/components/hotelReservation/signupPromo"
export default function SignupPromoDesktop({
memberPrice,
badgeContent,
}: SignupPromoProps) {
const intl = useIntl()
if (!memberPrice) {
return null
}
const { amount, currency } = memberPrice
const price = intl.formatNumber(amount, { currency, style: "currency" })
return memberPrice ? (
<div className={styles.memberDiscountBannerDesktop}>
{badgeContent && <span className={styles.badge}>{badgeContent}</span>}
<Footnote color="burgundy">
{intl.formatMessage<React.ReactNode>(
{
id: "To get the member price <span>{price}</span>, log in or join when completing the booking.",
},
{
span: (str) => (
<Caption color="red" type="bold" asChild>
<span>{str}</span>
</Caption>
),
price,
}
)}
</Footnote>
</div>
) : null
}

View File

@@ -0,0 +1,20 @@
"use client"
import { useIntl } from "react-intl"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import styles from "./signupPromo.module.css"
export default function SignupPromoMobile() {
const intl = useIntl()
return (
<div className={styles.memberDiscountBannerMobile}>
<Footnote color="burgundy">
{intl.formatMessage({
id: "Join or log in while booking for member pricing.",
})}
</Footnote>
</div>
)
}

View File

@@ -0,0 +1,43 @@
.memberDiscountBannerMobile {
width: 100%;
background: var(--Primary-Light-Surface-Normal);
padding: var(--Spacing-x-one-and-half);
display: flex;
align-items: center;
justify-content: center;
}
.memberDiscountBannerDesktop {
display: none;
background: var(--Primary-Light-Surface-Normal);
border-radius: var(--Corner-radius-xLarge) var(--Corner-radius-xLarge) 0px
var(--Corner-radius-xLarge);
align-items: center;
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
gap: var(--Spacing-x2);
position: relative;
}
.badge {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: -12px;
left: -12px;
height: 30px;
width: 30px;
background-color: var(--Main-Grey-White);
border-radius: 50%;
font-size: 24px;
overflow: hidden;
}
@media (min-width: 768px) {
.memberDiscountBannerMobile {
display: none;
}
.memberDiscountBannerDesktop {
display: flex;
}
}

View File

@@ -387,7 +387,7 @@
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
"This room is not available": "Dette værelse er ikke tilgængeligt",
"Times": "Tider",
"To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "For at få medlemsprisen <span>{amount} {currency}</span>, log ind eller tilmeld dig, når du udfylder bookingen.",
"To get the member price <span>{price}</span>, log in or join when completing the booking.": "For at få medlemsprisen <span>{price}</span>, log ind eller tilmeld dig, når du udfylder bookingen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.",
"Total": "Total",
"Total Points": "Samlet antal point",

View File

@@ -386,7 +386,7 @@
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
"This room is not available": "Dieses Zimmer ist nicht verfügbar",
"Times": "Zeiten",
"To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "Um den Mitgliederpreis von <span>{amount} {currency}</span> zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.",
"To get the member price <span>{price}</span>, log in or join when completing the booking.": "Um den Mitgliederpreis von <span>{price}</span> zu erhalten, loggen Sie sich ein oder treten Sie Scandic Friends bei, wenn Sie die Buchung abschließen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.",
"Total": "Gesamt",
"Total Points": "Gesamtpunktzahl",

View File

@@ -426,7 +426,7 @@
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
"This room is not available": "This room is not available",
"Times": "Times",
"To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.",
"To get the member price <span>{price}</span>, log in or join when completing the booking.": "To get the member price <span>{price}</span>, log in or join when completing the booking.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
"Total": "Total",
"Total Points": "Total Points",

View File

@@ -387,7 +387,7 @@
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
"This room is not available": "Tämä huone ei ole käytettävissä",
"Times": "Ajat",
"To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.",
"To get the member price <span>{price}</span>, log in or join when completing the booking.": "Jäsenhintaan saavat sisäänkirjautuneet tai liittyneet jäsenet.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.",
"Total": "Kokonais",
"Total Points": "Kokonaispisteet",

View File

@@ -384,7 +384,7 @@
"Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}",
"This room is not available": "Dette rommet er ikke tilgjengelig",
"Times": "Tider",
"To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "For å få medlemsprisen <span>{amount} {currency}</span>, logg inn eller bli med når du fullfører bestillingen.",
"To get the member price <span>{price}</span>, log in or join when completing the booking.": "For å få medlemsprisen <span>{price}</span>, logg inn eller bli med når du fullfører bestillingen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For å sikre din reservasjon, ber vi om at du gir oss dine betalingskortdetaljer. Vær sikker på at ingen gebyrer vil bli belastet på dette tidspunktet.",
"Total": "Total",
"Total Points": "Totale poeng",

View File

@@ -385,7 +385,7 @@
"Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}",
"This room is not available": "Detta rum är inte tillgängligt",
"Times": "Tider",
"To get the member price <span>{amount} {currency}</span>, log in or join when completing the booking.": "För att få medlemsprisen <span>{amount} {currency}</span>, logga in eller bli medlem när du slutför bokningen.",
"To get the member price <span>{price}</span>, log in or join when completing the booking.": "För att få medlemsprisen <span>{price}</span>, logga in eller bli medlem när du slutför bokningen.",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "För att säkra din bokning ber vi om att du ger oss dina betalkortdetaljer. Välj säker på att ingen avgifter kommer att debiteras just nu.",
"Total": "Totalt",
"Total Points": "Poäng totalt",

View File

@@ -514,7 +514,7 @@ const linksSchema = z.object({
export const priceSchema = z.object({
pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(),
currency: z.string(),
currency: z.nativeEnum(CurrencyEnum),
})
export const productTypePriceSchema = z.object({
@@ -530,7 +530,7 @@ const productSchema = z.object({
rateCode: "",
rateType: "",
localPrice: {
currency: "SEK",
currency: CurrencyEnum.SEK,
pricePerNight: 0,
pricePerStay: 0,
},

View File

@@ -0,0 +1,9 @@
import { CurrencyEnum } from "@/types/enums/currency"
export interface SignupPromoProps {
memberPrice: {
amount: number
currency: CurrencyEnum
}
badgeContent?: string
}