Merged in feat/LOY-391-my-points-transactions-table-design (pull request #3415)
Feat/LOY-391 my points transactions table design * feat(LOY-391): Added new design to point transaction table * fix(LOY-391): rebase fix * fix(LOY-391): fix * fix(LOY-391): fix * fix(LOY-391): fixed sticky header etc. * feat(LOY-391): added focus on the newest loaded item in the list * fix(LOY-391): cleaned up * fix(LOY-391): style fix * fix(LOY-391): fixed PR-comments, types, removed the old files for earn and burn table * fix(LOY-391): fixed PR-comments * feat(LOY-391): added useCallback so scrolling is avoided when clicking see all on expiring points Approved-by: Anton Gunnarsson Approved-by: Matilda Landström
This commit is contained in:
@@ -1,45 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useState } from "react"
|
||||
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import Pagination from "@/components/MyPages/Pagination"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import ClientTable from "./ClientTable"
|
||||
|
||||
export default function TransactionTable() {
|
||||
const limit = 5
|
||||
const [page, setPage] = useState(1)
|
||||
const lang = useLang()
|
||||
const { data, isFetching, isLoading } =
|
||||
trpc.user.transaction.friendTransactions.useQuery(
|
||||
{
|
||||
limit,
|
||||
page,
|
||||
lang,
|
||||
},
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
return isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<>
|
||||
<ClientTable transactions={data?.data.transactions || []} />
|
||||
{data && data.meta.totalPages > 1 ? (
|
||||
<Pagination
|
||||
handlePageChange={setPage}
|
||||
pageCount={data.meta.totalPages}
|
||||
isFetching={isFetching}
|
||||
currentPage={page}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import Table from "@scandic-hotels/design-system/Table"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { Transactions } from "@scandic-hotels/trpc/enums/transactions"
|
||||
|
||||
import { webviews } from "@/constants/routes/webviews"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import AwardPoints from "../../../AwardPoints"
|
||||
|
||||
import type { Transaction } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
interface RowProps {
|
||||
transaction: Transaction
|
||||
}
|
||||
|
||||
export default function Row({ transaction }: RowProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const pathName = usePathname()
|
||||
const isWebview = webviews.includes(pathName)
|
||||
|
||||
const { hotelName, city } = transaction
|
||||
const nightsMsg = intl.formatMessage(
|
||||
{
|
||||
id: "booking.numberOfNights",
|
||||
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
|
||||
},
|
||||
{
|
||||
totalNights: transaction.nights,
|
||||
}
|
||||
)
|
||||
let description =
|
||||
transaction.confirmationNumber === "non-transactional" &&
|
||||
transaction.nights === 0
|
||||
? intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.pointsActivity",
|
||||
defaultMessage: "Point activity",
|
||||
})
|
||||
: hotelName && city
|
||||
? `${hotelName}, ${city} ${nightsMsg}`
|
||||
: `${nightsMsg}`
|
||||
|
||||
switch (transaction.type) {
|
||||
case Transactions.rewardType.stay:
|
||||
case Transactions.rewardType.stayAdj:
|
||||
if (transaction.hotelId === "ORS") {
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.formerScandicHotel",
|
||||
defaultMessage: "Former Scandic Hotel",
|
||||
})
|
||||
}
|
||||
if (transaction.confirmationNumber === "BALFWD") {
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.pointsEarnedPriorMay2021",
|
||||
defaultMessage: "Points earned prior to May 1, 2021",
|
||||
})
|
||||
}
|
||||
break
|
||||
case Transactions.rewardType.ancillary:
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.extrasToBooking",
|
||||
defaultMessage: "Extras to your booking",
|
||||
})
|
||||
break
|
||||
case Transactions.rewardType.enrollment:
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.signUpBonus",
|
||||
defaultMessage: "Sign up bonus",
|
||||
})
|
||||
break
|
||||
case Transactions.rewardType.mastercard_points:
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.scandicFriendsMastercard",
|
||||
defaultMessage: "Scandic Friends Mastercard",
|
||||
})
|
||||
break
|
||||
case Transactions.rewardType.tui_points:
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.tuiPoints",
|
||||
defaultMessage: "TUI Points",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.pointShop:
|
||||
description = intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.pointShop",
|
||||
defaultMessage: "Scandic Friends Point Shop",
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY")
|
||||
|
||||
function renderConfirmationNumber() {
|
||||
if (
|
||||
transaction.confirmationNumber === "BALFWD" ||
|
||||
transaction.confirmationNumber === "non-transactional"
|
||||
)
|
||||
return null
|
||||
|
||||
if (
|
||||
!isWebview &&
|
||||
transaction.bookingUrl &&
|
||||
(transaction.type === Transactions.rewardType.stay ||
|
||||
transaction.type === Transactions.rewardType.rewardNight)
|
||||
) {
|
||||
return (
|
||||
<TextLink href={transaction.bookingUrl}>
|
||||
{transaction.confirmationNumber}
|
||||
</TextLink>
|
||||
)
|
||||
}
|
||||
|
||||
return transaction.confirmationNumber
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.TR>
|
||||
<Table.TD>
|
||||
<AwardPoints
|
||||
awardPoints={transaction.awardPoints}
|
||||
isCalculated={transaction.pointsCalculated}
|
||||
/>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{description}</p>
|
||||
</Typography>
|
||||
</Table.TD>
|
||||
<Table.TD>{renderConfirmationNumber()}</Table.TD>
|
||||
<Table.TD>
|
||||
{transaction.checkinDate && transaction.confirmationNumber !== "BALFWD"
|
||||
? arrival
|
||||
: null}
|
||||
</Table.TD>
|
||||
</Table.TR>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
.container {
|
||||
overflow-x: auto;
|
||||
border-radius: var(--Corner-radius-sm);
|
||||
}
|
||||
|
||||
.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-lg);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Table from "@scandic-hotels/design-system/Table"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import Row from "./Row"
|
||||
|
||||
import styles from "./clientTable.module.css"
|
||||
|
||||
import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
interface ClientTableProps {
|
||||
transactions: Transactions
|
||||
}
|
||||
|
||||
export default function ClientTable({ transactions }: ClientTableProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const tableHeadings = [
|
||||
intl.formatMessage({
|
||||
id: "common.points",
|
||||
defaultMessage: "Points",
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.description",
|
||||
defaultMessage: "Description",
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.bookingNumberReference",
|
||||
defaultMessage: "Booking number / Reference",
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.date",
|
||||
defaultMessage: "Date",
|
||||
}),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Table>
|
||||
<Table.THead>
|
||||
<Table.TR>
|
||||
{tableHeadings.map((heading) => (
|
||||
<Table.TH key={heading}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>{heading}</p>
|
||||
</Typography>
|
||||
</Table.TH>
|
||||
))}
|
||||
</Table.TR>
|
||||
</Table.THead>
|
||||
<Table.TBody>
|
||||
{transactions.length ? (
|
||||
transactions.map((transaction, index) => (
|
||||
<Row
|
||||
key={`${transaction.confirmationNumber}-${index}`}
|
||||
transaction={transaction}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Table.TR className={styles.placeholder}>
|
||||
<Table.TD colSpan={tableHeadings.length}>
|
||||
{intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.noTransactions",
|
||||
defaultMessage: "No transactions available",
|
||||
})}
|
||||
</Table.TD>
|
||||
</Table.TR>
|
||||
)}
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import ClientJourney from "./Client"
|
||||
|
||||
export default async function JourneyTable() {
|
||||
return <ClientJourney />
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import AwardPoints from "../../EarnAndBurn/AwardPoints"
|
||||
import AwardPoints from "../AwardPoints"
|
||||
|
||||
export default function ExpiringPointsTable({
|
||||
points,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Link } from "react-aria-components"
|
||||
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
|
||||
export function ConditionalPointTransactionLink({
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
canLinkBookingUrl,
|
||||
focusRef,
|
||||
}: {
|
||||
href?: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
canLinkBookingUrl: boolean
|
||||
focusRef: React.Ref<HTMLAnchorElement>
|
||||
}) {
|
||||
if (canLinkBookingUrl) {
|
||||
return (
|
||||
<Link className={className} href={href} ref={focusRef}>
|
||||
{children}
|
||||
<MaterialIcon icon="arrow_forward" color="Icon/Interactive/Default" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
return <div className={className}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
"use client"
|
||||
import React from "react"
|
||||
import { type IntlShape, useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
BALFWD,
|
||||
NON_TRANSACTIONAL,
|
||||
} from "@scandic-hotels/common/constants/transactionType"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { Transactions } from "@scandic-hotels/trpc/enums/transactions"
|
||||
|
||||
import { ConditionalPointTransactionLink } from "../ConditionalPointTransactionLink"
|
||||
|
||||
import styles from "../pointTransactionList.module.css"
|
||||
|
||||
import type { Transaction } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export function PointTransactionRow({
|
||||
transaction,
|
||||
lang,
|
||||
focusRef,
|
||||
}: {
|
||||
transaction: Transaction
|
||||
lang: string
|
||||
focusRef: React.Ref<HTMLAnchorElement>
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
const { confirmationNumber, bookingUrl, checkinDate, awardPoints } =
|
||||
transaction.attributes
|
||||
const balfwd = confirmationNumber === BALFWD
|
||||
const nonTransactional = confirmationNumber === NON_TRANSACTIONAL
|
||||
|
||||
const day = checkinDate.split("-")[2].replace(/^0/, "")
|
||||
const month = dt(checkinDate.split("-")[1]).locale(lang).format("MMM")
|
||||
|
||||
const formattedPoints = intl.formatNumber(Math.abs(awardPoints))
|
||||
const calculatedPoints =
|
||||
awardPoints === 0
|
||||
? formattedPoints
|
||||
: `${awardPoints > 0 ? "+" : "-"} ${formattedPoints}`
|
||||
|
||||
const canLinkBookingUrl = !balfwd && !nonTransactional
|
||||
|
||||
const description = getDescription(transaction, intl)
|
||||
|
||||
return (
|
||||
<ConditionalPointTransactionLink
|
||||
className={styles.row}
|
||||
href={bookingUrl}
|
||||
canLinkBookingUrl={canLinkBookingUrl}
|
||||
focusRef={focusRef}
|
||||
>
|
||||
<div className={styles.date}>
|
||||
<Typography variant="Title/Subtitle/lg">
|
||||
<time>{day}</time>
|
||||
</Typography>
|
||||
<Typography variant="Tag/sm">
|
||||
<time>{month}</time>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className={styles.textArea}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "common.pointsInLine",
|
||||
defaultMessage: "{points} points",
|
||||
},
|
||||
{
|
||||
points: (
|
||||
<Typography variant="Body/Paragraph/mdBold" key="points">
|
||||
<strong>{calculatedPoints}</strong>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
className={styles.description}
|
||||
variant="Body/Supporting text (caption)/smRegular"
|
||||
>
|
||||
<p>{description}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</ConditionalPointTransactionLink>
|
||||
)
|
||||
}
|
||||
|
||||
function getDescription(transaction: Transaction, intl: IntlShape) {
|
||||
const hotelInformation = transaction.attributes.hotelInformation
|
||||
const balfwd = transaction.attributes.confirmationNumber === BALFWD
|
||||
const nonTransactional =
|
||||
transaction.attributes.confirmationNumber === NON_TRANSACTIONAL
|
||||
switch (transaction.type) {
|
||||
case Transactions.rewardType.stay:
|
||||
return nonTransactional && transaction.attributes.nights === 0
|
||||
? intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointsActivity",
|
||||
defaultMessage: "Point activity",
|
||||
})
|
||||
: hotelInformation?.name
|
||||
? intl.formatMessage(
|
||||
{
|
||||
id: "myPoints.pointTransactions.stayAt",
|
||||
defaultMessage: "Stay at {hotelName}",
|
||||
},
|
||||
{ hotelName: hotelInformation?.name }
|
||||
)
|
||||
: ""
|
||||
|
||||
case Transactions.rewardType.stayAdj:
|
||||
if (transaction.attributes.hotelOperaId === "ORS") {
|
||||
return intl.formatMessage({
|
||||
id: "earnAndBurn.journeyTable.formerScandicHotel",
|
||||
defaultMessage: "Former Scandic Hotel",
|
||||
})
|
||||
}
|
||||
if (balfwd) {
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointsEarnedPriorMay2021",
|
||||
defaultMessage: "Points earned prior to May 1, 2021",
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case Transactions.rewardType.ancillary:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.extrasToBooking",
|
||||
defaultMessage: "Extras to your booking",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.enrollment:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.signUpBonus",
|
||||
defaultMessage: "Sign up bonus",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.mastercard_points:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.scandicFriendsMastercard",
|
||||
defaultMessage: "Scandic Friends Mastercard",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.tui_points:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.tuiPoints",
|
||||
defaultMessage: "TUI Points",
|
||||
})
|
||||
|
||||
case Transactions.rewardType.pointShop:
|
||||
return intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.pointShop",
|
||||
defaultMessage: "Scandic Friends Point Shop",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client"
|
||||
import { Fragment, useCallback, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
||||
import { StickyElementNameEnum } from "@scandic-hotels/common/stores/sticky-position"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
|
||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import { PointTransactionRow } from "./PointTransactionRow"
|
||||
|
||||
import styles from "./pointTransactionList.module.css"
|
||||
|
||||
import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export function PointTransactionList() {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||
trpc.user.transaction.friendTransactions.useInfiniteQuery(
|
||||
{
|
||||
limit: 10,
|
||||
lang,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => {
|
||||
return lastPage?.nextCursor
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const transactions = data?.pages
|
||||
.filter((page): page is Transactions => !!page?.data)
|
||||
.flatMap((page) => page.data)
|
||||
|
||||
const groupedTransactions =
|
||||
transactions?.reduce<Record<number, typeof transactions>>(
|
||||
(acc, transaction) => {
|
||||
const year = new Date(transaction.attributes.checkinDate).getFullYear()
|
||||
if (!acc[year]) acc[year] = []
|
||||
acc[year].push(transaction)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
) ?? {}
|
||||
|
||||
const sortedYears = Object.keys(groupedTransactions)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a)
|
||||
|
||||
const lastPage = data?.pages?.[data.pages.length - 1]
|
||||
const firstNewTransaction = lastPage?.data?.[0]
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null)
|
||||
useStickyPosition({
|
||||
ref: headerRef,
|
||||
name: StickyElementNameEnum.TRANSACTION_LIST_HEADER,
|
||||
})
|
||||
|
||||
const isEmpty = !isLoading && (transactions?.length ?? 0) === 0
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
shouldFocusNextItem.current = true
|
||||
fetchNextPage()
|
||||
}
|
||||
}
|
||||
const shouldFocusNextItem = useRef(false)
|
||||
|
||||
const focusRef = useCallback((node: HTMLAnchorElement | null) => {
|
||||
if (!node || !shouldFocusNextItem.current) return
|
||||
|
||||
node.focus()
|
||||
shouldFocusNextItem.current = false
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.list}>
|
||||
{isLoading && <LoadingSpinner />}
|
||||
{isEmpty && (
|
||||
<div className={styles.emptyState}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.noTransactions",
|
||||
defaultMessage: "No transactions available",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
!isEmpty &&
|
||||
sortedYears.map((year) => (
|
||||
<Fragment key={year}>
|
||||
<div ref={headerRef} className={styles.yearHeader}>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h2>{year}</h2>
|
||||
</Typography>
|
||||
</div>
|
||||
<ul className={styles.listContent}>
|
||||
{groupedTransactions[year].map((transaction, i) => {
|
||||
const isFirstNewItem = transaction === firstNewTransaction
|
||||
return (
|
||||
<li key={`${year}-${i}`}>
|
||||
<PointTransactionRow
|
||||
transaction={transaction}
|
||||
lang={lang}
|
||||
focusRef={isFirstNewItem ? focusRef : null}
|
||||
/>
|
||||
{i < groupedTransactions[year].length - 1 && (
|
||||
<div className={styles.divider}>
|
||||
<Divider />
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.showMoreButton}>
|
||||
{isFetchingNextPage ? (
|
||||
<LoadingSpinner />
|
||||
) : hasNextPage ? (
|
||||
<ShowMoreButton
|
||||
loadMoreData={loadMoreData}
|
||||
textShowMore={intl.formatMessage({
|
||||
id: "myPoints.pointTransactions.showMoreTransactions",
|
||||
defaultMessage: "Show more transactions",
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.list {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
border: 1px solid var(--Border-Default);
|
||||
background: var(--Surface-Primary-Default);
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--Space-x4);
|
||||
color: var(--Text-Tertiary);
|
||||
}
|
||||
|
||||
.yearHeader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 var(--Space-x2);
|
||||
background: var(--Surface-Brand-Primary-1-Default);
|
||||
color: var(--Text-Brand-OnPrimary-1-Heading);
|
||||
height: 37px;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.yearHeader:first-child {
|
||||
border-radius: calc(var(--Corner-radius-md) - 1px)
|
||||
calc(var(--Corner-radius-md) - 1px) 0 0;
|
||||
}
|
||||
|
||||
.listContent {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.divider {
|
||||
padding: 0 var(--Space-x2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--Space-x2);
|
||||
padding: var(--Space-x2);
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
padding: var(--Space-x05);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--Corner-radius-sm);
|
||||
color: var(--Text-Tertiary);
|
||||
background: var(--Surface-Secondary-Default, #f2ece6);
|
||||
}
|
||||
|
||||
.date > * + * {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: var(--Space-x05);
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.points {
|
||||
display: flex;
|
||||
height: var(--Space-x3);
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
height: var(--Space-x3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.showMoreButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Section } from "@/components/Section"
|
||||
import { SectionHeader } from "@/components/Section/Header"
|
||||
import SectionLink from "@/components/Section/Link"
|
||||
|
||||
import ClaimPoints from "../ClaimPoints"
|
||||
import JourneyTable from "./JourneyTable"
|
||||
import { PointTransactionList } from "./PointTransactionList"
|
||||
|
||||
import styles from "./earnAndBurn.module.css"
|
||||
import styles from "./pointTransactions.module.css"
|
||||
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default function EarnAndBurn({
|
||||
export function PointTransactions({
|
||||
link,
|
||||
subtitle,
|
||||
title,
|
||||
@@ -17,12 +16,10 @@ export default function EarnAndBurn({
|
||||
return (
|
||||
<Section>
|
||||
<div className={styles.header}>
|
||||
<SectionHeader heading={title} link={link} preamble={subtitle} />
|
||||
|
||||
<SectionHeader link={link} preamble={subtitle} heading={title} />
|
||||
<ClaimPoints />
|
||||
</div>
|
||||
<JourneyTable />
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
<PointTransactionList />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { ManageCookieConsent } from "@/components/Blocks/DynamicContent/ManageCo
|
||||
import MyPagesOverviewShortcuts from "@/components/Blocks/DynamicContent/MyPagesOverviewShortcuts"
|
||||
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
||||
import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable"
|
||||
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
||||
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
|
||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
||||
@@ -25,6 +24,7 @@ import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming"
|
||||
import { ProfilingConsentBanner } from "@/components/MyPages/ProfilingConsent/Banner"
|
||||
import { SJWidget } from "@/components/SJWidget"
|
||||
|
||||
import { PointTransactions } from "./Points/PointTransactions"
|
||||
import JobylonFeed from "./JobylonFeed"
|
||||
import { RewardNights } from "./RewardNights"
|
||||
|
||||
@@ -44,7 +44,7 @@ function DynamicContentBlocks(props: DynamicContentProps) {
|
||||
case DynamicContentEnum.Blocks.components.current_benefits:
|
||||
return <CurrentRewardsBlock {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
||||
return <EarnAndBurn {...dynamic_content} />
|
||||
return <PointTransactions {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.expiring_points:
|
||||
return <ExpiringPoints {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.how_it_works:
|
||||
|
||||
@@ -3,8 +3,8 @@ import { BlocksEnums } from "@scandic-hotels/trpc/types/blocksEnum"
|
||||
import { DynamicContentEnum } from "@scandic-hotels/trpc/types/dynamicContent"
|
||||
|
||||
import Overview from "@/components/Blocks/DynamicContent/Overview"
|
||||
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||
import { PointTransactions } from "@/components/Blocks/DynamicContent/Points/PointTransactions"
|
||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
||||
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
||||
import ShortcutsList from "@/components/Blocks/ShortcutsList"
|
||||
@@ -41,7 +41,7 @@ async function DynamicComponent({ dynamic_content }: AccountPageContentProps) {
|
||||
// return <ExpiringPoints />
|
||||
return null
|
||||
case DynamicContentEnum.Blocks.components.earn_and_burn:
|
||||
return <EarnAndBurn {...dynamicContent} />
|
||||
return <PointTransactions {...dynamicContent} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
import type { awardPointsVariants } from "@/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/awardPointsVariants"
|
||||
import type { awardPointsVariants } from "@/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/awardPointsVariants"
|
||||
import type { UserQueryRouter } from "../user"
|
||||
|
||||
type TransactionResponse = Awaited<
|
||||
ReturnType<UserQueryRouter["transaction"]["friendTransactions"]>
|
||||
>
|
||||
type TransactionsNonNullResponseObject = NonNullable<TransactionResponse>
|
||||
export type Transactions =
|
||||
NonNullable<TransactionsNonNullResponseObject>["data"]["transactions"]
|
||||
export type Transaction = Transactions[number]
|
||||
export type Transactions = TransactionsNonNullResponseObject
|
||||
export type Transaction = Transactions["data"][number]
|
||||
|
||||
export type AwardPointsVariantProps = VariantProps<typeof awardPointsVariants>
|
||||
|
||||
Reference in New Issue
Block a user