diff --git a/package-lock.json b/package-lock.json index e33614f2..9bfdddb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tooltip": "1.2.8", "@tailwindcss/vite": "4.1.18", + "@tanstack/match-sorter-utils": "8.19.4", "@tanstack/react-query": "5.90.16", "@tanstack/react-table": "8.21.3", "class-variance-authority": "0.7.1", @@ -3460,6 +3461,22 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", + "license": "MIT", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/query-core": { "version": "5.90.16", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.16.tgz", @@ -7997,6 +8014,12 @@ "node": ">=8" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index 49701fd6..a19b1231 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tooltip": "1.2.8", "@tailwindcss/vite": "4.1.18", + "@tanstack/match-sorter-utils": "8.19.4", "@tanstack/react-query": "5.90.16", "@tanstack/react-table": "8.21.3", "class-variance-authority": "0.7.1", diff --git a/src/components/orderbook/OrderbookContent.tsx b/src/components/orderbook/OrderbookContent.tsx index 888a7398..7f0634d4 100644 --- a/src/components/orderbook/OrderbookContent.tsx +++ b/src/components/orderbook/OrderbookContent.tsx @@ -1,6 +1,7 @@ import { useState, useMemo } from 'react' import { DropdownMenu } from '@radix-ui/react-dropdown-menu' import { useQuery } from '@tanstack/react-query' +import type { RowModel } from '@tanstack/react-table' import type { i18n } from 'i18next' import { ChevronDownIcon, RefreshCwIcon, PlusIcon, AlertCircleIcon, Loader2Icon } from 'lucide-react' import { useTranslation } from 'react-i18next' @@ -171,34 +172,15 @@ export const OrderbookContent = ({ className }: OrderbookContentProps) => { ) const pinnedToTopOffers = useMemo(() => (isPinMyOffers ? myOffers : []), [isPinMyOffers, myOffers]) - const tableEntriesFiltered = useMemo(() => { - if (!tableEntries) return [] - const searchVal = searchInputRaw.replace('.', '').toLowerCase() - let offers = tableEntries - if (searchVal !== '') { - offers = offers.filter((entry) => { - return ( - entry.type.displayValue.toLowerCase().includes(searchVal) || - entry.counterparty.toLowerCase().includes(searchVal) || - entry.fee.displayValue.replace('.', '').toLowerCase().includes(searchVal) || - entry.minimumSize.replace('.', '').toLowerCase().includes(searchVal) || - entry.maximumSize.replace('.', '').toLowerCase().includes(searchVal) || - entry.minerFeeContribution.replace('.', '').toLowerCase().includes(searchVal) || - entry.bondValue.displayValue.replace('.', '').toLowerCase().includes(searchVal) || - entry.orderId.toLowerCase().includes(searchVal) - ) - }) - } - return offers - }, [tableEntries, searchInputRaw]) + const [tableRowModel, setTableRowModel] = useState>() const summary = useMemo(() => { - const uniqueCounterparties = new Set(tableEntriesFiltered.map((it) => it.counterparty)) + const uniqueCounterparties = new Set((tableRowModel?.rows ?? []).map((it) => it.original.counterparty)) return { - count: tableEntriesFiltered.length, + count: tableRowModel?.rows.length || 0, counterpartyCount: uniqueCounterparties.size, } - }, [tableEntriesFiltered]) + }, [tableRowModel]) const handleClearAndReload = async () => { await refetchOrderbookRefresh().then(() => refetchOrderbookData()) @@ -337,9 +319,13 @@ export const OrderbookContent = ({ className }: OrderbookContentProps) => { ) : ( { + setTableRowModel(table.getFilteredRowModel()) + }} /> )} diff --git a/src/components/orderbook/OrderbookTable.tsx b/src/components/orderbook/OrderbookTable.tsx index 0b7286d0..9d21c443 100644 --- a/src/components/orderbook/OrderbookTable.tsx +++ b/src/components/orderbook/OrderbookTable.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useEffect } from 'react' +import { rankItem } from '@tanstack/match-sorter-utils' import { createColumnHelper, flexRender, @@ -13,6 +14,9 @@ import { type RowSelectionState, type VisibilityState, type Column, + type FilterFn, + type FilterFnOption, + type Table as TableType, } from '@tanstack/react-table' import { ArrowUpDownIcon, @@ -26,7 +30,7 @@ import { import { useTranslation } from 'react-i18next' import { Badge } from '@/components/ui/badge' import { Balance } from '@/components/ui/jam/Balance' -import { Pagination } from '@/components/ui/pagination' +import { TablePagination } from '@/components/ui/jam/TablePagination' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn, BTC } from '@/lib/utils' @@ -92,16 +96,26 @@ const SortIcon = ({ sortKey, column, className }: SortIconProps) => { return dir === 'desc' ? : } +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value) + addMeta({ itemRank }) + return itemRank.passed +} + interface OrderbookTableProps { + globalFilter?: string tableEntries: OrderTableEntry[] selectedEntries: OrderTableEntry[] pinnedEntries: OrderTableEntry[] + onChange?: (table: TableType) => void } export const OrderbookTable = ({ + globalFilter, tableEntries, selectedEntries: highlightedEntries, pinnedEntries, + onChange, }: OrderbookTableProps) => { const { t } = useTranslation() @@ -230,10 +244,14 @@ export const OrderbookTable = ({ bottom: [], }) - const table = useReactTable({ + const table = useReactTable({ data: tableEntries, columns, + filterFns: { + fuzzy: fuzzyFilter, //define as a filter function that can be used in column definitions + }, state: { + globalFilter, sorting, pagination: { pageIndex: Math.max(0, currentPage - 1), @@ -243,6 +261,7 @@ export const OrderbookTable = ({ rowSelection, columnVisibility, }, + globalFilterFn: 'fuzzy' as FilterFnOption, keepPinnedRows: true, enableRowSelection: true, onSortingChange: setSorting, @@ -253,7 +272,6 @@ export const OrderbookTable = ({ getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), - manualFiltering: true, // we filter before passing to table }) useEffect(() => { @@ -302,6 +320,12 @@ export const OrderbookTable = ({ } } + useEffect(() => { + if (onChange) { + onChange(table) + } + }, [table, onChange]) + return (
@@ -385,7 +409,7 @@ export const OrderbookTable = ({
- svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 69ea7bcc..be558de1 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,36 +1,8 @@ import * as React from 'react' import { Slot } from '@radix-ui/react-slot' -import { type VariantProps, cva } from 'class-variance-authority' +import { type VariantProps } from 'class-variance-authority' import { cn } from '@/lib/utils' - -const buttonVariants = cva( - "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', - destructive: - 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', - outline: - 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', - secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', - 'ghost-navbar': 'hover:bg-zinc-700 light:hover:bg-zinc-200', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: 'h-9 px-4 py-2 has-[>svg]:px-3', - sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', - lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', - icon: 'size-9', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - }, -) +import { buttonVariants } from './button-variants' function Button({ className, diff --git a/src/components/ui/jam/TablePagination.tsx b/src/components/ui/jam/TablePagination.tsx new file mode 100644 index 00000000..c763baa4 --- /dev/null +++ b/src/components/ui/jam/TablePagination.tsx @@ -0,0 +1,124 @@ +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' +import { + Pagination, + PaginationContent, + PaginationFirst, + PaginationItem, + PaginationLast, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '../pagination' + +const DEFAULT_PAGE_SIZES = [25, 50, 100] +const DEFAULT_ALLOW_SHOW_ALL = true + +interface TablePaginationProps { + currentPage: number + totalPages: number + itemsPerPage: number + totalItems: number + pageSizes?: number[] + allowShowAll?: boolean + onPageChange: (page: number) => void + onItemsPerPageChange: (itemsPerPage: number) => void + className?: string +} + +const TablePagination = ({ + currentPage, + totalPages, + itemsPerPage, + totalItems, + onPageChange, + onItemsPerPageChange, + className, + pageSizes = DEFAULT_PAGE_SIZES, + allowShowAll = DEFAULT_ALLOW_SHOW_ALL, +}: TablePaginationProps) => { + const { t } = useTranslation() + const handlePageChange = (page: number) => { + if (page >= 1 && page <= totalPages) { + onPageChange(page) + } + } + + const handleItemsPerPageChange = (e: React.ChangeEvent) => { + onItemsPerPageChange(parseInt(e.target.value, 10)) + } + + const isShowingAll = itemsPerPage === -1 + const startItem = isShowingAll ? 1 : (currentPage - 1) * itemsPerPage + 1 + const endItem = isShowingAll ? totalItems : Math.min(currentPage * itemsPerPage, totalItems) + + return ( +
+
+ {t('global.table.pagination.items_per_page.label')} + +
+ +
+ {totalItems > 0 ? ( +
+ {`${startItem}-${endItem} of ${totalItems}`} +
+ ) : undefined} + + {!isShowingAll && ( + + + + handlePageChange(1)} disabled={currentPage === 1} /> + + + handlePageChange(currentPage - 1)} disabled={currentPage - 1 < 1} /> + + {[currentPage, currentPage - 1, currentPage + 1, currentPage - 2, currentPage + 2] + .filter((it) => it >= 1 && it <= totalPages) + .sort() + .slice(0, 3) + .map((it, index) => ( + + handlePageChange(it)}> + {it} + + + ))} + + handlePageChange(currentPage + 1)} + disabled={currentPage + 1 > totalPages} + /> + + + handlePageChange(totalPages)} disabled={currentPage === totalPages} /> + + + + )} +
+
+ ) +} + +export { TablePagination } diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx index eec090b5..e4a3f172 100644 --- a/src/components/ui/pagination.tsx +++ b/src/components/ui/pagination.tsx @@ -1,108 +1,119 @@ +import * as React from 'react' import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' -interface PaginationProps { - currentPage: number - totalPages: number - itemsPerPage: number - totalItems: number - onPageChange: (page: number) => void - onItemsPerPageChange: (itemsPerPage: number) => void - className?: string -} - -const Pagination = ({ - currentPage, - totalPages, - itemsPerPage, - totalItems, - onPageChange, - onItemsPerPageChange, - className, -}: PaginationProps) => { - const handlePageChange = (page: number) => { - if (page >= 1 && page <= totalPages) { - onPageChange(page) - } - } - - const handleItemsPerPageChange = (e: React.ChangeEvent) => { - onItemsPerPageChange(parseInt(e.target.value, 10)) - } - - const isShowingAll = itemsPerPage === -1 - const startItem = isShowingAll ? 1 : (currentPage - 1) * itemsPerPage + 1 - const endItem = isShowingAll ? totalItems : Math.min(currentPage * itemsPerPage, totalItems) - +function Pagination({ className, ...props }: React.ComponentProps<'nav'>) { return ( -
-
- Items per page - -
- -
-
- {totalItems > 0 ? `${startItem}-${endItem} of ${totalItems}` : '0-0 of 0'} -
- - {!isShowingAll && ( -
- - - - -
{currentPage}
- - - - -
- )} -
-
+