feat(SW-66, SW-348): search functionality and ui
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import CopyIcon from "@/components/Icons/Copy"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import styles from "./copybutton.module.css"
|
||||
|
||||
import type { CopyButtonProps } from "@/types/components/myPages/membership"
|
||||
|
||||
export default function CopyButton({ membershipNumber }: CopyButtonProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(membershipNumber)
|
||||
toast.success(
|
||||
intl.formatMessage({ id: "Membership ID copied to clipboard" })
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleCopy}
|
||||
className={styles.button}
|
||||
type="button"
|
||||
variant="icon"
|
||||
size="small"
|
||||
intent="tertiary"
|
||||
>
|
||||
<CopyIcon color="pale" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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> {}
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { membershipLevels } from "@/constants/membershipLevels"
|
||||
|
||||
import {
|
||||
BestFriend,
|
||||
CloseFriend,
|
||||
DearFriend,
|
||||
GoodFriend,
|
||||
LoyalFriend,
|
||||
NewFriend,
|
||||
TrueFriend,
|
||||
} from "@/components/Levels"
|
||||
|
||||
import styles from "./membershipLevel.module.css"
|
||||
|
||||
import type { MembershipLevelProps } from "@/types/components/myPages/membership"
|
||||
|
||||
export default function MembershipLevel({ level }: MembershipLevelProps) {
|
||||
switch (level) {
|
||||
case membershipLevels.L1:
|
||||
return <NewFriend className={styles.level} color="pale" />
|
||||
case membershipLevels.L2:
|
||||
return <GoodFriend className={styles.level} color="pale" />
|
||||
case membershipLevels.L3:
|
||||
return <CloseFriend className={styles.level} color="pale" />
|
||||
case membershipLevels.L4:
|
||||
return <DearFriend className={styles.level} color="pale" />
|
||||
case membershipLevels.L5:
|
||||
return <LoyalFriend className={styles.level} color="pale" />
|
||||
case membershipLevels.L6:
|
||||
return <TrueFriend className={styles.level} color="pale" />
|
||||
case membershipLevels.L7:
|
||||
return <BestFriend className={styles.level} color="pale" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.level {
|
||||
height: 105px;
|
||||
width: 219px;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import CopyButton from "../../Buttons/CopyButton"
|
||||
import { membershipNumberVariants } from "./membershipNumberVariants"
|
||||
|
||||
import styles from "./membershipNumber.module.css"
|
||||
|
||||
import type { MembershipNumberProps } from "@/types/components/myPages/membershipNumber"
|
||||
|
||||
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" asChild>
|
||||
<code>{membership?.membershipNumber ?? "N/A"}</code>
|
||||
</Caption>
|
||||
{membership && (
|
||||
<CopyButton membershipNumber={membership.membershipNumber} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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-x2) 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
.friend {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x4);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.levelLabel {
|
||||
position: relative;
|
||||
transform: rotate(-13deg) translate(0px, -15px);
|
||||
margin-left: -35px;
|
||||
}
|
||||
|
||||
.friend .name {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.membership {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
52
components/Blocks/DynamicContent/Overview/Friend/index.tsx
Normal file
52
components/Blocks/DynamicContent/Overview/Friend/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { membershipLevels } from "@/constants/membershipLevels"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { isHighestMembership } from "@/utils/user"
|
||||
|
||||
import MembershipLevel from "./MembershipLevel"
|
||||
|
||||
import styles from "./friend.module.css"
|
||||
|
||||
import type { FriendProps } from "@/types/components/myPages/friend"
|
||||
|
||||
export default async function Friend({
|
||||
children,
|
||||
membership,
|
||||
name,
|
||||
}: FriendProps) {
|
||||
const { formatMessage } = await getIntl()
|
||||
if (!membership?.membershipLevel) {
|
||||
return null
|
||||
}
|
||||
// @ts-expect-error: membershiplevel needs proper fix
|
||||
const isHighestLevel = isHighestMembership(membership.membershipLevel)
|
||||
|
||||
return (
|
||||
<section className={styles.friend}>
|
||||
<header className={styles.header}>
|
||||
<Body color="white" textTransform="bold" textAlign="center">
|
||||
{formatMessage(
|
||||
isHighestLevel
|
||||
? { id: "Highest level" }
|
||||
: // @ts-expect-error: membershiplevel needs proper fix
|
||||
{ id: `Level ${membershipLevels[membership.membershipLevel]}` }
|
||||
)}
|
||||
</Body>
|
||||
{membership ? (
|
||||
<MembershipLevel
|
||||
// @ts-expect-error: membershiplevel needs proper fix
|
||||
level={membershipLevels[membership.membershipLevel]}
|
||||
/>
|
||||
) : null}
|
||||
</header>
|
||||
<div className={styles.membership}>
|
||||
<Title className={styles.name} color="pale" level="h3">
|
||||
{name}
|
||||
</Title>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import type { UserProps } from "@/types/components/myPages/user"
|
||||
|
||||
export default async function ExpiringPoints({ user }: UserProps) {
|
||||
const intl = await getIntl()
|
||||
const membership = getMembership(user.memberships)
|
||||
|
||||
if (!membership || !membership.pointsToExpire) {
|
||||
// TODO: handle this case?
|
||||
return null
|
||||
}
|
||||
|
||||
// sv hardcoded to force space on thousands
|
||||
const formatter = new Intl.NumberFormat(Lang.sv)
|
||||
const d = dt(membership.pointsExpiryDate)
|
||||
|
||||
const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD"
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Body color="white" textTransform="bold" textAlign="center">
|
||||
{intl.formatMessage(
|
||||
{ id: "spendable points expiring by" },
|
||||
{
|
||||
points: formatter.format(membership.pointsToExpire),
|
||||
date: d.format(dateFormat),
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
.points {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x5);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.points {
|
||||
grid-auto-flow: column;
|
||||
row-gap: 0;
|
||||
column-gap: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import styles from "./container.module.css"
|
||||
|
||||
export default function PointsContainer({ children }: React.PropsWithChildren) {
|
||||
return <section className={styles.points}>{children}</section>
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
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 to spend",
|
||||
subtitle: "as of today",
|
||||
})
|
||||
|
||||
export const NextLevelPointsColumn = ({ points, subtitle }: PointsColumn) =>
|
||||
PointsColumn({
|
||||
points,
|
||||
title: "Points needed to level up",
|
||||
subtitle,
|
||||
})
|
||||
|
||||
export const StayOnLevelColumn = ({ points, subtitle }: PointsColumn) =>
|
||||
PointsColumn({
|
||||
points,
|
||||
title: "Points needed to stay on level",
|
||||
subtitle,
|
||||
})
|
||||
|
||||
export const NextLevelNightsColumn = ({ nights, subtitle }: NightsColumn) =>
|
||||
PointsColumn({
|
||||
nights,
|
||||
title: "Nights needed to level up",
|
||||
subtitle,
|
||||
})
|
||||
|
||||
async function PointsColumn({
|
||||
points,
|
||||
nights,
|
||||
title,
|
||||
subtitle,
|
||||
}: PointsColumnProps) {
|
||||
const { formatMessage } = await getIntl()
|
||||
|
||||
return (
|
||||
<article className={styles.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>
|
||||
{subtitle ? (
|
||||
<Body color="white" textAlign="center">
|
||||
{subtitle}
|
||||
</Body>
|
||||
) : null}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@media screen and (min-width: 768px) {
|
||||
.firstRow {
|
||||
align-content: flex-end;
|
||||
}
|
||||
|
||||
.article {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { getMembershipLevelObject } from "@/utils/membershipLevel"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import PointsContainer from "./Container"
|
||||
import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn"
|
||||
|
||||
import { UserProps } from "@/types/components/myPages/user"
|
||||
|
||||
export default async function Points({ user }: UserProps) {
|
||||
const { formatMessage } = await getIntl()
|
||||
|
||||
const membership = getMembership(user.memberships)
|
||||
const nextLevel = getMembershipLevelObject(
|
||||
membership?.nextLevel as MembershipLevelEnum,
|
||||
getLang()
|
||||
)
|
||||
|
||||
return (
|
||||
<PointsContainer>
|
||||
<YourPointsColumn points={membership?.currentPoints} />
|
||||
{nextLevel && (
|
||||
<NextLevelPointsColumn
|
||||
points={membership?.pointsRequiredToNextlevel}
|
||||
subtitle={`${formatMessage({ id: "next level:" })} ${nextLevel.name}`}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: Show NextLevelNightsColumn when nightsToTopTier data is correct from Antavo */}
|
||||
{/* {membership?.nightsToTopTier && (
|
||||
<NextLevelNightsColumn
|
||||
nights={membership.nightsToTopTier}
|
||||
subtitle={
|
||||
membership.tierExpirationDate &&
|
||||
`by ${membership.tierExpirationDate}`
|
||||
}
|
||||
/>
|
||||
)} */}
|
||||
</PointsContainer>
|
||||
)
|
||||
}
|
||||
18
components/Blocks/DynamicContent/Overview/Stats/index.tsx
Normal file
18
components/Blocks/DynamicContent/Overview/Stats/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import ExpiringPoints from "./ExpiringPoints"
|
||||
import Points from "./Points"
|
||||
|
||||
import styles from "./stats.module.css"
|
||||
|
||||
import type { UserProps } from "@/types/components/myPages/user"
|
||||
|
||||
export default function Stats({ user }: UserProps) {
|
||||
return (
|
||||
<section className={styles.stats}>
|
||||
<Points user={user} />
|
||||
<Divider variant="default" color="pale" />
|
||||
<ExpiringPoints user={user} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.stats {
|
||||
align-content: center;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.stats {
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
}
|
||||
40
components/Blocks/DynamicContent/Overview/index.tsx
Normal file
40
components/Blocks/DynamicContent/Overview/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
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 Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import Hero from "./Friend/Hero"
|
||||
import MembershipNumber from "./Friend/MembershipNumber"
|
||||
import Friend from "./Friend"
|
||||
import Stats from "./Stats"
|
||||
|
||||
import styles from "./overview.module.css"
|
||||
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default async function Overview({
|
||||
link,
|
||||
subtitle,
|
||||
title,
|
||||
}: AccountPageComponentProps) {
|
||||
const user = await getProfile()
|
||||
if (!user || "error" in user) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<SectionHeader link={link} preamble={subtitle} title={title} topTitle />
|
||||
<Hero color="red">
|
||||
<Friend membership={user.membership} name={user.name}>
|
||||
<MembershipNumber color="burgundy" membership={user.membership} />
|
||||
</Friend>
|
||||
<Divider className={styles.divider} color="peach" />
|
||||
<Stats user={user} />
|
||||
</Hero>
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.divider {
|
||||
padding-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.container {
|
||||
/* Full-width override styling */
|
||||
left: 50%;
|
||||
margin-left: -50vw;
|
||||
margin-right: -50vw;
|
||||
padding: 0 var(--Spacing-x2);
|
||||
position: relative;
|
||||
right: 50%;
|
||||
width: 100dvw;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user