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

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;
}