From 238de4cd3a5d9e561182e69aaa9da171d68b3289 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 5 Sep 2024 09:28:25 +0000 Subject: [PATCH] Merged in feat/CJ-17-points-expiration-table (pull request #527) Feat/CJ-17 points expiration table * feat(CJ-17): Added point expiration table and refactored to use Table component * feat(CJ-17): Use Table component inside Row * fix(CJ-117): Added missing css class and update date formatting * fix(CJ-117): Added copy of membershipLevel route with a protectedProcedure Approved-by: Christel Westerberg --- .../Loyalty/Blocks/DynamicContent/index.tsx | 2 +- components/MyPages/AccountPage/Content.tsx | 8 +-- .../AwardPoints/awardPoints.module.css | 23 +++++++ .../awardPointsVariants.ts | 4 +- .../AwardPoints.tsx => AwardPoints/index.tsx} | 12 ++-- .../EarnAndBurn/JourneyTable/Client.tsx | 4 +- .../{Table => ClientTable}/Row/index.tsx | 31 +++++---- .../ClientTable/clientTable.module.css | 18 +++++ .../JourneyTable/ClientTable/index.tsx | 57 ++++++++++++++++ .../JourneyTable/Table/Row/row.module.css | 45 ------------ .../EarnAndBurn/JourneyTable/Table/index.tsx | 68 ------------------- .../JourneyTable/Table/table.module.css | 62 ----------------- .../expiringPointsTable.module.css | 12 ++++ .../ExpiringPointsTable/index.tsx | 48 +++++++++++++ .../Blocks/Points/ExpiringPoints/index.tsx | 47 +++++++------ components/TempDesignSystem/Table/TD.tsx | 11 ++- components/TempDesignSystem/Table/TR.tsx | 11 ++- .../TempDesignSystem/Table/table.module.css | 16 ++++- server/routers/user/query.ts | 11 ++- .../components/myPages/myPage/earnAndBurn.ts | 4 +- 20 files changed, 260 insertions(+), 234 deletions(-) create mode 100644 components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPoints.module.css rename components/MyPages/Blocks/Points/EarnAndBurn/{JourneyTable/Table/Row => AwardPoints}/awardPointsVariants.ts (59%) rename components/MyPages/Blocks/Points/EarnAndBurn/{JourneyTable/Table/Row/AwardPoints.tsx => AwardPoints/index.tsx} (75%) rename components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/{Table => ClientTable}/Row/index.tsx (82%) create mode 100644 components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css create mode 100644 components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx delete mode 100644 components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css delete mode 100644 components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx delete mode 100644 components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css create mode 100644 components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/expiringPointsTable.module.css create mode 100644 components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/index.tsx diff --git a/components/Loyalty/Blocks/DynamicContent/index.tsx b/components/Loyalty/Blocks/DynamicContent/index.tsx index 79908505c..48b488dbf 100644 --- a/components/Loyalty/Blocks/DynamicContent/index.tsx +++ b/components/Loyalty/Blocks/DynamicContent/index.tsx @@ -19,7 +19,7 @@ import type { import { LoyaltyComponentEnum } from "@/types/components/loyalty/enums" async function DynamicComponentBlock({ component }: DynamicComponentProps) { - const membershipLevel = await serverClient().user.membershipLevel() + const membershipLevel = await serverClient().user.safeMembershipLevel() switch (component) { case LoyaltyComponentEnum.how_it_works: return diff --git a/components/MyPages/AccountPage/Content.tsx b/components/MyPages/AccountPage/Content.tsx index 69568bc1f..3ccff01d0 100644 --- a/components/MyPages/AccountPage/Content.tsx +++ b/components/MyPages/AccountPage/Content.tsx @@ -3,6 +3,8 @@ import CurrentBenefitsBlock from "@/components/MyPages/Blocks/Benefits/CurrentLe import NextLevelBenefitsBlock from "@/components/MyPages/Blocks/Benefits/NextLevel" import Overview from "@/components/MyPages/Blocks/Overview" import EarnAndBurn from "@/components/MyPages/Blocks/Points/EarnAndBurn" +import ExpiringPoints from "@/components/MyPages/Blocks/Points/ExpiringPoints" +import PointsOverview from "@/components/MyPages/Blocks/Points/Overview" import Shortcuts from "@/components/MyPages/Blocks/Shortcuts" import PreviousStays from "@/components/MyPages/Blocks/Stays/Previous" import SoonestStays from "@/components/MyPages/Blocks/Stays/Soonest" @@ -10,8 +12,6 @@ import UpcomingStays from "@/components/MyPages/Blocks/Stays/Upcoming" import { getLang } from "@/i18n/serverContext" import { removeMultipleSlashes } from "@/utils/url" -import PointsOverview from "../Blocks/Points/Overview" - import { AccountPageContentProps, ContentProps, @@ -38,9 +38,7 @@ function DynamicComponent({ component, props }: AccountPageContentProps) { case DynamicContentComponents.next_benefits: return case DynamicContentComponents.expiring_points: - // TODO: Add once available - // return - return null + return case DynamicContentComponents.earn_and_burn: return default: diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPoints.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPoints.module.css new file mode 100644 index 000000000..b0cc58b66 --- /dev/null +++ b/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPoints.module.css @@ -0,0 +1,23 @@ +.awardPoints { + color: var(--Base-Text-High-contrast); +} + +.addition { + color: var(--Secondary-Light-On-Surface-Accent); +} + +.addition::before { + color: var(--Secondary-Light-On-Surface-Accent); + content: "+"; + margin-right: var(--Spacing-x-half); +} + +.negation { + color: var(--Base-Text-Accent); +} + +.negation::before { + color: var(--Base-Text-Accent); + content: "-"; + margin-right: var(--Spacing-x-half); +} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants.ts b/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPointsVariants.ts similarity index 59% rename from components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants.ts rename to components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPointsVariants.ts index 0dbf37606..cca6721f1 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants.ts +++ b/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPointsVariants.ts @@ -1,8 +1,8 @@ import { cva } from "class-variance-authority" -import styles from "./row.module.css" +import styles from "./awardPoints.module.css" -export const awardPointsVariants = cva(styles.td, { +export const awardPointsVariants = cva(styles.awardPoints, { variants: { variant: { addition: styles.addition, diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/AwardPoints.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/index.tsx similarity index 75% rename from components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/AwardPoints.tsx rename to components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/index.tsx index a2d281dca..119324806 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/AwardPoints.tsx +++ b/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/index.tsx @@ -2,6 +2,8 @@ import { useIntl } from "react-intl" import { Lang } from "@/constants/languages" +import Body from "@/components/TempDesignSystem/Text/Body" + import { awardPointsVariants } from "./awardPointsVariants" import type { AwardPointsVariantProps } from "@/types/components/myPages/myPage/earnAndBurn" @@ -9,14 +11,16 @@ import type { AwardPointsVariantProps } from "@/types/components/myPages/myPage/ export default function AwardPoints({ awardPoints, isCalculated, + isExpiringPoints = false, }: { awardPoints: number isCalculated: boolean + isExpiringPoints?: boolean }) { - let variant: AwardPointsVariantProps["variant"] = undefined + let variant: AwardPointsVariantProps["variant"] = null const intl = useIntl() - if (isCalculated) { + if (isCalculated && !isExpiringPoints) { if (awardPoints > 0) { variant = "addition" } else if (awardPoints < 0) { @@ -31,10 +35,10 @@ export default function AwardPoints({ // sv hardcoded to force space on thousands const formatter = new Intl.NumberFormat(Lang.sv) return ( - + {isCalculated ? formatter.format(awardPoints) : intl.formatMessage({ id: "Points being calculated" })} - + ) } diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx index 3e08de171..eca762942 100644 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Client.tsx @@ -7,8 +7,8 @@ import { trpc } from "@/lib/trpc/client" import LoadingSpinner from "@/components/LoadingSpinner" +import ClientTable from "./ClientTable" import Pagination from "./Pagination" -import Table from "./Table" import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn" @@ -39,7 +39,7 @@ export default function TransactionTable({ ) : ( <> - + {data && data.meta.totalPages > 1 ? ( - - - - - + + ) } diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css new file mode 100644 index 000000000..b0205a813 --- /dev/null +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css @@ -0,0 +1,18 @@ +.container { + overflow-x: auto; + border-radius: var(--Corner-radius-Small); +} + +.placeholder { + width: 100%; + padding: 24px; + text-align: center; + border: 1px solid var(--Scandic-Brand-Pale-Peach); + background-color: #fff; +} + +@media screen and (min-width: 768px) { + .container { + border-radius: var(--Corner-radius-Large); + } +} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx new file mode 100644 index 000000000..cc688b21d --- /dev/null +++ b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx @@ -0,0 +1,57 @@ +"use client" + +import { useIntl } from "react-intl" + +import Table from "@/components/TempDesignSystem/Table" +import Body from "@/components/TempDesignSystem/Text/Body" + +import Row from "./Row" + +import styles from "./clientTable.module.css" + +import type { ClientTableProps } from "@/types/components/myPages/myPage/earnAndBurn" + +const tableHeadings = [ + "Points", + "Description", + "Booking number", + "Arrival date", +] + +export default function ClientTable({ transactions }: ClientTableProps) { + const intl = useIntl() + + return ( +
+
{description}{renderConfirmationNumber()} + + + + + + {description} + + {renderConfirmationNumber()} + {transaction.checkinDate && transaction.confirmationNumber !== "BALFWD" ? arrival : null} -
+ + + {tableHeadings.map((heading) => ( + + + {intl.formatMessage({ id: heading })} + + + ))} + + + + {transactions.length ? ( + transactions.map((transaction, index) => ( + + )) + ) : ( + + + {intl.formatMessage({ id: "No transactions available" })} + + + )} + +
+ + ) +} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css deleted file mode 100644 index eb59b55ea..000000000 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/row.module.css +++ /dev/null @@ -1,45 +0,0 @@ -.tr { - border-bottom: 1px solid var(--Scandic-Brand-Pale-Peach); - &:last-child { - border-bottom: none; - } -} - -.td { - background-color: #fff; - color: var(--UI-Text-High-contrast); - padding: var(--Spacing-x2); - position: relative; - text-align: left; - text-wrap: nowrap; -} - -.description { - font-weight: var(--typography-Body-Bold-fontWeight); -} - -.addition { - color: var(--Secondary-Light-On-Surface-Accent); -} - -.addition::before { - color: var(--Secondary-Light-On-Surface-Accent); - content: "+"; - margin-right: var(--Spacing-x-half); -} - -.negation { - color: var(--Base-Text-Accent); -} - -.negation::before { - color: var(--Base-Text-Accent); - content: "-"; - margin-right: var(--Spacing-x-half); -} - -@media screen and (min-width: 768px) { - .td { - padding: var(--Spacing-x3); - } -} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx deleted file mode 100644 index c49695e88..000000000 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client" - -import { useIntl } from "react-intl" - -import Body from "@/components/TempDesignSystem/Text/Body" - -import Row from "./Row" - -import styles from "./table.module.css" - -import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn" - -const tableHeadings = [ - "Points", - "Description", - "Booking number", - "Arrival date", -] - -export default function Table({ transactions }: TableProps) { - const intl = useIntl() - return ( -
- {transactions.length ? ( - - - - {tableHeadings.map((heading) => ( - - ))} - - - - {transactions.map((transaction, index) => ( - - ))} - -
- - {intl.formatMessage({ id: heading })} - -
- ) : ( - - - - {tableHeadings.map((heading) => ( - - ))} - - - - - - - -
- {heading} -
- {intl.formatMessage({ id: "No transactions available" })} -
- )} -
- ) -} diff --git a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css b/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css deleted file mode 100644 index 8c319b619..000000000 --- a/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/table.module.css +++ /dev/null @@ -1,62 +0,0 @@ -.container { - display: flex; - flex-direction: column; - overflow-x: auto; - border-radius: var(--Corner-radius-Small); -} - -.table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; -} - -.thead { - background-color: var(--Scandic-Brand-Pale-Peach); - border-left: 1px solid var(--Scandic-Brand-Pale-Peach); - border-right: 1px solid var(--Scandic-Brand-Pale-Peach); - color: var(--Main-Brand-Burgundy); -} - -.th { - text-align: left; - text-wrap: nowrap; - padding: var(--Spacing-x2); -} - -.placeholder { - width: 100%; - padding: 24px; - text-align: center; - border: 1px solid var(--Scandic-Brand-Pale-Peach); - background-color: #fff; -} - -.footer { - background-color: var(--Scandic-Brand-Pale-Peach); - border-left: 1px solid var(--Scandic-Brand-Pale-Peach); - border-right: 1px solid var(--Scandic-Brand-Pale-Peach); - display: flex; - padding: 20px 32px; - justify-content: center; -} - -.loadMoreButton { - border: none; - background-color: transparent; - color: var(--Main-Brand-Burgundy); - font-size: var(--typography-Caption-Bold-Desktop-fontSize); - display: flex; - align-items: center; - gap: var(--Spacing-x-half); - cursor: pointer; -} -@media screen and (min-width: 768px) { - .container { - border-radius: var(--Corner-radius-Large); - } - - .th { - padding: var(--Spacing-x2) var(--Spacing-x3); - } -} diff --git a/components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/expiringPointsTable.module.css b/components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/expiringPointsTable.module.css new file mode 100644 index 000000000..21d7724ab --- /dev/null +++ b/components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/expiringPointsTable.module.css @@ -0,0 +1,12 @@ +.container { + display: flex; + flex-direction: column; + overflow-x: auto; + border-radius: var(--Corner-radius-Small); +} + +@media screen and (min-width: 768px) { + .container { + border-radius: var(--Corner-radius-Large); + } +} diff --git a/components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/index.tsx b/components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/index.tsx new file mode 100644 index 000000000..1113d638d --- /dev/null +++ b/components/MyPages/Blocks/Points/ExpiringPoints/ExpiringPointsTable/index.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import AwardPoints from "@/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints" +import Table from "@/components/TempDesignSystem/Table" +import Body from "@/components/TempDesignSystem/Text/Body" +import useLang from "@/hooks/useLang" + +const tableHeadings = ["Points", "Expiration Date"] + +export default function ExpiringPointsTable({ + points, + expirationDate, +}: { + points: number + expirationDate: string +}) { + const intl = useIntl() + const lang = useLang() + const expiration = dt(expirationDate).locale(lang).format("DD MMM YYYY") + + return ( + + + + {tableHeadings.map((heading) => ( + + + {intl.formatMessage({ id: heading })} + + + ))} + + + + + + + + {expiration} + + +
+ ) +} diff --git a/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx b/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx index d550a8996..239002402 100644 --- a/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx +++ b/components/MyPages/Blocks/Points/ExpiringPoints/index.tsx @@ -1,27 +1,30 @@ -import { getIntl } from "@/i18n" +import { serverClient } from "@/lib/trpc/server" -import styles from "./expiringPoints.module.css" +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" + +import ExpiringPointsTable from "./ExpiringPointsTable" + +import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage" + +export default async function ExpiringPoints({ + link, + subtitle, + title, +}: AccountPageComponentProps) { + const membershipLevel = await serverClient().user.membershipLevel() + + if (!membershipLevel?.pointsToExpire || !membershipLevel?.pointsExpiryDate) { + return null + } -export async function ExpiringPoints() { - const { formatMessage } = await getIntl() return ( - - - - - - - - - - - - - - - - - -
{formatMessage({ id: "Arrival date" })}{formatMessage({ id: "Points" })}
23 May 202330000
23 May 2023-15000
+ + + + ) } diff --git a/components/TempDesignSystem/Table/TD.tsx b/components/TempDesignSystem/Table/TD.tsx index 3e912e3f2..210437ecf 100644 --- a/components/TempDesignSystem/Table/TD.tsx +++ b/components/TempDesignSystem/Table/TD.tsx @@ -1,7 +1,14 @@ import styles from "./table.module.css" -function TD({ children }: React.PropsWithChildren) { - return {children} +function TD({ + children, + ...rest +}: React.PropsWithChildren>) { + return ( + + {children} + + ) } export default TD diff --git a/components/TempDesignSystem/Table/TR.tsx b/components/TempDesignSystem/Table/TR.tsx index a48ec8733..d6b12ded8 100644 --- a/components/TempDesignSystem/Table/TR.tsx +++ b/components/TempDesignSystem/Table/TR.tsx @@ -1,7 +1,14 @@ import styles from "./table.module.css" -function TR({ children }: React.PropsWithChildren) { - return {children} +function TR({ + children, + ...rest +}: React.PropsWithChildren>) { + return ( + + {children} + + ) } export default TR diff --git a/components/TempDesignSystem/Table/table.module.css b/components/TempDesignSystem/Table/table.module.css index 9ad18553c..fcbb3fec2 100644 --- a/components/TempDesignSystem/Table/table.module.css +++ b/components/TempDesignSystem/Table/table.module.css @@ -18,10 +18,22 @@ } .th { - padding: var(--Spacing-x2) var(--Spacing-x3); + padding: var(--Spacing-x2); + text-align: left; + text-wrap: nowrap; } .td { - padding: var(--Spacing-x3); + padding: var(--Spacing-x2); +} + +@media screen and (min-width: 768px) { + .th { + padding: var(--Spacing-x2) var(--Spacing-x3); + } + + .td { + padding: var(--Spacing-x3); + } } diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 287ebee0b..94312edc4 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -357,7 +357,16 @@ export const userQueryRouter = router({ lastName: verifiedData.data.lastName, } }), - membershipLevel: safeProtectedProcedure.query(async function ({ ctx }) { + membershipLevel: protectedProcedure.query(async function ({ ctx }) { + const verifiedData = await getVerifiedUser({ session: ctx.session }) + if (!verifiedData || "error" in verifiedData) { + return null + } + + const membershipLevel = getMembership(verifiedData.data.memberships) + return membershipLevel + }), + safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) { if (!ctx.session) { return null } diff --git a/types/components/myPages/myPage/earnAndBurn.ts b/types/components/myPages/myPage/earnAndBurn.ts index 9bc0c69aa..615f7fa29 100644 --- a/types/components/myPages/myPage/earnAndBurn.ts +++ b/types/components/myPages/myPage/earnAndBurn.ts @@ -1,4 +1,4 @@ -import { awardPointsVariants } from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Table/Row/awardPointsVariants" +import { awardPointsVariants } from "@/components/MyPages/Blocks/Points/EarnAndBurn/AwardPoints/awardPointsVariants" import type { VariantProps } from "class-variance-authority" @@ -23,7 +23,7 @@ export type EarnAndBurnProps = { lang: Lang } -export interface TableProps { +export interface ClientTableProps { transactions: Transactions }