Merged in feat/LOY-400-create-spend-points-modal (pull request #3131)
Feat/LOY-400 create spend points modal * feat(LOY-400): Added custom button to my pages overview and skeleton file to custom modal for my points. * feat(LOY-400): Added custom button to my pages overview and components for custom modal for my points. * feat(LOY-400): Changed some style and infogridcardover * feat(LOY-400):Removed custom card components and changed in infoCard: Added imagePosition top, added optional height prop. In Card: Changed Text-wrap styling, added min-width styling to buttons, added optional Icon prop, added optional height prop * feat(LOY-400):Added linkList, LinkListItem component and messageBanner component. Added granola illustration. * feat(LOY-400): Removed background in several illustrations. Added component for illustration. Fixed LinkedList and styling for UsePointsButton. * feat(LOY-400): Added modal to PointsToSpendCard and fixed UsePointsButton. * fix(LOY-400):added some styling * feat(LOY-400): Linked Modal to contentstack and fetch the data in cards with UsePointsModal for now * feat(LOY-400): changed link to aria-component, cleaned up a bit * feat(LOY-400): Changed height for larger modals in mobile, fixed zod schema for no illustration input, cleaned up * fix(LOY-400): fixed graphql after rebase * fix(LOY-400): mini fix * fix(LOY-400): fixed pr-comments * fix(LOY-400): fixed some PR-comments * fix(LOY-400): fixed a PR-comment * feat(LOY-400): added size prop to ilustration in LinkListItem to be able to use illustrations in IllustrationByIconName * fix(LOY-400): fixed pr-comments * Merged in feat/LOY-402-pre-ticked-book-reward-night-in-booking-flow (pull request #3210) Feat/LOY-402 pre ticked book reward night in booking flow * feat(LOY-402): Changed UsePointsModal structure to handle button actions in card. * feat(LOY-402): added functionality for book now button * feat(LOY-400): pr comment fix * feat(LOY-402): transformed the contentstack data * fix(LOY-402): fixed pr comments Approved-by: Chuma Mcphoy (We Ahead) Approved-by: Anton Gunnarsson Approved-by: Matilda Landström * Merged in feat/LOY-404-add-tracking-for-spend-points-modal (pull request #3229) Feat/LOY-404 add tracking for spend points modal * feat(LOY-402): Changed UsePointsModal structure to handle button actions in card. * feat(LOY-402): added functionality for book now button * feat(LOY-400): pr comment fix * feat(LOY-402): transformed the contentstack data * feat(LOY-404): added tracking * fix(LOY-404): fix for session storage removal of bookNowFromPointsModal * feat(LOY-404): added consts * fix(LOY-404): moved foxusWidget const * fix(LOY-404): moved BOOKING_WIDGET_STATE const * fix(LOY-404):fix Approved-by: Matilda Landström * fix(LOY-400): some fixes * feat(LOY-400): created linkList storybook Approved-by: Chuma Mcphoy (We Ahead) Approved-by: Matilda Landström
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
width: 100%;
|
||||
padding: var(--Space-x3) var(--Space-x2) var(--Space-x4) var(--Space-x2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x1);
|
||||
padding: 0 var(--Space-x2);
|
||||
}
|
||||
|
||||
.points {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x15);
|
||||
}
|
||||
|
||||
.pointsText {
|
||||
color: var(--Text-Brand-OnAccent-Heading);
|
||||
}
|
||||
|
||||
.pointsNumber {
|
||||
color: var(--Text-Brand-OnPrimary-1-Accent);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modalContent {
|
||||
padding: var(--Space-x3) var(--Space-x3) var(--Space-x3);
|
||||
width: 560px; /* From figma design */
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: var(--Space-x15) var(--Space-x2) var(--Space-x15) var(--Space-x3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useIsMobile } from "@scandic-hotels/booking-flow/hooks/useBreakpoint"
|
||||
import { Button, type ButtonProps } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
|
||||
import styles from "./UsePoints.module.css"
|
||||
|
||||
export function UsePointsButton({ variant, className, onPress }: ButtonProps) {
|
||||
const intl = useIntl()
|
||||
const isSmallScreen = useIsMobile()
|
||||
|
||||
const buttonVariant =
|
||||
variant === "Text" && isSmallScreen ? "Secondary" : variant
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="Medium"
|
||||
className={cx(styles.button, className)}
|
||||
variant={buttonVariant}
|
||||
typography="Body/Paragraph/mdBold"
|
||||
onPress={onPress}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "myPages.membershipPointsOverview.usePointsButton",
|
||||
defaultMessage: "Use points",
|
||||
})}
|
||||
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
"use client"
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { serializeBookingSearchParams } from "@scandic-hotels/booking-flow/utils/url"
|
||||
import { BOOK_NOW_SESSION_KEY } from "@scandic-hotels/common/constants/sessionKeys"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { LinkList } from "@scandic-hotels/design-system/LinkList"
|
||||
import { MessageBanner } from "@scandic-hotels/design-system/MessageBanner"
|
||||
import Modal from "@scandic-hotels/design-system/Modal"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trackClick, trackEvent } from "@scandic-hotels/tracking/base"
|
||||
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
||||
|
||||
import { rewardNightsURL } from "@/constants/rewards"
|
||||
|
||||
import InfoCard from "@/components/ContentType/StartPage/InfoCard"
|
||||
|
||||
import { UsePointsButton } from "./UsePointsButton"
|
||||
|
||||
import styles from "./UsePoints.module.css"
|
||||
|
||||
import type { ButtonProps } from "@scandic-hotels/design-system/Button"
|
||||
import type { UsePointsModalData } from "@scandic-hotels/trpc/routers/contentstack/UsePointsModal/output"
|
||||
|
||||
function trackButtonClick(label: string) {
|
||||
trackClick(label)
|
||||
if (label === "book now") {
|
||||
sessionStorage.setItem(BOOK_NOW_SESSION_KEY, "true")
|
||||
}
|
||||
}
|
||||
|
||||
function trackLinkListClick(linkText: string) {
|
||||
if (!linkText) return
|
||||
trackEvent({
|
||||
event: "linkClick",
|
||||
link: { name: linkText },
|
||||
})
|
||||
}
|
||||
|
||||
type UsePointsModalProps = {
|
||||
buttonVariant: ButtonProps["variant"]
|
||||
contentData: UsePointsModalData
|
||||
points: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function UsePointsModal({
|
||||
buttonVariant,
|
||||
contentData,
|
||||
points,
|
||||
className,
|
||||
}: UsePointsModalProps) {
|
||||
const intl = useIntl()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const pathname = usePathname()
|
||||
const initialSearchParams = useSearchParams()
|
||||
|
||||
const searchParams = serializeBookingSearchParams(
|
||||
{
|
||||
searchType: SEARCH_TYPE_REDEMPTION,
|
||||
focusWidget: true,
|
||||
},
|
||||
{ initialSearchParams }
|
||||
)
|
||||
|
||||
const bookLink = `${pathname}?${searchParams}`
|
||||
|
||||
const [items] = contentData.all_usepointsmodal.items
|
||||
|
||||
const linkListItems = items.link_group.map((link) => ({
|
||||
text: link.text,
|
||||
isExternal: link.isExternal,
|
||||
href: link.href,
|
||||
illustration: link.illustration,
|
||||
onClick: () => trackLinkListClick(link.text),
|
||||
}))
|
||||
|
||||
return (
|
||||
<>
|
||||
<UsePointsButton
|
||||
variant={buttonVariant}
|
||||
onPress={() => {
|
||||
setIsOpen(true)
|
||||
trackButtonClick("use points")
|
||||
}}
|
||||
className={className}
|
||||
/>
|
||||
<Modal isOpen={isOpen} onToggle={setIsOpen} withActions>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={styles.header}>
|
||||
<Typography variant="Tag/sm">
|
||||
<p className={styles.pointsText}>
|
||||
{intl.formatMessage({
|
||||
id: "myPages.membershipPoints.youHave",
|
||||
defaultMessage: "You have",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.points}>
|
||||
<Typography variant="Title/lg">
|
||||
<p className={styles.pointsNumber}>
|
||||
{intl.formatNumber(points)}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Tag/sm">
|
||||
<p className={styles.pointsText}>
|
||||
{intl.formatMessage({
|
||||
id: "myPages.membershipPoints.pointsToSpend",
|
||||
defaultMessage: "points to spend",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{points >= 10000 ? (
|
||||
<InfoCard
|
||||
image={items.image}
|
||||
heading={intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.heading",
|
||||
defaultMessage: "You’ve earned a night away",
|
||||
})}
|
||||
bodyText={intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.bodytext",
|
||||
defaultMessage: "Reward nights start at 10,000 points",
|
||||
})}
|
||||
primaryButton={{
|
||||
href: bookLink,
|
||||
title: intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.bookNow",
|
||||
defaultMessage: "Book now",
|
||||
}),
|
||||
forceReload: true,
|
||||
}}
|
||||
secondaryButton={{
|
||||
href: rewardNightsURL,
|
||||
title: intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.priceList",
|
||||
defaultMessage: "Price list",
|
||||
}),
|
||||
openInNewTab: true,
|
||||
materialIcon: (
|
||||
<MaterialIcon
|
||||
icon="open_in_new"
|
||||
color="CurrentColor"
|
||||
size={20}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onPrimaryButtonClick={() => trackButtonClick("book now")}
|
||||
onSecondaryButtonClick={() => trackButtonClick("price list")}
|
||||
theme="primaryDark"
|
||||
imagePosition="top"
|
||||
height="dynamic"
|
||||
></InfoCard>
|
||||
) : (
|
||||
<InfoCard
|
||||
heading={intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.heading",
|
||||
defaultMessage:
|
||||
"Earn at least 10 000 points for a reward night",
|
||||
})}
|
||||
secondaryButton={{
|
||||
href: rewardNightsURL,
|
||||
title: intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.priceList",
|
||||
defaultMessage: "Price list",
|
||||
}),
|
||||
openInNewTab: true,
|
||||
materialIcon: (
|
||||
<MaterialIcon
|
||||
icon="open_in_new"
|
||||
color="CurrentColor"
|
||||
size={20}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
onSecondaryButtonClick={() => trackButtonClick("price list")}
|
||||
theme="primaryDark"
|
||||
imagePosition="top"
|
||||
height="dynamic"
|
||||
></InfoCard>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<LinkList linkListItems={linkListItems}></LinkList>
|
||||
</div>
|
||||
<MessageBanner
|
||||
type="info"
|
||||
text={intl.formatMessage({
|
||||
id: "myPages.membershipPointsModal.infoBanner",
|
||||
defaultMessage:
|
||||
"Spending points do not affect your level progress.",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -5,10 +5,12 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { isBoostedBySas } from "@scandic-hotels/trpc/routers/user/helpers"
|
||||
|
||||
import { membershipLevels } from "@/constants/membershipLevels"
|
||||
import { getUsePointsModal } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import MembershipLevelIcon from "@/components/Levels/Icon"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { UsePointsModal } from "./UsePoints/UsePointsModal"
|
||||
import SasBoostStatus from "./SasBoostStatus"
|
||||
|
||||
import styles from "./membershipOverviewCard.module.css"
|
||||
@@ -24,6 +26,8 @@ export default async function MembershipOverviewCard({
|
||||
}: MembershipOverviewCardProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
const usePointsData = await getUsePointsModal()
|
||||
|
||||
if (!user.membership?.membershipLevel) {
|
||||
return null
|
||||
}
|
||||
@@ -70,17 +74,28 @@ export default async function MembershipOverviewCard({
|
||||
color="Border/Divider/Brand/OnPrimary 3/Default"
|
||||
/>
|
||||
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h3 className={styles.headingText}>
|
||||
{intl.formatMessage({
|
||||
id: "common.pointsToSpend",
|
||||
defaultMessage: "Points to spend",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
<Typography variant="Title/lg">
|
||||
<p className={styles.pointsValue}>{pointsToSpendText}</p>
|
||||
</Typography>
|
||||
<div className={styles.bottom}>
|
||||
<div>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h3 className={styles.headingText}>
|
||||
{intl.formatMessage({
|
||||
id: "common.pointsToSpend",
|
||||
defaultMessage: "Points to spend",
|
||||
})}
|
||||
</h3>
|
||||
</Typography>
|
||||
<Typography variant="Title/lg">
|
||||
<p className={styles.pointsValue}>{pointsToSpendText}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
{user.membership.currentPoints > 0 && usePointsData && (
|
||||
<UsePointsModal
|
||||
buttonVariant="Primary"
|
||||
contentData={usePointsData}
|
||||
points={user.membership.currentPoints}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,13 +23,26 @@
|
||||
.divider {
|
||||
margin: var(--Space-x4) 0;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
.pointsValue {
|
||||
color: var(--Text-Brand-OnPrimary-3-Accent);
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
.bottom {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: var(--Space-x4);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.card {
|
||||
padding: var(--Space-x3) var(--Space-x4);
|
||||
padding: var(--Space-x3) var(--Space-x4) var(--Space-x4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.usePointsButton {
|
||||
margin-top: var(--Space-x2);
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
.content {
|
||||
grid-template-columns: auto 1fr;
|
||||
@@ -105,6 +109,9 @@
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.usePointsButton {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import MoneyHandEllipsisIcon from "@scandic-hotels/design-system/Icons/MoneyHandEllipsisIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { spendPoints } from "@/constants/webHrefs"
|
||||
import { getUsePointsModal } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { UsePointsModal } from "../../Overview/MembershipOverviewCard/UsePoints/UsePointsModal"
|
||||
import ExpiringPointsSeeAllButton from "./ExpiringPointsSeeAllButton"
|
||||
import { getExpiryLabel } from "./utils"
|
||||
|
||||
@@ -25,6 +24,8 @@ export default async function PointsToSpendCard({
|
||||
const intl = await getIntl()
|
||||
const lang = await getLang()
|
||||
|
||||
const usePointsData = await getUsePointsModal()
|
||||
|
||||
if (!user.membership) {
|
||||
return null
|
||||
}
|
||||
@@ -74,18 +75,13 @@ export default async function PointsToSpendCard({
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
{hasPointsToSpend && (
|
||||
<ButtonLink href={spendPoints[lang]} target="_blank" variant="Text">
|
||||
{intl.formatMessage({
|
||||
id: "points.pointsToSpendCard.howToSpendCta",
|
||||
defaultMessage: "How to spend points",
|
||||
})}
|
||||
<MaterialIcon
|
||||
icon="chevron_right"
|
||||
color="CurrentColor"
|
||||
size={24}
|
||||
/>
|
||||
</ButtonLink>
|
||||
{hasPointsToSpend && usePointsData && (
|
||||
<UsePointsModal
|
||||
buttonVariant="Text"
|
||||
contentData={usePointsData}
|
||||
points={user.membership.currentPoints}
|
||||
className={styles.usePointsButton}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user