diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx
deleted file mode 100644
index 3b572b68d..000000000
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/Client.tsx
+++ /dev/null
@@ -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 ? (
-
- ) : (
- <>
-
- {data && data.meta.totalPages > 1 ? (
-
- ) : null}
- >
- )
-}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx
deleted file mode 100644
index f35fe468a..000000000
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/Row/index.tsx
+++ /dev/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 (
-
- {transaction.confirmationNumber}
-
- )
- }
-
- return transaction.confirmationNumber
- }
-
- return (
-
-
-
-
-
-
- {description}
-
-
- {renderConfirmationNumber()}
-
- {transaction.checkinDate && transaction.confirmationNumber !== "BALFWD"
- ? arrival
- : null}
-
-
- )
-}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css
deleted file mode 100644
index afc051b4c..000000000
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/clientTable.module.css
+++ /dev/null
@@ -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);
- }
-}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx
deleted file mode 100644
index 0a77a1d9d..000000000
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/ClientTable/index.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
- {tableHeadings.map((heading) => (
-
-
- {heading}
-
-
- ))}
-
-
-
- {transactions.length ? (
- transactions.map((transaction, index) => (
-
- ))
- ) : (
-
-
- {intl.formatMessage({
- id: "earnAndBurn.journeyTable.noTransactions",
- defaultMessage: "No transactions available",
- })}
-
-
- )}
-
-
-
- )
-}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/index.tsx
deleted file mode 100644
index 2bf170316..000000000
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/JourneyTable/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import ClientJourney from "./Client"
-
-export default async function JourneyTable() {
- return
-}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/awardPoints.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/awardPoints.module.css
similarity index 100%
rename from apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/awardPoints.module.css
rename to apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/awardPoints.module.css
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/awardPointsVariants.ts b/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/awardPointsVariants.ts
similarity index 100%
rename from apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/awardPointsVariants.ts
rename to apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/awardPointsVariants.ts
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/index.tsx
similarity index 100%
rename from apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/AwardPoints/index.tsx
rename to apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/AwardPoints/index.tsx
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/ExpiringPointsTable/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/ExpiringPointsTable/index.tsx
index 0ae9351ce..6e942425a 100644
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/ExpiringPointsTable/index.tsx
+++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/ExpiringPoints/ExpiringPointsTable/index.tsx
@@ -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,
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/ConditionalPointTransactionLink.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/ConditionalPointTransactionLink.tsx
new file mode 100644
index 000000000..aff5872e2
--- /dev/null
+++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/ConditionalPointTransactionLink.tsx
@@ -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
+}) {
+ if (canLinkBookingUrl) {
+ return (
+
+ {children}
+
+
+ )
+ }
+ return {children}
+}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/PointTransactionRow/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/PointTransactionRow/index.tsx
new file mode 100644
index 000000000..466c1d56f
--- /dev/null
+++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/PointTransactionRow/index.tsx
@@ -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
+}) {
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {intl.formatMessage(
+ {
+ id: "common.pointsInLine",
+ defaultMessage: "{points} points",
+ },
+ {
+ points: (
+
+ {calculatedPoints}
+
+ ),
+ }
+ )}
+
+
+
+
+ {description}
+
+
+
+ )
+}
+
+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",
+ })
+ }
+}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/index.tsx
new file mode 100644
index 000000000..a1fd0c422
--- /dev/null
+++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/index.tsx
@@ -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>(
+ (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(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 (
+
+
+ {isLoading &&
}
+ {isEmpty && (
+
+
+
+ {intl.formatMessage({
+ id: "myPoints.pointTransactions.noTransactions",
+ defaultMessage: "No transactions available",
+ })}
+
+
+
+ )}
+ {!isLoading &&
+ !isEmpty &&
+ sortedYears.map((year) => (
+
+
+
+ {year}
+
+
+
+ {groupedTransactions[year].map((transaction, i) => {
+ const isFirstNewItem = transaction === firstNewTransaction
+ return (
+ -
+
+ {i < groupedTransactions[year].length - 1 && (
+
+ )}
+
+ )
+ })}
+
+
+ ))}
+
+
+ {isFetchingNextPage ? (
+
+ ) : hasNextPage ? (
+
+ ) : null}
+
+
+ )
+}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/pointTransactionList.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/pointTransactionList.module.css
new file mode 100644
index 000000000..2725c99e6
--- /dev/null
+++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/PointTransactionList/pointTransactionList.module.css
@@ -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;
+}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/index.tsx
similarity index 58%
rename from apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/index.tsx
rename to apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/index.tsx
index 8d49f8457..82b458e3b 100644
--- a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/index.tsx
+++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/index.tsx
@@ -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 (
)
}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/earnAndBurn.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/pointTransactions.module.css
similarity index 100%
rename from apps/scandic-web/components/Blocks/DynamicContent/Points/EarnAndBurn/earnAndBurn.module.css
rename to apps/scandic-web/components/Blocks/DynamicContent/Points/PointTransactions/pointTransactions.module.css
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx
index 97175fed5..93614bf88 100644
--- a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx
+++ b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx
@@ -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
case DynamicContentEnum.Blocks.components.earn_and_burn:
- return
+ return
case DynamicContentEnum.Blocks.components.expiring_points:
return
case DynamicContentEnum.Blocks.components.how_it_works:
diff --git a/apps/scandic-web/components/Webviews/AccountPage/Blocks.tsx b/apps/scandic-web/components/Webviews/AccountPage/Blocks.tsx
index 32be19367..8ba49be27 100644
--- a/apps/scandic-web/components/Webviews/AccountPage/Blocks.tsx
+++ b/apps/scandic-web/components/Webviews/AccountPage/Blocks.tsx
@@ -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
return null
case DynamicContentEnum.Blocks.components.earn_and_burn:
- return
+ return
default:
return null
}
diff --git a/apps/scandic-web/types/components/myPages/myPage/earnAndBurn.ts b/apps/scandic-web/types/components/myPages/myPage/earnAndBurn.ts
index 3cb6b0f67..d2c71ebc6 100644
--- a/apps/scandic-web/types/components/myPages/myPage/earnAndBurn.ts
+++ b/apps/scandic-web/types/components/myPages/myPage/earnAndBurn.ts
@@ -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
>
type TransactionsNonNullResponseObject = NonNullable
-export type Transactions =
- NonNullable["data"]["transactions"]
-export type Transaction = Transactions[number]
+export type Transactions = TransactionsNonNullResponseObject
+export type Transaction = Transactions["data"][number]
export type AwardPointsVariantProps = VariantProps
diff --git a/packages/common/constants/transactionType.ts b/packages/common/constants/transactionType.ts
new file mode 100644
index 000000000..7d759e39b
--- /dev/null
+++ b/packages/common/constants/transactionType.ts
@@ -0,0 +1,4 @@
+// 'BALFWD' are transactions from Opera migration that happended in May 2021
+export const BALFWD = "BALFWD"
+
+export const NON_TRANSACTIONAL = "non-transactional"
diff --git a/packages/common/package.json b/packages/common/package.json
index d6b77d206..0cdca7995 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -31,6 +31,7 @@
"./constants/routes/*": "./constants/routes/*.ts",
"./constants/sessionKeys": "./constants/sessionKeys.ts",
"./constants/signatureHotels": "./constants/signatureHotels.ts",
+ "./constants/transactionType": "./constants/transactionType.ts",
"./dataCache": "./dataCache/index.ts",
"./dt": "./dt/dt.ts",
"./dt/utils/hasOverlappingDates": "./dt/utils/hasOverlappingDates.ts",
diff --git a/packages/common/stores/sticky-position.ts b/packages/common/stores/sticky-position.ts
index 5d558a297..5b4e86228 100644
--- a/packages/common/stores/sticky-position.ts
+++ b/packages/common/stores/sticky-position.ts
@@ -7,6 +7,7 @@ export enum StickyElementNameEnum {
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
DESTINATION_SIDEBAR = "DESTINATION_SIDEBAR",
+ TRANSACTION_LIST_HEADER = "TRANSACTION_LIST_HEADER",
}
export interface StickyElement {
@@ -39,6 +40,7 @@ const priorityMap: Record = {
[StickyElementNameEnum.MEETING_PACKAGE_WIDGET]: 3,
[StickyElementNameEnum.DESTINATION_SIDEBAR]: 3,
+ [StickyElementNameEnum.TRANSACTION_LIST_HEADER]: 3,
}
const useStickyPositionStore = create((set, get) => ({
diff --git a/packages/trpc/lib/routers/user/input.ts b/packages/trpc/lib/routers/user/input.ts
index 0464f9b2e..c1121f939 100644
--- a/packages/trpc/lib/routers/user/input.ts
+++ b/packages/trpc/lib/routers/user/input.ts
@@ -20,11 +20,11 @@ export const staysInput = z
export const friendTransactionsInput = z
.object({
- limit: z.number().int().positive(),
- page: z.number().int().positive(),
+ cursor: z.number().optional(),
+ limit: z.number().min(0).default(10),
lang: z.nativeEnum(Lang).optional(),
})
- .default({ limit: 5, page: 1 })
+ .default({})
// Mutation
export const addCreditCardInput = z.object({
diff --git a/packages/trpc/lib/routers/user/query/index.ts b/packages/trpc/lib/routers/user/query/index.ts
index 0e397f6ff..43cd7dfce 100644
--- a/packages/trpc/lib/routers/user/query/index.ts
+++ b/packages/trpc/lib/routers/user/query/index.ts
@@ -1,3 +1,4 @@
+import { BALFWD } from "@scandic-hotels/common/constants/transactionType"
import { createCounter } from "@scandic-hotels/common/telemetry"
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
@@ -239,22 +240,23 @@ export const userQueryRouter = router({
friendTransactions: languageProtectedProcedure
.input(friendTransactionsInput)
.query(async ({ ctx, input }) => {
- const { limit, page, lang } = input
+ const { limit, cursor, lang } = input
+ const language = lang ?? ctx.lang
+
+ const page = cursor ? Number(cursor) : 1
const friendTransactionsCounter = createCounter(
- "trpc.user.transactions.friendTransactions"
+ "trpc.user.transactions.friendTransactions2"
)
const metricsFriendTransactions = friendTransactionsCounter.init({
limit,
- page,
- lang,
+ cursor,
+ language,
})
metricsFriendTransactions.start()
- const language = lang ?? ctx.lang
-
const apiResponse = await api.get(
api.endpoints.v1.Profile.Transaction.friendTransactions,
{
@@ -282,15 +284,13 @@ export const userQueryRouter = router({
const updatedData = await updateStaysBookingUrl(
verifiedData.data.data,
ctx.session,
- ctx.lang
+ language
)
-
- const pageData = updatedData
+ const allTransactions = updatedData
.filter((t) => t.type !== Transactions.rewardType.expired)
.sort((a, b) => {
- // 'BALFWD' are transactions from Opera migration that happended in May 2021
- if (a.attributes.confirmationNumber === "BALFWD") return 1
- if (b.attributes.confirmationNumber === "BALFWD") return -1
+ if (a.attributes.confirmationNumber === BALFWD) return 1
+ if (b.attributes.confirmationNumber === BALFWD) return -1
const dateA = new Date(
a.attributes.checkinDate
@@ -306,35 +306,20 @@ export const userQueryRouter = router({
return dateA > dateB ? -1 : 1
})
- const slicedData = pageData.slice(limit * (page - 1), limit * page)
-
- const result = {
- data: {
- transactions: slicedData.map(({ type, attributes }) => {
- return {
- type,
- awardPoints: attributes.awardPoints,
- checkinDate: attributes.checkinDate,
- checkoutDate: attributes.checkoutDate,
- confirmationNumber: attributes.confirmationNumber,
- nights: attributes.nights,
- pointsCalculated: attributes.pointsCalculated,
- hotelId: attributes.hotelOperaId,
- transactionDate: attributes.transactionDate,
- bookingUrl: attributes.bookingUrl,
- hotelName: attributes.hotelInformation?.name,
- city: attributes.hotelInformation?.city,
- }
- }),
- },
- meta: {
- totalPages: Math.ceil(pageData.length / limit),
- },
- }
+ const startIndex = limit * (page - 1)
+ const endIndex = limit * page
+ const paginatedTransactions = allTransactions.slice(
+ startIndex,
+ endIndex
+ )
+ const hasMore = endIndex < allTransactions.length
metricsFriendTransactions.success()
- return result
+ return {
+ data: paginatedTransactions,
+ nextCursor: hasMore ? Number(page + 1) : undefined,
+ }
}),
}),