From 1e49c8dd72171c873c97fe079c25890725f4c6ea Mon Sep 17 00:00:00 2001 From: Drishit Mitra Date: Sun, 7 Jan 2024 20:33:16 +0530 Subject: [PATCH] Feat: Table view on large screens for transaction history --- app/components/TransactionsTable.tsx | 247 +++++++++++++++++++++++++++ app/routes/transaction/history.tsx | 54 ++++-- app/utils/text.utils.ts | 7 + 3 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 app/components/TransactionsTable.tsx diff --git a/app/components/TransactionsTable.tsx b/app/components/TransactionsTable.tsx new file mode 100644 index 0000000..6e55115 --- /dev/null +++ b/app/components/TransactionsTable.tsx @@ -0,0 +1,247 @@ +import type { Navigation, SubmitFunction, SubmitOptions } from "@remix-run/react"; +import { Form, Link, useNavigation, useOutletContext, useSubmit } from "@remix-run/react"; +import { Ripple } from "@rmwc/ripple"; +import type { SortingState } from "@tanstack/react-table"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import type { TransactionResponse } from "~/modules/transaction/transaction.schema"; +import type { AppContext } from "~/root"; +import { formatDate_YYYY_MM_DD } from "~/utils/date.utils"; +import { formatNumber } from "~/utils/number.utils"; +import EditIcon from "./icons/EditIcon"; +import RepeatIcon from "./icons/RepeatIcon"; +import TrashIcon from "./icons/TrashIcon"; +import { firstLetterToUpperCase } from "~/utils/text.utils"; +import { getTransactionColor } from "~/utils/colors.utils"; +import { useMemo, useState } from "react"; +import Decimal from "decimal.js"; + +const columnHelper = createColumnHelper(); + +function getColumns(context: AppContext, navigation: Navigation, submit: SubmitFunction) { + return [ + columnHelper.accessor("createdAt", { + header: () => "Date", + cell: (info) => ( +
{formatDate_YYYY_MM_DD(new Date(info.getValue()))}
+ ), + }), + columnHelper.accessor("type", { + header: () => "Type", + cell: (info) => {firstLetterToUpperCase(info.getValue())}, + }), + columnHelper.accessor("category", { + header: () => "Category", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("amount", { + header: () => "Amount", + cell: (info) => { + const transactionType = info.row.original.type; + return ( +
+ {formatNumber( + info.getValue().toString(), + context.userPreferredLocale ?? context.locale + )} +
+ ); + }, + sortingFn: (a, b) => + new Decimal(a.getValue("amount")) + .sub(new Decimal(b.getValue("amount"))) + .toNumber(), + }), + columnHelper.accessor("paymentMode", { + header: () => "Payment mode", + cell: (info) => info.getValue(), + }), + columnHelper.accessor("description", { + header: () => "Description", + cell: (info) => ( +
+ {info.getValue()} +
+ ), + meta: { + type: "longText", + }, + }), + columnHelper.display({ + id: "actions", + header: () => "", + meta: { + type: "actions", + }, + cell: ({ row }) => { + const isTransactionUpdateInProgress = + navigation.state === "submitting" && + navigation.formMethod === "DELETE" && + navigation.formData?.get("transactionId") === row.id; + return ( + + + + + + + + + + + + +
+ + + + +
+
+ ); + }, + }), + ]; +} + +export default function TransactionsTable({ + transactions, +}: { + transactions: TransactionResponse[]; +}) { + const context = useOutletContext(); + const navigation = useNavigation(); + const submit = useSubmit(); + const columns = useMemo(() => getColumns(context, navigation, submit), []); + const [sorting, setSorting] = useState([]); + const table = useReactTable({ + data: transactions, + columns: columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getRowId: (row) => row.id, + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, index) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell, index) => ( + + ))} + + ))} + +
+ {header.isPlaceholder ? null : ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: " ▲", + desc: " ▼", + }[header.column.getIsSorted() as string] ?? null} +
+ )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ); +} diff --git a/app/routes/transaction/history.tsx b/app/routes/transaction/history.tsx index 9187bf3..fceb897 100644 --- a/app/routes/transaction/history.tsx +++ b/app/routes/transaction/history.tsx @@ -36,6 +36,7 @@ import { InlineSpacer } from "~/components/InlineSpacer"; import type { NewFilter } from "~/components/FilterBottomSheet"; import FilterBottomSheet from "~/components/FilterBottomSheet"; import { divide, formatToCurrency, sum } from "~/utils/number.utils"; +import TransactionsTable from "~/components/TransactionsTable"; export const meta: MetaFunction = ({ matches }) => { let rootModule = matches.find((match) => match.id === "root"); @@ -129,11 +130,26 @@ export default function TransactionHistory() { const [expandedTransactionIndex, setExpandedTransactionIndex] = useState< number | undefined >(undefined); + const [isLargeScreen, setIsLargeScreen] = useState(false); useEffect(() => { context.showBackButton(true); }, [context]); + const onWindowResize = () => { + if (!document.hidden) { + setIsLargeScreen(window.innerWidth >= 1180); + } + }; + + useEffect(() => { + setIsLargeScreen(window.innerWidth >= 1180); + window.addEventListener("resize", onWindowResize); + return () => { + window.removeEventListener("resize", onWindowResize); + }; + }, []); + function setNewFilter(newFilter: NewFilter) { const { selectedCategories, month, selectedTypes, selectedPaymentModes } = newFilter; @@ -166,7 +182,7 @@ export default function TransactionHistory() {

Your transactions

-
+
-
    - {transactions.map((transaction, index) => { - return ( -
  • - -
  • - ); - })} -
+ {isLargeScreen ? ( + + ) : ( +
    + {transactions.map((transaction, index) => { + return ( +
  • + +
  • + ); + })} +
+ )}
diff --git a/app/utils/text.utils.ts b/app/utils/text.utils.ts index 86828b5..8735c08 100644 --- a/app/utils/text.utils.ts +++ b/app/utils/text.utils.ts @@ -27,3 +27,10 @@ export function base64ToBuffer(text: string) { }) ); } + +export function firstLetterToUpperCase(text: string) { + if (text == null) return ""; + if (text.length === 0) return ""; + if (text.length === 1) return text.toUpperCase(); + return text.charAt(0).toUpperCase() + text.slice(1); +}