feat(SW-66, SW-348): search functionality and ui
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useState } from "react"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
import ClientTable from "./ClientTable"
|
||||
import Pagination from "./Pagination"
|
||||
|
||||
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export default function TransactionTable({
|
||||
initialJourneyTransactions,
|
||||
}: {
|
||||
initialJourneyTransactions: {
|
||||
data: { transactions: Transactions }
|
||||
meta: { totalPages: number }
|
||||
}
|
||||
}) {
|
||||
const limit = 5
|
||||
const [page, setPage] = useState(1)
|
||||
const { data, isFetching, isLoading } =
|
||||
trpc.user.transaction.friendTransactions.useQuery(
|
||||
{
|
||||
limit,
|
||||
page,
|
||||
},
|
||||
{
|
||||
// TODO: fix the initial data issues on page load
|
||||
// initialData: initialJourneyTransactions,
|
||||
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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { webviews } from "@/constants/routes/webviews"
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Table from "@/components/TempDesignSystem/Table"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import AwardPoints from "../../../AwardPoints"
|
||||
|
||||
import type { RowProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
import { Transactions } from "@/types/enums/transactions"
|
||||
|
||||
export default function Row({ transaction }: RowProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const pathName = usePathname()
|
||||
const isWebview = webviews.includes(pathName)
|
||||
|
||||
const nightString = `${transaction.nights} ${transaction.nights === 1 ? intl.formatMessage({ id: "night" }) : intl.formatMessage({ id: "nights" })}`
|
||||
|
||||
let description =
|
||||
transaction.hotelName && transaction.city
|
||||
? `${transaction.hotelName}, ${transaction.city} ${nightString}`
|
||||
: `${nightString}`
|
||||
|
||||
switch (transaction.type) {
|
||||
case Transactions.rewardType.stay:
|
||||
case Transactions.rewardType.stayAdj:
|
||||
if (transaction.hotelId === "ORS") {
|
||||
description = intl.formatMessage({ id: "Former Scandic Hotel" })
|
||||
}
|
||||
if (transaction.confirmationNumber === "BALFWD") {
|
||||
description = intl.formatMessage({
|
||||
id: "Points earned prior to May 1, 2021",
|
||||
})
|
||||
}
|
||||
break
|
||||
case Transactions.rewardType.ancillary:
|
||||
description = intl.formatMessage({ id: "Extras to your booking" })
|
||||
break
|
||||
case Transactions.rewardType.enrollment:
|
||||
description = intl.formatMessage({ id: "Sign up bonus" })
|
||||
break
|
||||
case Transactions.rewardType.mastercard_points:
|
||||
description = intl.formatMessage({ id: "Scandic Friends Mastercard" })
|
||||
break
|
||||
case Transactions.rewardType.tui_points:
|
||||
description = intl.formatMessage({ id: "TUI Points" })
|
||||
|
||||
case Transactions.rewardType.pointShop:
|
||||
description = intl.formatMessage({ id: "Scandic Friends Point Shop" })
|
||||
break
|
||||
}
|
||||
|
||||
const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY")
|
||||
|
||||
function renderConfirmationNumber() {
|
||||
if (transaction.confirmationNumber === "BALFWD") return null
|
||||
|
||||
if (
|
||||
!isWebview &&
|
||||
transaction.bookingUrl &&
|
||||
(transaction.type === Transactions.rewardType.stay ||
|
||||
transaction.type === Transactions.rewardType.rewardNight)
|
||||
) {
|
||||
return (
|
||||
<Link variant="underscored" href={transaction.bookingUrl}>
|
||||
{transaction.confirmationNumber}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return transaction.confirmationNumber
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.TR>
|
||||
<Table.TD>
|
||||
<AwardPoints
|
||||
awardPoints={transaction.awardPoints}
|
||||
isCalculated={transaction.pointsCalculated}
|
||||
/>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
<Body textTransform="bold">{description}</Body>
|
||||
</Table.TD>
|
||||
<Table.TD>{renderConfirmationNumber()}</Table.TD>
|
||||
<Table.TD>
|
||||
{transaction.checkinDate && transaction.confirmationNumber !== "BALFWD"
|
||||
? arrival
|
||||
: null}
|
||||
</Table.TD>
|
||||
</Table.TR>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.container {
|
||||
overflow-x: auto;
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--Scandic-Brand-Pale-Peach);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Table from "@/components/TempDesignSystem/Table"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import Row from "./Row"
|
||||
|
||||
import styles from "./clientTable.module.css"
|
||||
|
||||
import type { ClientTableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
const tableHeadings = [
|
||||
"Points",
|
||||
"Description",
|
||||
"Booking number",
|
||||
"Arrival date",
|
||||
]
|
||||
|
||||
export default function ClientTable({ transactions }: ClientTableProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Table>
|
||||
<Table.THead>
|
||||
<Table.TR>
|
||||
{tableHeadings.map((heading) => (
|
||||
<Table.TH key={heading}>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: heading })}
|
||||
</Body>
|
||||
</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: "No transactions available" })}
|
||||
</Table.TD>
|
||||
</Table.TR>
|
||||
)}
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./pagination.module.css"
|
||||
|
||||
import {
|
||||
PaginationButtonProps,
|
||||
PaginationProps,
|
||||
} from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
function PaginationButton({
|
||||
children,
|
||||
isActive,
|
||||
handleClick,
|
||||
disabled,
|
||||
}: React.PropsWithChildren<PaginationButtonProps>) {
|
||||
return (
|
||||
<button
|
||||
type={"button"}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
className={`${styles.paginationButton} ${isActive ? styles.paginationButtonActive : ""}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Pagination({
|
||||
pageCount,
|
||||
isFetching,
|
||||
handlePageChange,
|
||||
currentPage,
|
||||
}: PaginationProps) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
padding: var(--Spacing-x2);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
margin: auto;
|
||||
gap: var(--Spacing-x5);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.paginationButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
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;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paginationButton[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.chevronLeft {
|
||||
transform: rotate(180deg);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.paginationButtonActive {
|
||||
color: var(--WHITE);
|
||||
background-color: var(--Base-Text-Accent);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import ClientJourney from "./Client"
|
||||
|
||||
export default async function JourneyTable() {
|
||||
const initialJourneyTransactions =
|
||||
await serverClient().user.transaction.friendTransactions({
|
||||
page: 1,
|
||||
limit: 5,
|
||||
})
|
||||
if (!initialJourneyTransactions?.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientJourney initialJourneyTransactions={initialJourneyTransactions} />
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user