Feat/lokalise rebuild * chore(lokalise): update translation ids * chore(lokalise): easier to switch between projects * chore(lokalise): update translation ids * . * . * . * . * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * . * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * . * . * chore(lokalise): update translation ids * chore(lokalise): update translation ids * chore(lokalise): new translations * merge * switch to errors for missing id's * merge * sync translations Approved-by: Linus Flood
353 lines
10 KiB
TypeScript
353 lines
10 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 SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
import { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers"
|
|
|
|
import {
|
|
SAS_EUROBONUS_TIER_TO_NAME_MAP,
|
|
TIER_TO_FRIEND_MAP,
|
|
} from "@/constants/membershipLevels"
|
|
import { getProfileWithExtendedPartnerData } from "@/lib/trpc/memoizedRequests"
|
|
|
|
import { Section } from "@/components/Section"
|
|
import SectionHeader from "@/components/Section/Header/Deprecated"
|
|
import SectionLink from "@/components/Section/Link"
|
|
import { getIntl } from "@/i18n"
|
|
import { getSasTierExpirationDate } from "@/utils/sas"
|
|
|
|
import { UnlinkSAS } from "./UnlinkSAS"
|
|
|
|
import styles from "./linkedAccounts.module.css"
|
|
|
|
import type { UserLoyalty } from "@scandic-hotels/trpc/types/user"
|
|
|
|
type Props = {
|
|
title?: string
|
|
link?: { href: string; text: string }
|
|
subtitle?: string
|
|
}
|
|
|
|
export default async function SASLinkedAccount({
|
|
title,
|
|
subtitle,
|
|
link,
|
|
}: Props) {
|
|
const intl = await getIntl()
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<Section>
|
|
<SectionHeader link={link} preamble={subtitle} title={title} />
|
|
<SectionLink link={link} variant="mobile" />
|
|
<Suspense fallback={<MatchedAccountInfoSkeleton />}>
|
|
<MatchedAccountInfo />
|
|
</Suspense>
|
|
</Section>
|
|
<div className={styles.mutationSection}>
|
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
<p className={styles.caption}>
|
|
<MaterialIcon icon="info" size={20} />
|
|
{intl.formatMessage({
|
|
id: "sas.linkedAccounts.changeDelayInfo",
|
|
defaultMessage:
|
|
"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 getProfileWithExtendedPartnerData()
|
|
|
|
if (!user || "error" in user || !user.loyalty) {
|
|
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.boostedTier || eurobonusMembership.tier
|
|
]
|
|
const sasMembershipNumber = eurobonusMembership.membershipNumber
|
|
const sasTierExpirationDate = getSasTierExpirationDate(eurobonusMembership)
|
|
|
|
const scandicLevelName = TIER_TO_FRIEND_MAP[friendsMembership.membershipLevel]
|
|
const scandicExpirationDate = friendsMembership.tierExpirationDate
|
|
|
|
const matchState = calculateMatchState(user.loyalty)
|
|
|
|
return (
|
|
<div className={styles.matchedAccountSection}>
|
|
<div className={styles.accountDetails}>
|
|
<div className={styles.stack}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "sas.linkedAccounts.linkedAccount",
|
|
defaultMessage: "Linked account",
|
|
})}
|
|
</Label>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
id: "partnerSas.sasEuroBonus",
|
|
defaultMessage: "SAS EuroBonus",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</div>
|
|
<div className={styles.stack}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "common.level",
|
|
defaultMessage: "Level",
|
|
})}
|
|
</Label>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>{sasLevelName}</p>
|
|
</Typography>
|
|
</div>
|
|
<div className={cx(styles.stack, styles.accountMemberNumber)}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "common.membershipId",
|
|
defaultMessage: "Membership ID",
|
|
})}
|
|
</Label>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
<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>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
async function MatchedAccountInfoSkeleton() {
|
|
const intl = await getIntl()
|
|
|
|
return (
|
|
<div className={styles.matchedAccountSection}>
|
|
<div className={styles.accountDetails}>
|
|
<div className={styles.stack}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "sas.linkedAccounts.linkedAccount",
|
|
defaultMessage: "Linked account",
|
|
})}
|
|
</Label>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>
|
|
{intl.formatMessage({
|
|
id: "partnerSas.sasEuroBonus",
|
|
defaultMessage: "SAS EuroBonus",
|
|
})}
|
|
</p>
|
|
</Typography>
|
|
</div>
|
|
<div className={styles.stack}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "common.level",
|
|
defaultMessage: "Level",
|
|
})}
|
|
</Label>
|
|
<SkeletonShimmer width="6ch" height="24px" />
|
|
</div>
|
|
<div className={cx(styles.stack, styles.accountMemberNumber)}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "common.membershipId",
|
|
defaultMessage: "Membership ID",
|
|
})}
|
|
</Label>
|
|
<SkeletonShimmer width="10ch" height="24px" />
|
|
</div>
|
|
</div>
|
|
<div className={styles.tierMatchStatus}>
|
|
<TierMatchMessageSkeleton />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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: "sas.linkedAccounts.euroBonusSasUpgradedText",
|
|
defaultMessage:
|
|
"<sasMark>EuroBonus {sasLevelName}</sasMark> has upgraded your Scandic Friends level to <scandicMark>{scandicLevelName}</scandicMark>.",
|
|
},
|
|
messageValues
|
|
),
|
|
boostedByScandic: intl.formatMessage(
|
|
{
|
|
id: "sas.linkedAccounts.scandicFriendsUpgradedText",
|
|
defaultMessage:
|
|
"Your Scandic Friends level <scandicMark>{scandicLevelName}</scandicMark> has upgraded you to <sasMark>EuroBonus {sasLevelName}</sasMark>.",
|
|
},
|
|
messageValues
|
|
),
|
|
noBoost: intl.formatMessage(
|
|
{
|
|
id: "sas.linkedAccounts.euroBonusSasText",
|
|
defaultMessage:
|
|
"<sasMark>EuroBonus {sasLevelName}</sasMark> and <scandicMark>{scandicLevelName}</scandicMark> are equally matched. 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: "sas.linkedAccounts.levelMatchStatus",
|
|
defaultMessage: "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: "sas.linkedAccounts.levelMatchStatus",
|
|
defaultMessage: "Level match status",
|
|
})}
|
|
</Label>
|
|
<div className={styles.tierMatchText}>
|
|
<SkeletonShimmer width="250px" height="24px" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
type TierMatchExpirationProps = {
|
|
matchState: MatchState
|
|
sasExpirationDate: string | null
|
|
scandicExpirationDate: string | undefined
|
|
}
|
|
async function TierMatchExpiration({
|
|
matchState,
|
|
sasExpirationDate,
|
|
scandicExpirationDate,
|
|
}: TierMatchExpirationProps) {
|
|
if (matchState === "noBoost") {
|
|
return null
|
|
}
|
|
|
|
const intl = await getIntl()
|
|
|
|
const displayedExpirationDate =
|
|
matchState === "boostedBySAS" ? scandicExpirationDate : sasExpirationDate
|
|
|
|
if (!displayedExpirationDate) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className={cx(styles.stack, styles.textRight)}>
|
|
<Label>
|
|
{intl.formatMessage({
|
|
id: "sas.linkedAccounts.upgradeValidUntil",
|
|
defaultMessage: "Upgrade valid until",
|
|
})}
|
|
</Label>
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>{displayedExpirationDate}</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 {
|
|
const eurobonusMembership = getEurobonusMembership(loyalty)
|
|
if (eurobonusMembership?.boostedByScandic) return "boostedByScandic"
|
|
|
|
if (!loyalty.tierBoostedBy) return "noBoost"
|
|
if (loyalty.tierBoostedBy === "SAS_EB") return "boostedBySAS"
|
|
|
|
return "noBoost"
|
|
}
|