From dd2f8092bc9dc7f5bf124f6101a06c4383479a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Sat, 12 Apr 2025 19:41:31 +0100 Subject: [PATCH] Category column filtering Uses server-side filtering through GET params. Imported react-debounce-input to add a delay from the user input to the backend request. This will reduce unnecessary bandwidth. --- package-lock.json | 129 +++++++++++++++++------------------- package.json | 1 + src/routes/transactions.tsx | 75 +++++++++++++++------ 3 files changed, 118 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9036d4a..d94d62c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tanstack/react-query": "^5.71.0", "@tanstack/react-table": "^8.21.2", "react": "^19.0.0", + "react-debounce-input": "^3.3.0", "react-dom": "^19.0.0", "react-router": "^7.4.0" }, @@ -2370,21 +2371,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2509,7 +2495,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2612,6 +2597,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2619,6 +2610,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2706,6 +2709,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2848,6 +2860,17 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2888,6 +2911,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-debounce-input": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz", + "integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", @@ -2900,6 +2936,12 @@ "react": "^19.0.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -2944,18 +2986,6 @@ "node": ">=4" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -3137,28 +3167,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, "node_modules/turbo-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", @@ -3257,9 +3265,9 @@ } }, "node_modules/vite": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", - "integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dev": true, "license": "MIT", "dependencies": { @@ -3361,21 +3369,6 @@ "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 def592a..8f68af8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@tanstack/react-query": "^5.71.0", "@tanstack/react-table": "^8.21.2", "react": "^19.0.0", + "react-debounce-input": "^3.3.0", "react-dom": "^19.0.0", "react-router": "^7.4.0" }, diff --git a/src/routes/transactions.tsx b/src/routes/transactions.tsx index 32c9ef2..df35da7 100644 --- a/src/routes/transactions.tsx +++ b/src/routes/transactions.tsx @@ -1,24 +1,26 @@ import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { PaginationState, + ColumnFiltersState, createColumnHelper, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { useState } from "react"; +import { DebounceInput } from "react-debounce-input"; const PageSize = 30; -async function loader(page = 0) { - const limit = PageSize; - const offset = page * PageSize; +async function loader(page = 0, category: string | undefined = "") { + const url = new URL("http://localhost:9000/transactions"); + url.search = new URLSearchParams({ + limit: String(PageSize), + offset: String(page * PageSize), + ...(category !== "" && { category: category }), + }).toString(); - return await fetch( - `http://localhost:9000/transactions?limit=${limit}&offset=${offset}` - ).then((response) => { - return response.json(); - }); + return await fetch(url).then((response) => response.json()); } type Transaction = { @@ -26,14 +28,22 @@ type Transaction = { date: string; description: string; value: number; + category: string; }; const columnHelper = createColumnHelper(); const columns = [ - columnHelper.accessor("date", {}), - columnHelper.accessor("description", {}), - columnHelper.accessor("value", {}), + columnHelper.accessor("date", { + enableColumnFilter: false, + }), + columnHelper.accessor("description", { + enableColumnFilter: false, + }), + columnHelper.accessor("value", { + enableColumnFilter: false, + }), + columnHelper.accessor("category", {}), ]; export default function Transactions() { @@ -41,11 +51,20 @@ export default function Transactions() { pageIndex: 0, pageSize: PageSize, }); + const [columnFilters, setColumnFilters] = useState([ + { + id: "category", + value: "", + }, + ]); - // const data: Transaction[] = useLoaderData(); - const { isPending, data } = useQuery({ - queryKey: ["transactions", pagination.pageIndex], - queryFn: () => loader(pagination.pageIndex), + const { data, isPending, isError } = useQuery({ + queryKey: ["transactions", pagination.pageIndex, columnFilters], + queryFn: () => + loader( + pagination.pageIndex, + columnFilters.find((filter) => filter.id == "category")!.value as string + ), placeholderData: keepPreviousData, }); @@ -53,13 +72,15 @@ export default function Transactions() { columns, data, getCoreRowModel: getCoreRowModel(), - // getPaginationRowModel: getPaginationRowModel(), // not needed for server-side pagination manualPagination: true, + manualFiltering: true, // rowCount: , // TODO: get this from the server pageCount: -1, onPaginationChange: setPagination, + onColumnFiltersChange: setColumnFilters, state: { pagination, + columnFilters, }, }); @@ -67,6 +88,10 @@ export default function Transactions() { return
Loading...
; } + if (isError) { + return
Error loading transactions
; + } + return ( <> @@ -75,12 +100,24 @@ export default function Transactions() { {headerGroup.headers.map((header) => ( ))}
- {header.isPlaceholder - ? null - : flexRender( + {header.isPlaceholder ? null : ( + <> + {flexRender( header.column.columnDef.header, header.getContext() )} + {header.column.getCanFilter() ? ( +
+ + header.column.setFilterValue(e.target.value) + } + /> +
+ ) : null} + + )}