wip: initial stab trpc pagination
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import Row from "./Row"
|
||||
|
||||
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 = [
|
||||
"Arrival date",
|
||||
@@ -19,11 +16,7 @@ const tableHeadings = [
|
||||
"Points",
|
||||
]
|
||||
|
||||
export default function DesktopTable({
|
||||
transactions,
|
||||
showMore,
|
||||
hasMore,
|
||||
}: TablePropsPagination) {
|
||||
export default function DesktopTable({ transactions }: TableProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
@@ -51,19 +44,6 @@ export default function DesktopTable({
|
||||
))}
|
||||
</tbody>
|
||||
</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>
|
||||
) : (
|
||||
<table className={styles.table}>
|
||||
@@ -1,23 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { ChevronDownIcon } from "@/components/Icons"
|
||||
import AwardPoints from "@/components/MyPages/Blocks/Points/EarnAndBurn/TransactionTable/Desktop/Row/AwardPoints"
|
||||
import AwardPoints from "@/components/MyPages/Blocks/Points/EarnAndBurn/JourneyTable/Desktop/Row/AwardPoints"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
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({
|
||||
transactions,
|
||||
showMore,
|
||||
hasMore,
|
||||
}: TablePropsPagination) {
|
||||
export default function MobileTable({ transactions }: TableProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
@@ -71,18 +64,6 @@ export default function MobileTable({
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{hasMore ? (
|
||||
<button
|
||||
className={styles.loadMoreButton}
|
||||
onClick={() => {
|
||||
showMore()
|
||||
}}
|
||||
>
|
||||
<ChevronDownIcon height={24} width={24} />
|
||||
{intl.formatMessage({ id: "Show more" })}
|
||||
</button>
|
||||
) : null}
|
||||
</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;
|
||||
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 SectionLink from "@/components/Section/Link"
|
||||
|
||||
import { TransactionTable } from "./TransactionTable"
|
||||
import JourneyTable from "./JourneyTable"
|
||||
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
@@ -13,17 +13,18 @@ export default async function EarnAndBurn({
|
||||
subtitle,
|
||||
title,
|
||||
}: AccountPageComponentProps) {
|
||||
const transactions =
|
||||
await serverClient().user.transaction.friendTransactions()
|
||||
if (!transactions) {
|
||||
const transactionsData =
|
||||
await serverClient().user.transaction.friendTransactions({
|
||||
limit: 10,
|
||||
page: 1,
|
||||
})
|
||||
if (!transactionsData) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SectionContainer>
|
||||
<SectionHeader title={title} link={link} subtitle={subtitle} />
|
||||
|
||||
<TransactionTable transactions={transactions.data} />
|
||||
|
||||
<JourneyTable />
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
</SectionContainer>
|
||||
)
|
||||
|
||||
@@ -29,3 +29,9 @@ export const saveCardInput = z.object({
|
||||
transactionId: z.string(),
|
||||
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 {
|
||||
friendTransactionsInput,
|
||||
getUserInputSchema,
|
||||
initiateSaveCardInput,
|
||||
saveCardInput,
|
||||
@@ -453,54 +454,69 @@ export const userQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
transaction: router({
|
||||
friendTransactions: protectedProcedure.query(async (opts) => {
|
||||
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
friendTransactions: protectedProcedure
|
||||
.input(friendTransactionsInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, page } = input
|
||||
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) {
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError()
|
||||
// case 401:
|
||||
// throw unauthorizedError()
|
||||
// case 403:
|
||||
// throw forbiddenError()
|
||||
// default:
|
||||
// throw internalServerError()
|
||||
// }
|
||||
console.error(`API Response Failed - Getting Friend Transactions`)
|
||||
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
if (!apiResponse.ok) {
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError()
|
||||
// case 401:
|
||||
// throw unauthorizedError()
|
||||
// case 403:
|
||||
// throw forbiddenError()
|
||||
// default:
|
||||
// throw internalServerError()
|
||||
// }
|
||||
console.error(`API Response Failed - Getting Friend Transactions`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to validate Friend Transactions Data`)
|
||||
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to validate Friend Transactions Data`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
data: verifiedData.data.data.map(({ attributes }) => {
|
||||
return {
|
||||
awardPoints: attributes.awardPoints,
|
||||
checkinDate: attributes.checkinDate,
|
||||
checkoutDate: attributes.checkoutDate,
|
||||
city: attributes.hotelInformation?.city,
|
||||
confirmationNumber: attributes.confirmationNumber,
|
||||
hotelName: attributes.hotelInformation?.name,
|
||||
nights: attributes.nights,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
const pageData = verifiedData.data.data.slice(
|
||||
limit * (page - 1),
|
||||
limit * page
|
||||
)
|
||||
|
||||
return {
|
||||
data: {
|
||||
transactions: pageData.map(({ attributes }) => {
|
||||
return {
|
||||
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 }) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,9 +10,9 @@ export type TransactionResponse = Awaited<
|
||||
>
|
||||
export type TransactionsNonNullResponseObject = NonNullable<TransactionResponse>
|
||||
export type Transactions =
|
||||
NonNullable<TransactionsNonNullResponseObject>["data"]
|
||||
NonNullable<TransactionsNonNullResponseObject>["data"]["transactions"]
|
||||
export type Transaction =
|
||||
NonNullable<TransactionsNonNullResponseObject>["data"][number]
|
||||
NonNullable<TransactionsNonNullResponseObject>["data"]["transactions"][number]
|
||||
|
||||
export type ClientEarnAndBurnProps = {
|
||||
initialData: TransactionsNonNullResponseObject
|
||||
@@ -27,11 +27,6 @@ export interface TableProps {
|
||||
transactions: Transactions
|
||||
}
|
||||
|
||||
export interface TablePropsPagination extends TableProps {
|
||||
showMore: () => void
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
export interface RowProps {
|
||||
transaction: Transaction
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user