wip: initial stab trpc pagination
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { ChevronDownIcon } from "@/components/Icons"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
import Row from "./Row"
|
import Row from "./Row"
|
||||||
|
|
||||||
import styles from "./desktop.module.css"
|
import styles from "./desktop.module.css"
|
||||||
|
|
||||||
import type { TablePropsPagination } from "@/types/components/myPages/myPage/earnAndBurn"
|
import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||||
|
|
||||||
const tableHeadings = [
|
const tableHeadings = [
|
||||||
"Arrival date",
|
"Arrival date",
|
||||||
@@ -19,11 +16,7 @@ const tableHeadings = [
|
|||||||
"Points",
|
"Points",
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function DesktopTable({
|
export default function DesktopTable({ transactions }: TableProps) {
|
||||||
transactions,
|
|
||||||
showMore,
|
|
||||||
hasMore,
|
|
||||||
}: TablePropsPagination) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,19 +44,6 @@ export default function DesktopTable({
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{hasMore ? (
|
|
||||||
<div className={styles.footer}>
|
|
||||||
<button
|
|
||||||
className={styles.loadMoreButton}
|
|
||||||
onClick={() => {
|
|
||||||
showMore()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon color="burgundy" height={24} width={24} />
|
|
||||||
{intl.formatMessage({ id: "Show more" })}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
@@ -1,23 +1,16 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
import { ChevronDownIcon } from "@/components/Icons"
|
import AwardPoints from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/AwardPoints"
|
||||||
import AwardPoints from "@/components/MyPages/Blocks/Points/EarnAndBurn/TransactionTable/Desktop/Row/AwardPoints"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./mobile.module.css"
|
import styles from "./mobile.module.css"
|
||||||
|
|
||||||
import type { TablePropsPagination } from "@/types/components/myPages/myPage/earnAndBurn"
|
import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||||
|
|
||||||
export default function MobileTable({
|
export default function MobileTable({ transactions }: TableProps) {
|
||||||
transactions,
|
|
||||||
showMore,
|
|
||||||
hasMore,
|
|
||||||
}: TablePropsPagination) {
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,18 +64,6 @@ export default function MobileTable({
|
|||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{hasMore ? (
|
|
||||||
<button
|
|
||||||
className={styles.loadMoreButton}
|
|
||||||
onClick={() => {
|
|
||||||
showMore()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon height={24} width={24} />
|
|
||||||
{intl.formatMessage({ id: "Show more" })}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import { ChevronRightIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import DesktopTable from "./Desktop"
|
||||||
|
import MobileTable from "./Mobile"
|
||||||
|
|
||||||
|
import styles from "../earnAndBurn.module.css"
|
||||||
|
|
||||||
|
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||||
|
|
||||||
|
function PaginationButton({
|
||||||
|
children,
|
||||||
|
isActive,
|
||||||
|
handleClick,
|
||||||
|
disabled,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
disabled: boolean
|
||||||
|
isActive?: boolean
|
||||||
|
handleClick: () => void
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={`${styles.paginationButton} ${isActive ? styles.paginationButtonActive : ""}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Pagination({
|
||||||
|
pageCount,
|
||||||
|
isFetching,
|
||||||
|
handlePageChange,
|
||||||
|
currentPage,
|
||||||
|
}: {
|
||||||
|
pageCount: number
|
||||||
|
isFetching: boolean
|
||||||
|
handlePageChange: (page: number) => void
|
||||||
|
currentPage: number
|
||||||
|
}) {
|
||||||
|
const isOnFirstPage = currentPage === 1
|
||||||
|
const isOnLastPage = currentPage === pageCount
|
||||||
|
return (
|
||||||
|
<div className={styles.pagination}>
|
||||||
|
<PaginationButton
|
||||||
|
disabled={isFetching || isOnFirstPage}
|
||||||
|
handleClick={() => {
|
||||||
|
handlePageChange(currentPage - 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon className={styles.chevronLeft} />
|
||||||
|
</PaginationButton>
|
||||||
|
{[...Array(pageCount)].map((_, idx) => (
|
||||||
|
<PaginationButton
|
||||||
|
isActive={currentPage === idx + 1}
|
||||||
|
disabled={isFetching || currentPage === idx + 1}
|
||||||
|
key={idx}
|
||||||
|
handleClick={() => {
|
||||||
|
handlePageChange(idx + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{idx + 1}
|
||||||
|
</PaginationButton>
|
||||||
|
))}
|
||||||
|
<PaginationButton
|
||||||
|
disabled={isFetching || isOnLastPage}
|
||||||
|
handleClick={() => {
|
||||||
|
handlePageChange(currentPage + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationButton>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TransactionTable() {
|
||||||
|
const limit = 5
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [totalPages, setTotalPages] = useState(0)
|
||||||
|
const [currentTransactions, setCurrentTransactions] = useState<Transactions>(
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const { data, isFetching, isLoading } =
|
||||||
|
trpc.user.transaction.friendTransactions.useQuery({
|
||||||
|
limit,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
// Should the active page be mirroried in the URL with params?
|
||||||
|
// That way the actual fetch could be moved up and Mobile/Desktop can be strictly server side
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof data?.data.pages === "number") {
|
||||||
|
setTotalPages(data?.data.pages)
|
||||||
|
}
|
||||||
|
}, [data?.data.pages])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.data.transactions) {
|
||||||
|
setCurrentTransactions(data.data.transactions)
|
||||||
|
}
|
||||||
|
}, [data?.data.transactions])
|
||||||
|
|
||||||
|
return !currentTransactions.length ? (
|
||||||
|
"Loading..." // Add loading state table
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MobileTable transactions={currentTransactions} />
|
||||||
|
<DesktopTable transactions={currentTransactions} />
|
||||||
|
{totalPages > 1 ? (
|
||||||
|
<Pagination
|
||||||
|
handlePageChange={setPage}
|
||||||
|
pageCount={totalPages}
|
||||||
|
isFetching={isFetching}
|
||||||
|
currentPage={page}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { useState } from "react"
|
|
||||||
|
|
||||||
import DesktopTable from "./Desktop"
|
|
||||||
import MobileTable from "./Mobile"
|
|
||||||
|
|
||||||
import { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
|
||||||
|
|
||||||
export function TransactionTable({ transactions }: TableProps) {
|
|
||||||
const [transactionDisplayCount, setTransactionDisplayCount] = useState(5)
|
|
||||||
|
|
||||||
const showMoreTransactions = () => {
|
|
||||||
setTransactionDisplayCount((count) => count + 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayedTransactions = transactions.slice(0, transactionDisplayCount)
|
|
||||||
const hasMoreTransactions = transactions.length > transactionDisplayCount
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MobileTable
|
|
||||||
transactions={displayedTransactions}
|
|
||||||
showMore={showMoreTransactions}
|
|
||||||
hasMore={hasMoreTransactions}
|
|
||||||
/>
|
|
||||||
<DesktopTable
|
|
||||||
transactions={displayedTransactions}
|
|
||||||
showMore={showMoreTransactions}
|
|
||||||
hasMore={hasMoreTransactions}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,3 +2,37 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x3);
|
gap: var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--Spacing-x2);
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
margin: auto;
|
||||||
|
gap: var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationButton {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
font-size: var(--typography-Body-Bold-fontSize);
|
||||||
|
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevronLeft {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationButtonActive {
|
||||||
|
color: var(--WHITE);
|
||||||
|
background-color: var(--Base-Text-Accent);
|
||||||
|
border-radius: var(--Corner-radius-Rounded);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import SectionContainer from "@/components/Section/Container"
|
|||||||
import SectionHeader from "@/components/Section/Header"
|
import SectionHeader from "@/components/Section/Header"
|
||||||
import SectionLink from "@/components/Section/Link"
|
import SectionLink from "@/components/Section/Link"
|
||||||
|
|
||||||
import { TransactionTable } from "./TransactionTable"
|
import JourneyTable from "./JourneyTable"
|
||||||
|
|
||||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
|
||||||
@@ -13,17 +13,18 @@ export default async function EarnAndBurn({
|
|||||||
subtitle,
|
subtitle,
|
||||||
title,
|
title,
|
||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const transactions =
|
const transactionsData =
|
||||||
await serverClient().user.transaction.friendTransactions()
|
await serverClient().user.transaction.friendTransactions({
|
||||||
if (!transactions) {
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
if (!transactionsData) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<SectionContainer>
|
<SectionContainer>
|
||||||
<SectionHeader title={title} link={link} subtitle={subtitle} />
|
<SectionHeader title={title} link={link} subtitle={subtitle} />
|
||||||
|
<JourneyTable />
|
||||||
<TransactionTable transactions={transactions.data} />
|
|
||||||
|
|
||||||
<SectionLink link={link} variant="mobile" />
|
<SectionLink link={link} variant="mobile" />
|
||||||
</SectionContainer>
|
</SectionContainer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,3 +29,9 @@ export const saveCardInput = z.object({
|
|||||||
transactionId: z.string(),
|
transactionId: z.string(),
|
||||||
merchantId: z.string().optional(),
|
merchantId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
export const friendTransactionsInput = z
|
||||||
|
.object({
|
||||||
|
limit: z.number().int().positive(),
|
||||||
|
page: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
.default({ limit: 5, page: 1 })
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { getMembership, getMembershipCards } from "@/utils/user"
|
|||||||
|
|
||||||
import encryptValue from "../utils/encryptValue"
|
import encryptValue from "../utils/encryptValue"
|
||||||
import {
|
import {
|
||||||
|
friendTransactionsInput,
|
||||||
getUserInputSchema,
|
getUserInputSchema,
|
||||||
initiateSaveCardInput,
|
initiateSaveCardInput,
|
||||||
saveCardInput,
|
saveCardInput,
|
||||||
@@ -453,54 +454,69 @@ export const userQueryRouter = router({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
transaction: router({
|
transaction: router({
|
||||||
friendTransactions: protectedProcedure.query(async (opts) => {
|
friendTransactions: protectedProcedure
|
||||||
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, {
|
.input(friendTransactionsInput)
|
||||||
cache: "no-store",
|
.query(async ({ ctx, input }) => {
|
||||||
headers: {
|
const { limit, page } = input
|
||||||
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
|
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, {
|
||||||
},
|
headers: {
|
||||||
})
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||||
|
},
|
||||||
|
next: { revalidate: 30 * 60 * 1000 },
|
||||||
|
})
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
// switch (apiResponse.status) {
|
// switch (apiResponse.status) {
|
||||||
// case 400:
|
// case 400:
|
||||||
// throw badRequestError()
|
// throw badRequestError()
|
||||||
// case 401:
|
// case 401:
|
||||||
// throw unauthorizedError()
|
// throw unauthorizedError()
|
||||||
// case 403:
|
// case 403:
|
||||||
// throw forbiddenError()
|
// throw forbiddenError()
|
||||||
// default:
|
// default:
|
||||||
// throw internalServerError()
|
// throw internalServerError()
|
||||||
// }
|
// }
|
||||||
console.error(`API Response Failed - Getting Friend Transactions`)
|
console.error(`API Response Failed - Getting Friend Transactions`)
|
||||||
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||||
console.error(apiResponse)
|
console.error(apiResponse)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||||
if (!verifiedData.success) {
|
if (!verifiedData.success) {
|
||||||
console.error(`Failed to validate Friend Transactions Data`)
|
console.error(`Failed to validate Friend Transactions Data`)
|
||||||
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||||
console.error(verifiedData.error)
|
console.error(verifiedData.error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const pageData = verifiedData.data.data.slice(
|
||||||
data: verifiedData.data.data.map(({ attributes }) => {
|
limit * (page - 1),
|
||||||
return {
|
limit * page
|
||||||
awardPoints: attributes.awardPoints,
|
)
|
||||||
checkinDate: attributes.checkinDate,
|
|
||||||
checkoutDate: attributes.checkoutDate,
|
return {
|
||||||
city: attributes.hotelInformation?.city,
|
data: {
|
||||||
confirmationNumber: attributes.confirmationNumber,
|
transactions: pageData.map(({ attributes }) => {
|
||||||
hotelName: attributes.hotelInformation?.name,
|
return {
|
||||||
nights: attributes.nights,
|
awardPoints: attributes.awardPoints,
|
||||||
}
|
checkinDate: attributes.checkinDate,
|
||||||
}),
|
checkoutDate: attributes.checkoutDate,
|
||||||
}
|
city: attributes.hotelInformation?.city,
|
||||||
}),
|
confirmationNumber: attributes.confirmationNumber,
|
||||||
|
hotelName: attributes.hotelInformation?.name,
|
||||||
|
nights: attributes.nights,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
pages: Math.ceil(verifiedData.data.data.length / limit),
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
totalPages: Math.ceil(verifiedData.data.data.length / limit),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { awardPointsVariants } from "@/components/MyPages/Blocks/Points/EarnAndBurn/TransactionTable/Desktop/Row/awardPointsVariants"
|
import { awardPointsVariants } from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/awardPointsVariants"
|
||||||
|
|
||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
@@ -10,9 +10,9 @@ export type TransactionResponse = Awaited<
|
|||||||
>
|
>
|
||||||
export type TransactionsNonNullResponseObject = NonNullable<TransactionResponse>
|
export type TransactionsNonNullResponseObject = NonNullable<TransactionResponse>
|
||||||
export type Transactions =
|
export type Transactions =
|
||||||
NonNullable<TransactionsNonNullResponseObject>["data"]
|
NonNullable<TransactionsNonNullResponseObject>["data"]["transactions"]
|
||||||
export type Transaction =
|
export type Transaction =
|
||||||
NonNullable<TransactionsNonNullResponseObject>["data"][number]
|
NonNullable<TransactionsNonNullResponseObject>["data"]["transactions"][number]
|
||||||
|
|
||||||
export type ClientEarnAndBurnProps = {
|
export type ClientEarnAndBurnProps = {
|
||||||
initialData: TransactionsNonNullResponseObject
|
initialData: TransactionsNonNullResponseObject
|
||||||
@@ -27,11 +27,6 @@ export interface TableProps {
|
|||||||
transactions: Transactions
|
transactions: Transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TablePropsPagination extends TableProps {
|
|
||||||
showMore: () => void
|
|
||||||
hasMore: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RowProps {
|
export interface RowProps {
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user