Files
web/apps/scandic-web/components/Blocks/DynamicContent/SAS/LinkedAccounts/index.tsx
Anton Gunnarsson c56a0b8ce9 Merged in feat/sw-1975-get-profile-v2 (pull request #1651)
Use get Profile V2 endpoint

Approved-by: Linus Flood
2025-04-08 06:26:00 +00:00

289 lines
8.7 KiB
TypeScript

import { cx } from "class-variance-authority"
import { type ReactNode, Suspense } from "react"
import DiamondAddIcon from "@scandic-hotels/design-system/Icons/DiamondAddIcon"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import {
SAS_EUROBONUS_TIER_TO_NAME_MAP,
TIER_TO_FRIEND_MAP,
} from "@/constants/membershipLevels"
import { env } from "@/env/server"
import { getProfile } from "@/lib/trpc/memoizedRequests"
import SectionContainer from "@/components/Section/Container"
import SectionHeader from "@/components/Section/Header"
import SectionLink from "@/components/Section/Link"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import { getIntl } from "@/i18n"
import { getEurobonusMembership } from "@/utils/user"
import { UnlinkSAS } from "./UnlinkSAS"
import styles from "./linkedAccounts.module.css"
import type { UserLoyalty } from "@/types/user"
type Props = {
title?: string
link?: { href: string; text: string }
subtitle?: string
}
export default async function SASLinkedAccount({
title,
subtitle,
link,
}: Props) {
if (!env.SAS_ENABLED) {
return null
}
const intl = await getIntl()
return (
<div className={styles.container}>
<SectionContainer>
<SectionHeader link={link} preamble={subtitle} title={title} />
<SectionLink link={link} variant="mobile" />
<section className={styles.cardsContainer}>
<Suspense fallback={<MatchedAccountInfoSkeleton />}>
<MatchedAccountInfo />
</Suspense>
</section>
</SectionContainer>
<div className={styles.mutationSection}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p className={styles.caption}>
<MaterialIcon icon="info" size={20} />
{intl.formatMessage({
id: "Changes in your level match can take up to 24 hours to be displayed.",
})}
</p>
</Typography>
<UnlinkSAS />
</div>
</div>
)
}
async function MatchedAccountInfo() {
const user = await getProfile()
if (!user || "error" in user) {
return null
}
const intl = await getIntl()
const eurobonusMembership = getEurobonusMembership(user.loyalty)
const friendsMembership = user.membership
if (!eurobonusMembership || !friendsMembership) {
return null
}
const sasLevelName = SAS_EUROBONUS_TIER_TO_NAME_MAP[eurobonusMembership.tier]
const sasMembershipNumber = eurobonusMembership.membershipNumber
const sasTierExpirationDate = eurobonusMembership.tierExpires
const scandicLevelName = TIER_TO_FRIEND_MAP[friendsMembership.membershipLevel]
const scandicExpirationDate = friendsMembership.tierExpirationDate
const matchState = calculateMatchState(user.loyalty)
return (
<section className={styles.matchedAccountSection}>
<div className={styles.accountDetails}>
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Linked account" })}</Label>
<Typography variant="Body/Paragraph/mdBold">
<p>{intl.formatMessage({ id: "SAS EuroBonus" })}</p>
</Typography>
</div>
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Level" })}</Label>
<Typography variant="Body/Paragraph/mdBold">
<p>{sasLevelName}</p>
</Typography>
</div>
<div className={cx(styles.stack, styles.accountMemberNumber)}>
<Label>{intl.formatMessage({ id: "Membership number" })}</Label>
<Typography variant="Body/Paragraph/mdBold">
<p className={styles.textRight}>EB{sasMembershipNumber}</p>
</Typography>
</div>
</div>
<div className={styles.tierMatchStatus}>
<TierMatchMessage
matchState={matchState}
scandicLevelName={scandicLevelName}
sasLevelName={sasLevelName}
/>
<TierMatchExpiration
matchState={matchState}
sasExpirationDate={sasTierExpirationDate}
scandicExpirationDate={scandicExpirationDate}
/>
</div>
</section>
)
}
async function MatchedAccountInfoSkeleton() {
const intl = await getIntl()
return (
<section className={styles.matchedAccountSection}>
<div className={styles.accountDetails}>
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Linked account" })}</Label>
<Typography variant="Body/Paragraph/mdBold">
<p>{intl.formatMessage({ id: "SAS EuroBonus" })}</p>
</Typography>
</div>
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Level" })}</Label>
<SkeletonShimmer width="6ch" height="24px" />
</div>
<div className={cx(styles.stack, styles.accountMemberNumber)}>
<Label>{intl.formatMessage({ id: "Membership number" })}</Label>
<SkeletonShimmer width="10ch" height="24px" />
</div>
</div>
<div className={styles.tierMatchStatus}>
<TierMatchMessageSkeleton />
</div>
</section>
)
}
type TierMatchMessageProps = {
matchState: MatchState
scandicLevelName: string
sasLevelName: string
}
async function TierMatchMessage({
matchState,
sasLevelName,
scandicLevelName,
}: TierMatchMessageProps) {
const intl = await getIntl()
const messageValues = {
sasLevelName: sasLevelName,
scandicLevelName: scandicLevelName,
sasMark: (text: ReactNode) => (
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.sasMark}>{text}</span>
</Typography>
),
scandicMark: (text: ReactNode) => (
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.scandicMark}>{text}</span>
</Typography>
),
}
const messageMap: Record<MatchState, ReactNode> = {
boostedBySAS: intl.formatMessage(
{
id: "<sasMark>EuroBonus {sasLevelName}</sasMark> has upgraded your Scandic Friends level to <scandicMark>{scandicLevelName}</scandicMark>.",
},
messageValues
),
boostedByScandic: intl.formatMessage(
{
id: "Your Scandic Friends level <scandicMark>{scandicLevelName}</scandicMark> has upgraded you to <sasMark>EuroBonus {sasLevelName}</sasMark>.",
},
messageValues
),
noBoost: intl.formatMessage(
{
id: "<sasMark>EuroBonus {sasLevelName}</sasMark> and <scandicMark>{scandicLevelName}</scandicMark> are equally matched tiers. Level up in one of your memberships to qualify for an upgrade!",
},
messageValues
),
}
const iconMap: Record<MatchState, ReactNode> = {
boostedBySAS: <DiamondAddIcon size={20} />,
boostedByScandic: <DiamondAddIcon size={20} />,
noBoost: <MaterialIcon icon="link" size={20} />,
}
return (
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Level match status" })}</Label>
<div className={styles.tierMatchText}>
<div className={styles.iconWrapper}>{iconMap[matchState]}</div>
<Typography variant="Body/Paragraph/mdRegular">
<p>{messageMap[matchState]}</p>
</Typography>
</div>
</div>
)
}
async function TierMatchMessageSkeleton() {
const intl = await getIntl()
return (
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Level match status" })}</Label>
<div className={styles.tierMatchText}>
<SkeletonShimmer width="250px" height="24px" />
</div>
</div>
)
}
type TierMatchExpirationProps = {
matchState: MatchState
sasExpirationDate: string | undefined
scandicExpirationDate: string | undefined
}
async function TierMatchExpiration({
matchState,
sasExpirationDate,
scandicExpirationDate,
}: TierMatchExpirationProps) {
if (matchState === "noBoost") {
return null
}
const intl = await getIntl()
return (
<div className={styles.stack}>
<Label>{intl.formatMessage({ id: "Upgrade valid until" })}</Label>
<Typography variant="Body/Paragraph/mdBold">
<p>
{matchState === "boostedBySAS"
? scandicExpirationDate
: sasExpirationDate}
</p>
</Typography>
</div>
)
}
function Label({ children }: { children: ReactNode }) {
return (
<Typography variant="Tag/sm">
<p className={styles.label}>{children}</p>
</Typography>
)
}
type MatchState = "boostedBySAS" | "boostedByScandic" | "noBoost"
function calculateMatchState(loyalty: UserLoyalty): MatchState {
if (!loyalty.tierBoostedBy) return "noBoost"
if (loyalty.tierBoostedBy === "SAS_EB") return "boostedBySAS"
// const eurobonusMembership = getEurobonusMembership(loyalty)
// if (eurobonusMembership.boostedByScandic) return "boostedByScandic"
return "noBoost"
}