Merged in feat/best-friend-hero (pull request #338)

Feat(SW-170): Update overview hero

Approved-by: Christel Westerberg
This commit is contained in:
Matilda Landström
2024-07-12 06:45:44 +00:00
parent c1892ace66
commit 801a041404
48 changed files with 595 additions and 203 deletions

36
components/Icons/Copy.tsx Normal file
View File

@@ -0,0 +1,36 @@
import { iconVariants } from "./variants"
import type { IconProps } from "@/types/components/icon"
export default function CopyIcon({ className, color, ...props }: IconProps) {
const classNames = iconVariants({ className, color })
return (
<svg
className={classNames}
fill="none"
height="20"
viewBox="0 0 20 20"
width="20"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<mask
height="20"
id="mask0_1572_4523"
maskUnits="userSpaceOnUse"
style={{ maskType: "alpha" }}
width="20"
x="0"
y="0"
>
<rect width="20" height="20" fill="#D9D9D9" />
</mask>
<g mask="url(#mask0_1572_4523)">
<path
d="M7.54804 15.4742C7.17293 15.4742 6.85587 15.3447 6.59685 15.0857C6.33783 14.8267 6.20831 14.5096 6.20831 14.1345V4.64737C6.20831 4.27226 6.33783 3.95519 6.59685 3.69616C6.85587 3.43713 7.17293 3.30762 7.54804 3.30762H15.0352C15.4103 3.30762 15.7273 3.43713 15.9864 3.69616C16.2454 3.95519 16.3749 4.27226 16.3749 4.64737V14.1345C16.3749 14.5096 16.2454 14.8267 15.9864 15.0857C15.7273 15.3447 15.4103 15.4742 15.0352 15.4742H7.54804ZM7.54804 14.3909H15.0352C15.0993 14.3909 15.158 14.3642 15.2115 14.3108C15.2649 14.2574 15.2916 14.1986 15.2916 14.1345V4.64737C15.2916 4.58326 15.2649 4.52449 15.2115 4.47106C15.158 4.41764 15.0993 4.39093 15.0352 4.39093H7.54804C7.48393 4.39093 7.42517 4.41764 7.37175 4.47106C7.31832 4.52449 7.2916 4.58326 7.2916 4.64737V14.1345C7.2916 14.1986 7.31832 14.2574 7.37175 14.3108C7.42517 14.3642 7.48393 14.3909 7.54804 14.3909ZM4.96473 18.0575C4.58963 18.0575 4.27257 17.928 4.01354 17.669C3.75451 17.41 3.625 17.0929 3.625 16.7178V6.68901C3.625 6.53528 3.67642 6.40657 3.77927 6.30289C3.8821 6.19921 4.00977 6.14737 4.16227 6.14737C4.31476 6.14737 4.44389 6.19921 4.54967 6.30289C4.65543 6.40657 4.70831 6.53528 4.70831 6.68901V16.7178C4.70831 16.7819 4.73502 16.8407 4.78844 16.8941C4.84187 16.9475 4.90063 16.9742 4.96473 16.9742H12.9935C13.1473 16.9742 13.276 17.0257 13.3796 17.1285C13.4833 17.2313 13.5352 17.359 13.5352 17.5115C13.5352 17.664 13.4833 17.7931 13.3796 17.8989C13.276 18.0047 13.1473 18.0575 12.9935 18.0575H4.96473Z"
fill="#060606"
/>
</g>
</svg>
)
}

View File

@@ -10,6 +10,8 @@ import SoonestStays from "@/components/MyPages/Blocks/Stays/Soonest"
import UpcomingStays from "@/components/MyPages/Blocks/Stays/Upcoming"
import { removeMultipleSlashes } from "@/utils/url"
import PointsOverview from "../Blocks/Points/Overview"
import {
AccountPageContentProps,
ContentProps,
@@ -23,6 +25,8 @@ function DynamicComponent({ component, props }: AccountPageContentProps) {
switch (component) {
case DynamicContentComponents.membership_overview:
return <Overview {...props} />
case DynamicContentComponents.points_overview:
return <PointsOverview {...props} />
case DynamicContentComponents.previous_stays:
return <PreviousStays {...props} />
case DynamicContentComponents.soonest_stays:

View File

@@ -8,6 +8,7 @@ import CurrentBenefitsBlock from "../../Blocks/Benefits/CurrentLevel"
import NextLevelBenefitsBlock from "../../Blocks/Benefits/NextLevel"
import CurrentPointsBalance from "../../Blocks/Points/CurrentPointsBalance"
import EarnAndBurn from "../../Blocks/Points/EarnAndBurn"
import PointsOverview from "../../Blocks/Points/Overview"
import {
AccountPageContentProps,
@@ -21,14 +22,9 @@ import {
function DynamicComponent({ component, props }: AccountPageContentProps) {
switch (component) {
case DynamicContentComponents.membership_overview:
return (
<Overview
lang={props.lang}
link={props.link}
subtitle={null}
title={props.title}
/>
)
return <Overview {...props} />
case DynamicContentComponents.points_overview:
return <PointsOverview {...props} />
case DynamicContentComponents.current_benefits:
return <CurrentBenefitsBlock {...props} />
case DynamicContentComponents.next_benefits:

View File

@@ -8,7 +8,6 @@ import SectionHeader from "@/components/Section/Header"
import SectionLink from "@/components/Section/Link"
import Chip from "@/components/TempDesignSystem/Chip"
import Grids from "@/components/TempDesignSystem/Grids"
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
@@ -30,9 +29,8 @@ export default async function NextLevelBenefitsBlock({
return null
}
const nextLevel = getMembershipLevelObject(
user.memberships[0].membershipLevel as MembershipLevelEnum,
lang,
"nextLevel"
user.memberships[0].nextLevel as MembershipLevelEnum,
lang
)
if (!nextLevel) {
// TODO: handle this case, when missing or when user is top level?

View File

@@ -1,25 +1,27 @@
"use client"
import Image from "@/components/Image"
import CopyIcon from "@/components/Icons/Copy"
import Button from "@/components/TempDesignSystem/Button"
import { getMembership } from "@/utils/user"
import type { User } from "@/types/user"
import styles from "./copybutton.module.css"
export default function CopyButton({ memberships }: Pick<User, "memberships">) {
import type { CopyButtonProps } from "@/types/components/myPages/membership"
export default function CopyButton({ membershipNumber }: CopyButtonProps) {
function handleCopy() {
const membership = getMembership(memberships)
console.log(`COPIED! (${membership ? membership.membershipNumber : "N/A"})`)
navigator.clipboard.writeText(membershipNumber)
}
return (
<Button onClick={handleCopy} type="button" variant="icon">
<Image
alt="Copy Icon"
height={20}
src="/_static/icons/copy.svg"
width={20}
/>
<Button
onClick={handleCopy}
className={styles.button}
type="button"
variant="icon"
size="small"
intent="tertiary"
>
<CopyIcon color="pale" />
</Button>
)
}

View File

@@ -0,0 +1,5 @@
.button {
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,21 @@
.hero {
border-radius: var(--Corner-radius-xLarge);
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: 1fr;
padding: var(--Spacing-x7) var(--Spacing-x6);
}
.burgundy {
background-color: var(--Scandic-Brand-Burgundy);
}
.red {
background-color: var(--Scandic-Brand-Scandic-Red);
}
@media screen and (min-width: 768px) {
.hero {
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -0,0 +1,7 @@
import { VariantProps } from "class-variance-authority"
import { heroVariants } from "./heroVariants"
export interface HeroProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
VariantProps<typeof heroVariants> {}

View File

@@ -0,0 +1,15 @@
import { cva } from "class-variance-authority"
import styles from "./hero.module.css"
export const heroVariants = cva(styles.hero, {
variants: {
color: {
burgundy: styles.burgundy,
red: styles.red,
},
},
defaultVariants: {
color: "red",
},
})

View File

@@ -0,0 +1,7 @@
import { HeroProps } from "./hero"
import { heroVariants } from "./heroVariants"
export default function Hero({ className, color, children }: HeroProps) {
const classNames = heroVariants({ className, color })
return <section className={classNames}>{children}</section>
}

View File

@@ -12,7 +12,7 @@ import {
import styles from "./membershipLevel.module.css"
import type { MembershipLevelProps } from "@/types/components/myPages/membershipLevel"
import type { MembershipLevelProps } from "@/types/components/myPages/membership"
export default function MembershipLevel({ level }: MembershipLevelProps) {
switch (level) {

View File

@@ -0,0 +1,34 @@
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n"
import CopyButton from "../../Buttons/CopyButton"
import { MembershipNumberProps } from "./membershipNumber"
import { membershipNumberVariants } from "./membershipNumberVariants"
import styles from "./membershipNumber.module.css"
export default async function MembershipNumber({
className,
color,
membership,
}: MembershipNumberProps) {
const { formatMessage } = await getIntl()
const classNames = membershipNumberVariants({ className, color })
return (
<div className={classNames}>
<Caption color="pale">
{formatMessage({ id: "Membership ID" })}
{": "}
</Caption>
<span className={styles.icon}>
<Caption className={styles.icon} color="pale">
{membership.membershipNumber ?? "N/A"}
</Caption>
{membership && (
<CopyButton membershipNumber={membership.membershipNumber} />
)}
</span>
</div>
)
}

View File

@@ -0,0 +1,37 @@
.membershipContainer {
align-items: center;
background: var(--Scandic-Brand-Burgundy);
border-radius: var(--Corner-radius-Small);
display: grid;
grid-template-columns: 1fr;
justify-items: center;
padding: var(--Spacing-x1) var(--Spacing-x7) 0 var(--Spacing-x7);
}
.icon {
display: flex;
align-items: center;
flex-direction: row;
justify-content: center;
padding-left: var(--Spacing-x2);
}
.burgundy {
background-color: var(--Main-Brand-Burgundy);
}
.red {
background-color: var(--Scandic-Brand-Scandic-Red);
}
@media screen and (min-width: 768px) {
.membershipContainer {
grid-template-columns: auto auto;
padding: 0 0 0 var(--Spacing-x2);
gap: var(--Spacing-x-half);
}
.icon {
padding-left: 0;
}
}

View File

@@ -0,0 +1,11 @@
import { VariantProps } from "class-variance-authority"
import { membershipNumberVariants } from "./membershipNumberVariants"
import { User } from "@/types/user"
export interface MembershipNumberProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
VariantProps<typeof membershipNumberVariants> {
membership: User["memberships"][number]
}

View File

@@ -0,0 +1,15 @@
import { cva } from "class-variance-authority"
import styles from "./membershipNumber.module.css"
export const membershipNumberVariants = cva(styles.membershipContainer, {
variants: {
color: {
burgundy: styles.burgundy,
red: styles.red,
},
},
defaultVariants: {
color: "burgundy",
},
})

View File

@@ -9,7 +9,7 @@
.header {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
gap: var(--Spacing-x2);
}
.levelLabel {
@@ -35,14 +35,7 @@
background: var(--Scandic-Brand-Burgundy);
border-radius: var(--Corner-radius-Small);
display: grid;
gap: var(--Spacing-x1);
grid-template-columns: 1fr;
justify-items: center;
padding: var(--Spacing-x1) var(--Spacing-x2);
}
@media screen and (min-width: 768px) {
.membershipContainer {
grid-template-columns: auto auto;
}
padding: var(--Spacing-x1) var(--Spacing-x7) 0 var(--Spacing-x7);
}

View File

@@ -1,30 +1,39 @@
import { membershipLevels } from "@/constants/membershipLevels"
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import { getMembership } from "@/utils/user"
import { getMembership, isHighestMembership } from "@/utils/user"
import MembershipLevel from "./MemberShipLevel"
import { MembershipNumberProps } from "./MemershipNumber/membershipNumber"
import MembershipLevel from "./MembershipLevel"
import MembershipNumber from "./MemershipNumber"
import styles from "./friend.module.css"
import type { UserProps } from "@/types/components/myPages/user"
export default async function Friend({ user }: UserProps) {
export default async function Friend({
user,
color,
}: UserProps & Pick<MembershipNumberProps, "color">) {
const { formatMessage } = await getIntl()
const membership = getMembership(user.memberships)
if (!membership?.membershipLevel) {
return null
}
const isHighestLevel = isHighestMembership(membership.membershipLevel)
return (
<section className={styles.friend}>
<header className={styles.header}>
<BiroScript className={styles.levelLabel} color="pale">
{formatMessage({ id: "Current level" })}:
</BiroScript>
<Body color="white" textTransform="bold" textAlign="center">
{formatMessage(
isHighestLevel
? { id: "Highest level" }
: { id: "Your current level" }
)}
</Body>
{membership ? (
<MembershipLevel
level={membershipLevels[membership.membershipLevel]}
@@ -35,12 +44,7 @@ export default async function Friend({ user }: UserProps) {
<Title className={styles.name} color="pale" level="h3">
{user.name}
</Title>
<div className={styles.membershipContainer}>
<Caption color="pale">
{formatMessage({ id: "Membership ID" })}:{" "}
{membership ? membership.membershipNumber : "N/A"}
</Caption>
</div>
<MembershipNumber membership={membership} color={color} />
</div>
</section>
)

View File

@@ -0,0 +1,24 @@
import Body from "@/components/TempDesignSystem/Text/Body"
import { getIntl } from "@/i18n"
import { getMembership } from "@/utils/user"
import type { UserProps } from "@/types/components/myPages/user"
export default async function ExpiringPoints({ user }: UserProps) {
const { formatMessage } = await getIntl()
const membership = getMembership(user.memberships)
// TODO - add correct points when available from API
if (!membership /* || !membership.expiringPoints*/) {
// TODO: handle this case?
return null
}
return (
<section>
<Body color="white" textTransform="bold" textAlign="center">
{membership.currentPoints} {formatMessage({ id: "points expiring by" })}{" "}
{membership.expirationDate}
</Body>
</section>
)
}

View File

@@ -1,50 +0,0 @@
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import { getMembershipLevelObject } from "@/utils/membershipLevel"
import { getMembership } from "@/utils/user"
import styles from "./nextLevel.module.css"
import type { UserProps } from "@/types/components/myPages/user"
import type { LangParams } from "@/types/params"
export default async function NextLevel({
user,
lang,
}: UserProps & LangParams) {
const { formatMessage } = await getIntl()
const membership = getMembership(user.memberships)
if (!membership?.membershipLevel) {
// TODO: handle this case?
return null
}
const nextLevel = getMembershipLevelObject(
membership.membershipLevel,
lang,
"nextLevel"
)
if (!nextLevel) {
// TODO: already at top level, no next level exists
return null
}
return (
<section>
<Body color="white" textAlign="center">
{formatMessage({ id: "Next level" })}:
</Body>
<Title
className={styles.nextLevel}
color="white"
level="h3"
textAlign="center"
>
{nextLevel?.name || "N/A"}
<BiroScript>{formatMessage({ id: "Coming up" })}!</BiroScript>
</Title>
</section>
)
}

View File

@@ -1,13 +0,0 @@
.nextLevel {
align-items: center;
display: grid;
grid-template-columns: 1fr;
}
@media screen and (min-width: 768px) {
.nextLevel {
gap: var(--Spacing-x1);
grid-template-columns: auto auto;
justify-content: center;
}
}

View File

@@ -0,0 +1,14 @@
.points {
display: grid;
gap: var(--Spacing-x5);
text-wrap: balance;
}
@media screen and (min-width: 768px) {
.points {
grid-template-rows: auto auto auto;
grid-template-columns: 1fr 1fr;
row-gap: 0;
column-gap: var(--Spacing-x2);
}
}

View File

@@ -0,0 +1,5 @@
import styles from "./container.module.css"
export default function PointsContainer({ children }: React.PropsWithChildren) {
return <section className={styles.points}>{children}</section>
}

View File

@@ -0,0 +1,79 @@
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import styles from "./pointsColumn.module.css"
import type {
NightsColumn,
PointsColumn,
PointsColumnProps,
} from "@/types/components/myPages/points"
export const YourPointsColumn = ({ points }: PointsColumn) =>
PointsColumn({
points,
title: "Your points",
subtitle: "as of today",
})
export const NextLevelPointsColumn = ({
points,
subtitleParam,
}: PointsColumn) =>
PointsColumn({
points,
title: "Points needed to level up",
subtitleParam,
subtitle: "next level:",
})
export const StayOnLevelColumn = ({ points, subtitleParam }: PointsColumn) =>
PointsColumn({
points,
title: "Points needed to stay on level",
subtitleParam,
subtitle: "by",
})
export const NextLevelNightsColumn = ({
nights,
subtitleParam,
}: NightsColumn) =>
PointsColumn({
nights,
title: "Nights needed to level up",
subtitleParam,
subtitle: "by",
})
async function PointsColumn({
points,
nights,
title,
subtitle,
subtitleParam,
}: PointsColumnProps) {
const { formatMessage } = await getIntl()
return (
<article>
<Body
color="white"
textTransform="bold"
textAlign="center"
className={styles.firstRow}
>
{formatMessage({
id: title,
})}
</Body>
<Title color="white" level="h2" textAlign="center">
{points ?? nights ?? "N/A"}
</Title>
<Body color="white" textAlign="center">
{formatMessage({ id: subtitle })} {subtitleParam}
</Body>
</article>
)
}

View File

@@ -0,0 +1,5 @@
@media screen and (min-width: 768px) {
.firstRow {
align-content: flex-end;
}
}

View File

@@ -1,34 +1,30 @@
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
import { getMembershipLevelObject } from "@/utils/membershipLevel"
import { getMembership } from "@/utils/user"
import styles from "./totalPoints.module.css"
import PointsContainer from "./Container"
import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn"
import type { UserProps } from "@/types/components/myPages/user"
import { UserProps } from "@/types/components/myPages/user"
import { LangParams } from "@/types/params"
export default async function Points({ user }: UserProps) {
const { formatMessage } = await getIntl()
export default async function Points({ user, lang }: UserProps & LangParams) {
const membership = getMembership(user.memberships)
const nextLevel = getMembershipLevelObject(
membership?.nextLevel as MembershipLevelEnum,
lang
)
return (
<section className={styles.points}>
<article>
<Body color="white" textAlign="center">
{formatMessage({ id: "Total Points" })}
</Body>
<Title color="white" level="h2" textAlign="center">
{membership ? membership.currentPoints : "N/A"}
</Title>
</article>
<article>
<Body color="white" textAlign="center">
{formatMessage({ id: "Points until next level" })}
{/* TODO */}
</Body>
<Title color="white" level="h2" textAlign="center">
{membership ? membership.currentPoints : "N/A"}
</Title>
</article>
</section>
<PointsContainer>
<YourPointsColumn points={membership?.currentPoints} />
{nextLevel && (
<NextLevelPointsColumn
points={membership?.pointsRequiredToNextlevel}
subtitleParam={nextLevel.name}
/>
)}
</PointsContainer>
)
}

View File

@@ -1,11 +0,0 @@
.points {
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: 1fr;
}
@media screen and (min-width: 768px) {
.points {
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -1,6 +1,6 @@
import Divider from "@/components/TempDesignSystem/Divider"
import NextLevel from "./NextLevel"
import ExpiringPoints from "./ExpiringPoints"
import Points from "./Points"
import styles from "./stats.module.css"
@@ -11,9 +11,9 @@ import type { LangParams } from "@/types/params"
export default function Stats({ user, lang }: UserProps & LangParams) {
return (
<section className={styles.stats}>
<Points user={user} />
<Divider variant="default" color="white" />
<NextLevel user={user} lang={lang} />
<Points user={user} lang={lang} />
<Divider variant="default" color="pale" />
<ExpiringPoints user={user} />
</section>
)
}

View File

@@ -5,6 +5,7 @@ import SectionHeader from "@/components/Section/Header"
import SectionLink from "@/components/Section/Link"
import Divider from "@/components/TempDesignSystem/Divider"
import Hero from "./Friend/Hero"
import Friend from "./Friend"
import Stats from "./Stats"
@@ -23,14 +24,15 @@ export default async function Overview({
if (!user) {
return null
}
return (
<SectionContainer>
<SectionHeader link={link} subtitle={subtitle} title={title} topTitle />
<section className={styles.overview}>
<Friend user={user} />
<Hero color="red">
<Friend user={user} color="burgundy" />
<Divider className={styles.divider} color="peach" />
<Stats user={user} lang={lang} />
</section>
</Hero>
<SectionLink link={link} variant="mobile" />
</SectionContainer>
)

View File

@@ -1,12 +1,3 @@
.overview {
background-color: var(--Scandic-Brand-Scandic-Red);
border-radius: var(--Corner-radius-xLarge);
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: 1fr;
padding: var(--Spacing-x7) var(--Spacing-x6);
}
.divider {
padding-top: var(--Spacing-x2);
}
@@ -25,11 +16,6 @@
}
@media screen and (min-width: 768px) {
.overview {
gap: var(--Spacing-x2);
grid-template-columns: 1fr 1fr;
}
.divider {
display: none;
}

View File

@@ -25,7 +25,7 @@ async function CurrentPointsBalance({
<SectionContainer>
<SectionHeader title={title} link={link} subtitle={subtitle} />
<div className={styles.card}>
<h2>{`${formatMessage({ id: "Total Points" })}*`}</h2>
<h2>{`${formatMessage({ id: "Your points" })}*`}</h2>
<p className={styles.points}>
{`${formatMessage({ id: "Points" })}: ${membership ? membership.currentPoints : "N/A"}`}
</p>

View File

@@ -0,0 +1,55 @@
import {
MembershipLevelEnum,
membershipLevels,
} from "@/constants/membershipLevels"
import PointsContainer from "@/components/MyPages/Blocks/Overview/Stats/Points/Container"
import {
NextLevelNightsColumn,
NextLevelPointsColumn,
StayOnLevelColumn,
YourPointsColumn,
} from "@/components/MyPages/Blocks/Overview/Stats/Points/PointsColumn"
import { getMembershipLevelObject } from "@/utils/membershipLevel"
import { getMembership } from "@/utils/user"
import { UserProps } from "@/types/components/myPages/user"
import { LangParams } from "@/types/params"
/* TODO */
export default async function Points({ user, lang }: UserProps & LangParams) {
const membership = getMembership(user.memberships)
const nextLevel = getMembershipLevelObject(
membership?.nextLevel as MembershipLevelEnum,
lang
)
return (
<PointsContainer>
<YourPointsColumn points={membership?.currentPoints} />
{nextLevel && (
<>
{membership?.currentPoints ? (
<StayOnLevelColumn
points={membership?.currentPoints} //TODO
subtitleParam={membership?.expirationDate}
/>
) : (
<>
<NextLevelPointsColumn
points={membership?.pointsRequiredToNextlevel}
subtitleParam={nextLevel.name}
/>
{nextLevel?.level === membershipLevels.L7 && (
<NextLevelNightsColumn
nights={100} //TODO
subtitleParam={membership?.expirationDate}
/>
)}
</>
)}
</>
)}
</PointsContainer>
)
}

View File

@@ -0,0 +1,39 @@
import { serverClient } from "@/lib/trpc/server"
import SectionContainer from "@/components/Section/Container"
import SectionHeader from "@/components/Section/Header"
import SectionLink from "@/components/Section/Link"
import Divider from "@/components/TempDesignSystem/Divider"
import Friend from "../../Overview/Friend"
import Hero from "../../Overview/Friend/Hero"
import Stats from "../../Overview/Stats"
import styles from "./overview.module.css"
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
import type { LangParams } from "@/types/params"
export default async function PointsOverview({
link,
subtitle,
title,
lang,
}: AccountPageComponentProps & LangParams) {
const user = await serverClient().user.get()
if (!user) {
return null
}
return (
<SectionContainer>
<SectionHeader link={link} subtitle={subtitle} title={title} topTitle />
<Hero color="burgundy">
<Friend user={user} color="red" />
<Divider className={styles.divider} color="peach" />
<Stats user={user} lang={lang} />
</Hero>
<SectionLink link={link} variant="mobile" />
</SectionContainer>
)
}

View File

@@ -0,0 +1,9 @@
.divider {
padding-top: var(--Spacing-x2);
}
@media screen and (min-width: 768px) {
.divider {
display: none;
}
}

View File

@@ -13,6 +13,10 @@
border-bottom-color: var(--Scandic-Brand-Burgundy);
}
.pale {
border-bottom-color: var(--Primary-Dark-On-Surface-Text);
}
.peach {
border-bottom-color: var(--Primary-Light-On-Surface-Divider);
}

View File

@@ -10,6 +10,7 @@ export const dividerVariants = cva(styles.divider, {
beige: styles.beige,
white: styles.white,
subtle: styles.subtle,
pale: styles.pale,
},
opacity: {
100: styles.opacity100,

View File

@@ -5,11 +5,13 @@
"All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter",
"Already a friend?": "Allerede en ven?",
"Arrival date": "Ankomstdato",
"as of today": "fra idag",
"As our": "Som vores",
"As our Close Friend": "Som vores nære ven",
"At the hotel": "På hotellet",
"Book": "Bestil",
"Booking number": "Bestillingsnummer",
"by": "inden",
"Cancel": "Afbestille",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tjek de kreditkort, der er gemt på din profil. Betal med et gemt kort, når du er logget ind for en mere jævn weboplevelse.",
"Choose room": "Vælg rum",
@@ -24,7 +26,7 @@
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
"Country": "Land",
"Country code": "Landekode",
"Current level": "Nuværende niveau",
"Your current level": "Dit nuværende niveau",
"Current password": "Nuværende kodeord",
"characters": "tegn",
"Date of Birth": "Fødselsdato",
@@ -41,6 +43,7 @@
"From": "Fra",
"Get inspired": "Blive inspireret",
"Go back to overview": "Gå tilbage til oversigten",
"Highest level": "Højeste niveau",
"How it works": "Hvordan det virker",
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
"Language": "Sprog",
@@ -60,12 +63,13 @@
"My wishes": "Mine ønsker",
"New password": "Nyt kodeord",
"Next": "Næste",
"Next level": "Næste niveau",
"next level:": "Næste niveau:",
"No content published": "Intet indhold offentliggjort",
"No transactions available": "Ingen tilgængelige transaktioner",
"Not found": "Ikke fundet",
"night": "nat",
"nights": "nætter",
"Nights needed to level up": "Nætter nødvendige for at komme i niveau",
"number": "nummer",
"On your journey": "På din rejse",
"Open": "Åben",
@@ -77,9 +81,10 @@
"Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer",
"Points": "Points",
"Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.",
"Points until next level": "Point indtil næste niveau",
"Points needed to level up": "Point nødvendige for at komme i niveau",
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
"points expiring by": "point udløber den",
"Previous victories": "Tidligere sejre",
"points until next level": "point indtil næste niveau",
"Read more": "Læs mere",
"Read more about the hotel": "Læs mere om hotellet",
"Retype new password": "Gentag den nye adgangskode",
@@ -95,6 +100,7 @@
"Street": "Gade",
"special character": "speciel karakter",
"Total Points": "Samlet antal point",
"Your points": "Dine pointer",
"Transaction date": "Overførselsdato",
"Transactions": "Transaktioner",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",

View File

@@ -5,11 +5,13 @@
"All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet",
"Already a friend?": "Schon ein Freund?",
"Arrival date": "Ankunftsdatum",
"as of today": "Ab heute",
"As our": "Als unsere",
"As our Close Friend": "Als unser enger Freund",
"At the hotel": "Im Hotel",
"Book": "Buch",
"Booking number": "Buchungsnummer",
"by": "bis",
"Cancel": "Stornieren",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sehen Sie sich die in Ihrem Profil gespeicherten Kreditkarten an. Bezahlen Sie mit einer gespeicherten Karte, wenn Sie angemeldet sind, für ein reibungsloseres Web-Erlebnis.",
"Choose room": "Zimmer wählen",
@@ -24,7 +26,7 @@
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
"Country": "Land",
"Country code": "Landesvorwahl",
"Current level": "Aktuelles Level",
"Your current level": "Ihr aktuelles Level",
"Current password": "Aktuelles Passwort",
"characters": "figuren",
"Date of Birth": "Geburtsdatum",
@@ -41,6 +43,7 @@
"From": "Fromm",
"Get inspired": "Lass dich inspirieren",
"Go back to overview": "Zurück zur Übersicht",
"Highest level": "Höchstes Level",
"How it works": "Wie es funktioniert",
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
"Language": "Sprache",
@@ -60,12 +63,13 @@
"My wishes": "Meine Wünsche",
"New password": "Neues Kennwort",
"Next": "Nächste",
"Next level": "Nächste Ebene",
"next level:": "Nächste Ebene:",
"No content published": "Kein Inhalt veröffentlicht",
"No transactions available": "Keine Transaktionen verfügbar",
"Not found": "Nicht gefunden",
"night": "nacht",
"nights": "nächte",
"Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden",
"number": "nummer",
"On your journey": "Auf deiner Reise",
"Open": "Offen",
@@ -77,9 +81,10 @@
"Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein",
"Points": "Punkte",
"Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.",
"Points until next level": "Punkte bis zum nächsten Level",
"Points needed to level up": "Punkte, die zum Levelaufstieg benötigt werden",
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Niveau zu bleiben",
"points expiring by": "punkte verfallen bis zum",
"Previous victories": "Bisherige Siege",
"points until next level": "punkte bis zum nächsten Level",
"Read more": "Mehr lesen",
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
"Retype new password": "Neues Passwort erneut eingeben",
@@ -95,6 +100,7 @@
"Street": "Straße",
"special character": "sonderzeichen",
"Total Points": "Gesamtpunktzahl",
"Your points": "Deine Punkte",
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen",
"Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)",

View File

@@ -5,11 +5,13 @@
"All rooms comes with standard amenities": "All rooms comes with standard amenities",
"Already a friend?": "Already a friend?",
"Arrival date": "Arrival date",
"as of today": "as of today",
"As our": "As our",
"As our Close Friend": "As our Close Friend",
"At the hotel": "At the hotel",
"Book": "Book",
"Booking number": "Booking number",
"by": "by",
"Cancel": "Cancel",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.",
"Choose room": "Choose room",
@@ -24,7 +26,7 @@
"Could not find requested resource": "Could not find requested resource",
"Country": "Country",
"Country code": "Country code",
"Current level": "Current level",
"Your current level": "Your current level",
"Current password": "Current password",
"characters": "characters",
"Date of Birth": "Date of Birth",
@@ -46,6 +48,7 @@
"hotelPages.rooms.roomCard.person": "person",
"hotelPages.rooms.roomCard.persons": "persons",
"hotelPages.rooms.roomCard.seeRoomDetails": "See room details",
"Highest level": "Highest level",
"How it works": "How it works",
"Join Scandic Friends": "Join Scandic Friends",
"Language": "Language",
@@ -65,12 +68,13 @@
"My wishes": "My wishes",
"New password": "New password",
"Next": "Next",
"Next level": "Next level",
"next level:": "next level:",
"No content published": "No content published",
"No transactions available": "No transactions available",
"Not found": "Not found",
"night": "night",
"nights": "nights",
"Nights needed to level up": "Nights needed to level up",
"number": "number",
"On your journey": "On your journey",
"Open": "Open",
@@ -82,9 +86,10 @@
"Please enter a valid phone number": "Please enter a valid phone number",
"Points": "Points",
"Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.",
"Points until next level": "Points until next level",
"Points needed to level up": "Points needed to level up",
"Points needed to stay on level": "Points needed to stay on level",
"points expiring by": "points expiring by",
"Previous victories": "Previous victories",
"points until next level": "points until next level",
"Read more": "Read more",
"Read more about the hotel": "Read more about the hotel",
"Retype new password": "Retype new password",
@@ -100,6 +105,7 @@
"Street": "Street",
"special character": "special character",
"Total Points": "Total Points",
"Your points": "Your points",
"Transaction date": "Transaction date",
"Transactions": "Transactions",
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",

View File

@@ -5,11 +5,13 @@
"All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet",
"Already a friend?": "Oletko jo ystävä?",
"Arrival date": "Saapumispäivä",
"as of today": "tästä päivästä lähtien",
"As our": "Kuin meidän",
"As our Close Friend": "Läheisenä ystävänämme",
"At the hotel": "Hotellissa",
"Book": "Kirja",
"Booking number": "Varausnumero",
"by": "mennessä",
"Cancel": "Peruuttaa",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Tarkista profiiliisi tallennetut luottokortit. Maksa tallennetulla kortilla kirjautuneena, jotta verkkokokemus on sujuvampi.",
"Choose room": "Valitse huone",
@@ -24,7 +26,7 @@
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
"Country": "Maa",
"Country code": "Maatunnus",
"Current level": "Nykyinen taso",
"Your current level": "Nykyinen tasosi",
"Current password": "Nykyinen salasana",
"characters": "hahmoja",
"Date of Birth": "Syntymäaika",
@@ -41,6 +43,7 @@
"From": "From",
"Get inspired": "Inspiroidu",
"Go back to overview": "Palaa yleiskatsaukseen",
"Highest level": "Korkein taso",
"How it works": "Kuinka se toimii",
"Join Scandic Friends": "Liity Scandic Friends",
"Language": "Kieli",
@@ -60,12 +63,13 @@
"My wishes": "Toiveeni",
"New password": "Uusi salasana",
"Next": "Seuraava",
"Next level": "Seuraava taso",
"next level:": "Seuraava taso:",
"No content published": "Ei julkaistua sisältöä",
"No transactions available": "Ei tapahtumia saatavilla",
"Not found": "Ei löydetty",
"night": "yö",
"nights": "yöt",
"Nights needed to level up": "Yöt, joita tarvitaan tasolle",
"number": "määrä",
"On your journey": "Matkallasi",
"Open": "Avata",
@@ -77,9 +81,10 @@
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
"Points": "Pisteet",
"Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.",
"Points until next level": "Pisteitä seuraavalle tasolle",
"Points needed to level up": "Pisteitä tarvitaan tasolle pääsemiseksi",
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
"points expiring by": "pisteet vanhenevat viimeistään",
"Previous victories": "Edelliset voitot",
"points until next level": "pisteitä seuraavalle tasolle",
"Read more": "Lue lisää",
"Read more about the hotel": "Lue lisää hotellista",
"Retype new password": "Kirjoita uusi salasana uudelleen",
@@ -95,6 +100,7 @@
"Street": "Katu",
"special character": "erikoishahmo",
"Total Points": "Kokonaispisteet",
"Your points": "Sinun pisteesi",
"Transaction date": "Tapahtuman päivämäärä",
"Transactions": "Tapahtumat",
"Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)",

View File

@@ -5,11 +5,13 @@
"All rooms comes with standard amenities": "Alle rommene har standard fasiliteter",
"Already a friend?": "Allerede en venn?",
"Arrival date": "Ankomstdato",
"as of today": "per idag",
"As our": "Som vår",
"As our Close Friend": "Som vår nære venn",
"At the hotel": "På hotellet",
"Book": "Bok",
"Booking number": "Bestillingsnummer",
"by": "innen",
"Cancel": "Avbryt",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Sjekk ut kredittkortene som er lagret på profilen din. Betal med et lagret kort når du er pålogget for en jevnere nettopplevelse.",
"Choose room": "Velg rom",
@@ -24,7 +26,7 @@
"Could not find requested resource": "Kunne ikke finne den forespurte ressursen",
"Country": "Land",
"Country code": "Landskode",
"Current level": "Nåværende nivå",
"Your current level": "Ditt nåværende nivå",
"Current password": "Nåværende passord",
"characters": "tegn",
"Date of Birth": "Fødselsdato",
@@ -41,6 +43,7 @@
"From": "Fra",
"Get inspired": "Bli inspirert",
"Go back to overview": "Gå tilbake til oversikten",
"Highest level": "Høyeste nivå",
"How it works": "Hvordan det fungerer",
"Join Scandic Friends": "Bli med i Scandic Friends",
"Language": "Språk",
@@ -60,12 +63,13 @@
"My wishes": "Mine ønsker",
"New password": "Nytt passord",
"Next": "Neste",
"Next level": "Neste nivå",
"next level:": "Neste nivå:",
"No content published": "Ingen innhold publisert",
"No transactions available": "Ingen transaksjoner tilgjengelig",
"Not found": "Ikke funnet",
"night": "natt",
"nights": "netter",
"Nights needed to level up": "Netter som trengs for å komme opp i nivå",
"number": "antall",
"On your journey": "På reisen din",
"Open": "Åpen",
@@ -77,9 +81,10 @@
"Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer",
"Points": "Poeng",
"Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.",
"Points until next level": "Poeng til neste nivå",
"Points needed to level up": "Poeng som trengs for å komme opp i nivå",
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
"points expiring by": "poeng utløper innen",
"Previous victories": "Tidligere seire",
"points until next level": "poeng til neste nivå",
"Read more": "Les mer",
"Read more about the hotel": "Les mer om hotellet",
"Retype new password": "Skriv inn nytt passord på nytt",
@@ -95,6 +100,7 @@
"Street": "Gate",
"special character": "spesiell karakter",
"Total Points": "Totale poeng",
"Your points": "Dine poeng",
"Transaction date": "Transaksjonsdato",
"Transactions": "Transaksjoner",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",

View File

@@ -5,11 +5,13 @@
"All rooms comes with standard amenities": "Alla rum har standardbekvämligheter",
"Already a friend?": "Redan en vän?",
"Arrival date": "Ankomstdatum",
"as of today": "från och med idag",
"As our": "Som vår",
"As our Close Friend": "Som vår nära vän",
"At the hotel": "På hotellet",
"Book": "Boka",
"Booking number": "Bokningsnummer",
"by": "innan",
"Cancel": "Avbryt",
"Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.": "Kolla in kreditkorten som sparats i din profil. Betala med ett sparat kort när du är inloggad för en smidigare webbupplevelse.",
"Choose room": "Välj rum",
@@ -24,7 +26,7 @@
"Could not find requested resource": "Det gick inte att hitta den begärda resursen",
"Country": "Land",
"Country code": "Landskod",
"Current level": "Nuvarande nivå",
"Your current level": "Din nuvarande nivå",
"Current password": "Nuvarande lösenord",
"characters": "tecken",
"Date of Birth": "Födelsedatum",
@@ -41,6 +43,7 @@
"From": "Från",
"Get inspired": "Bli inspirerad",
"Go back to overview": "Gå tillbaka till översikten",
"Highest level": "Högsta nivå",
"How it works": "Hur det fungerar",
"hotelPages.rooms.title": "Rum",
"hotelPages.rooms.roomCard.person": "person",
@@ -64,12 +67,13 @@
"My wishes": "Mina önskningar",
"New password": "Nytt lösenord",
"Next": "Nästa",
"Next level": "Nästa nivå",
"next level:": "Nästa nivå:",
"No content published": "Inget innehåll publicerat",
"No transactions available": "Inga transaktioner tillgängliga",
"Not found": "Hittades inte",
"night": "natt",
"nights": "nätter",
"Nights needed to level up": "Nätter som behövs för att gå upp i nivå",
"number": "nummer",
"On your journey": "På din resa",
"Open": "Öppna",
@@ -81,9 +85,10 @@
"Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer",
"Points": "Poäng",
"Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.",
"Points until next level": "Poäng till nästa nivå",
"Points needed to level up": "Poäng som behövs för att gå upp i nivå",
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
"points expiring by": "poäng förfaller till",
"Previous victories": "Tidigare segrar",
"points until next level": "poäng till nästa nivå",
"Read more": "Läs mer",
"Read more about the hotel": "Läs mer om hotellet",
"Retype new password": "Upprepa nytt lösenord",
@@ -99,6 +104,7 @@
"Street": "Gata",
"special character": "speciell karaktär",
"Total Points": "Total poäng",
"Your points": "Dina poäng",
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktioner",
"Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)",

View File

@@ -25,6 +25,8 @@ export const getUserSchema = z.object({
membershipLevel: z.nativeEnum(MembershipLevelEnum).optional(),
memberSince: z.string(),
membershipType: z.string(),
nextLevel: z.string().optional(),
pointsRequiredToNextlevel: z.number().optional(),
})
),
phoneNumber: z.string().optional(),

View File

@@ -3,3 +3,7 @@ import { membershipLevels } from "@/constants/membershipLevels"
export type MembershipLevelProps = {
level: membershipLevels
}
export type CopyButtonProps = {
membershipNumber: string
}

View File

@@ -1,5 +1,6 @@
export enum DynamicContentComponents {
membership_overview = "membership_overview",
points_overview = "points_overview",
soonest_stays = "soonest_stays",
previous_stays = "previous_stays",
upcoming_stays = "upcoming_stays",

View File

@@ -0,0 +1,16 @@
interface PointsOrNightColumn {
title?: string
subtitle?: string
subtitleParam?: string
}
export interface PointsColumn extends PointsOrNightColumn {
points: number | undefined
nights?: never
}
export interface NightsColumn extends PointsOrNightColumn {
points?: never
nights: number | undefined
}
export type PointsColumnProps = PointsColumn | NightsColumn

View File

@@ -8,13 +8,9 @@ import levelsData from "@/components/Loyalty/Blocks/DynamicContent/LoyaltyLevels
export function getMembershipLevelObject(
membershipLevel: MembershipLevelEnum,
lang: Lang,
level: "currentLevel" | "nextLevel" = "currentLevel"
lang: Lang
) {
let levelNumber = membershipLevels[membershipLevel]
if (level === "nextLevel") {
levelNumber += 1
}
return levelsData[lang].levels.find((level) => level.level === levelNumber)
return levelsData[lang].levels.find(
(level) => level.level === membershipLevels[membershipLevel]
)
}

View File

@@ -1,5 +1,6 @@
import { z } from "zod"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
import { getMembershipCardsSchema } from "@/server/routers/user/output"
import { User } from "@/types/user"
@@ -29,6 +30,12 @@ export function getMembershipCards(
})
}
export function isHighestMembership(
membershipLevel: MembershipLevelEnum | undefined
) {
return membershipLevel == MembershipLevelEnum.L7
}
export function getInitials(
firstName: User["firstName"],
lastName: User["lastName"]