diff --git a/package-lock.json b/package-lock.json index 484f24a..9036d4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "webapp-spa-react", "version": "0.0.0", "dependencies": { + "@tanstack/react-query": "^5.71.0", + "@tanstack/react-table": "^8.21.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^7.4.0" @@ -1321,6 +1323,65 @@ "win32" ] }, + "node_modules/@tanstack/query-core": { + "version": "5.71.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.71.0.tgz", + "integrity": "sha512-p4+T7CIEe1kMhii4booWiw42nuaiYI9La/bRCNzBaj1P3PDb0dEZYDhc/7oBifKJfHYN+mtS1ynW1qsmzQW7Og==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.71.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.71.0.tgz", + "integrity": "sha512-Udhlz9xHwk0iB7eLDchIqvu666NZFxPZZF80KnL8sZy+5J0kMvnJkzQNYRJwF70g8Vc1nn0TSMkPJgvx6+Pn4g==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.71.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz", + "integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz", + "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3196,9 +3257,9 @@ } }, "node_modules/vite": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz", - "integrity": "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", + "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", "dev": true, "license": "MIT", "dependencies": { @@ -3300,6 +3361,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 5cc7f2f..def592a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.71.0", + "@tanstack/react-table": "^8.21.2", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router": "^7.4.0" diff --git a/src/main.tsx b/src/main.tsx index cb313d8..fbe5e92 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,9 +3,14 @@ import { createRoot } from "react-dom/client"; import { RouterProvider } from "react-router"; import "./index.css"; import { router } from "./routes.ts"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +const queryClient = new QueryClient(); createRoot(document.getElementById("root")!).render( - + + + ); diff --git a/src/routes.ts b/src/routes.ts index a372e34..76b0b22 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -2,9 +2,7 @@ import { createBrowserRouter } from "react-router"; import App from "./root"; import Banks, { loader as banksLoader } from "./routes/banks"; import Categories, { loader as categoriesLoader } from "./routes/categories"; -import Transactions, { - loader as transactionsLoader, -} from "./routes/transactions"; +import Transactions from "./routes/transactions"; export const router = createBrowserRouter([ { @@ -14,7 +12,7 @@ export const router = createBrowserRouter([ { path: "transactions", Component: Transactions, - loader: transactionsLoader, + // loader: transactionsLoader, // loading transaction data is done on the component, as it need the pagination state }, { path: "banks", diff --git a/src/routes/transactions.tsx b/src/routes/transactions.tsx index 58ac78d..32c9ef2 100644 --- a/src/routes/transactions.tsx +++ b/src/routes/transactions.tsx @@ -1,37 +1,130 @@ -import { useLoaderData } from "react-router"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { + PaginationState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useState } from "react"; -export async function loader() { - return await fetch(`http://localhost:9000/transactions`).then((response) => { +const PageSize = 30; + +async function loader(page = 0) { + const limit = PageSize; + const offset = page * PageSize; + + return await fetch( + `http://localhost:9000/transactions?limit=${limit}&offset=${offset}` + ).then((response) => { return response.json(); }); } +type Transaction = { + id: number; + date: string; + description: string; + value: number; +}; + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor("date", {}), + columnHelper.accessor("description", {}), + columnHelper.accessor("value", {}), +]; + export default function Transactions() { - const data: { - id: number; - date: string; - description: string; - value: number; - }[] = useLoaderData(); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: PageSize, + }); + + // const data: Transaction[] = useLoaderData(); + const { isPending, data } = useQuery({ + queryKey: ["transactions", pagination.pageIndex], + queryFn: () => loader(pagination.pageIndex), + placeholderData: keepPreviousData, + }); + + const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + // getPaginationRowModel: getPaginationRowModel(), // not needed for server-side pagination + manualPagination: true, + // rowCount: , // TODO: get this from the server + pageCount: -1, + onPaginationChange: setPagination, + state: { + pagination, + }, + }); + + if (isPending) { + return
Loading...
; + } return ( - - - - - - - - - - {data.map((t) => ( - - - - - - ))} - -
datedescriptionvalue
{t.date}{t.description}{t.value}
+ <> + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ + + + +
+
Page {table.getState().pagination.pageIndex + 1}
+ ); }