mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-19 13:18:30 +02:00
chore(orderbook): use filter function of library
This commit is contained in:
parent
a45116b5d6
commit
16aa02b234
8 changed files with 329 additions and 158 deletions
23
package-lock.json
generated
23
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<OrderTableEntry[]>(() => (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<RowModel<OrderTableEntry>>()
|
||||
|
||||
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) => {
|
|||
</div>
|
||||
) : (
|
||||
<OrderbookTable
|
||||
tableEntries={tableEntriesFiltered}
|
||||
tableEntries={tableEntries}
|
||||
selectedEntries={highlightedOffers}
|
||||
pinnedEntries={pinnedToTopOffers}
|
||||
globalFilter={searchInputRaw}
|
||||
onChange={(table) => {
|
||||
setTableRowModel(table.getFilteredRowModel())
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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' ? <SortDescIcon className={className} /> : <SortAscIcon className={className} />
|
||||
}
|
||||
|
||||
const fuzzyFilter: FilterFn<OrderTableEntry> = (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<OrderTableEntry>) => 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<OrderTableEntry>({
|
||||
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<OrderTableEntry>,
|
||||
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 (
|
||||
<div className="flex flex-1 flex-col gap-2 overflow-hidden rounded-lg border shadow-lg">
|
||||
<div className="flex-1 overflow-auto">
|
||||
|
|
@ -385,7 +409,7 @@ export const OrderbookTable = ({
|
|||
</Table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
<TablePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
itemsPerPage={itemsPerPage}
|
||||
|
|
|
|||
30
src/components/ui/button-variants.ts
Normal file
30
src/components/ui/button-variants.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export 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',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
124
src/components/ui/jam/TablePagination.tsx
Normal file
124
src/components/ui/jam/TablePagination.tsx
Normal file
|
|
@ -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<HTMLSelectElement>) => {
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-2 px-2 py-4 sm:flex-row sm:justify-between',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="text-muted-foreground flex items-center space-x-2 text-sm">
|
||||
<span>{t('global.table.pagination.items_per_page.label')}</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={handleItemsPerPageChange}
|
||||
className="border-input bg-background rounded border px-2 py-1 text-sm"
|
||||
>
|
||||
{pageSizes.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
{allowShowAll && (
|
||||
<option value={-1}>{t('global.table.pagination.items_per_page.text_option_show_all')}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{totalItems > 0 ? (
|
||||
<div className="hidden text-sm font-medium text-nowrap sm:block">
|
||||
{`${startItem}-${endItem} of ${totalItems}`}
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
{!isShowingAll && (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationFirst onClick={() => handlePageChange(1)} disabled={currentPage === 1} />
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage - 1 < 1} />
|
||||
</PaginationItem>
|
||||
{[currentPage, currentPage - 1, currentPage + 1, currentPage - 2, currentPage + 2]
|
||||
.filter((it) => it >= 1 && it <= totalPages)
|
||||
.sort()
|
||||
.slice(0, 3)
|
||||
.map((it, index) => (
|
||||
<PaginationItem key={index}>
|
||||
<PaginationLink isActive={it === currentPage} onClick={() => handlePageChange(it)}>
|
||||
{it}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage + 1 > totalPages}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLast onClick={() => handlePageChange(totalPages)} disabled={currentPage === totalPages} />
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { TablePagination }
|
||||
|
|
@ -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<HTMLSelectElement>) => {
|
||||
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 (
|
||||
<div className={cn('flex items-center justify-between px-2 py-4', className)}>
|
||||
<div className="text-muted-foreground flex items-center space-x-2 text-sm">
|
||||
<span>Items per page</span>
|
||||
<select
|
||||
value={itemsPerPage}
|
||||
onChange={handleItemsPerPageChange}
|
||||
className="border-input bg-background rounded border px-2 py-1 text-sm"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={-1}>All</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||
<div className="text-sm font-medium">
|
||||
{totalItems > 0 ? `${startItem}-${endItem} of ${totalItems}` : '0-0 of 0'}
|
||||
</div>
|
||||
|
||||
{!isShowingAll && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="hidden lg:flex"
|
||||
>
|
||||
<ChevronsLeftIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center text-sm font-medium">{currentPage}</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="hidden lg:flex"
|
||||
>
|
||||
<ChevronsRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Pagination }
|
||||
function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
|
||||
return <ul data-slot="pagination-content" className={cn('flex flex-row items-center gap-1', className)} {...props} />
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
||||
React.ComponentProps<'button'>
|
||||
|
||||
function PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) {
|
||||
return (
|
||||
<Button
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
size={size}
|
||||
variant={isActive ? 'outline' : 'ghost'}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label={t('global.table.pagination.page_selector.label_previous')}
|
||||
title={t('global.table.pagination.page_selector.label_previous')}
|
||||
size="default"
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">{t('global.table.pagination.page_selector.label_previous')}</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationFirst({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label={t('global.table.pagination.page_selector.label_first')}
|
||||
title={t('global.table.pagination.page_selector.label_first')}
|
||||
size="default"
|
||||
className={cn('gap-1 px-2.5 md:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronsLeftIcon />
|
||||
<span className="hidden md:block">{t('global.table.pagination.page_selector.label_first')}</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationLast({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label={t('global.table.pagination.page_selector.label_last')}
|
||||
title={t('global.table.pagination.page_selector.label_last')}
|
||||
size="default"
|
||||
className={cn('gap-1 px-2.5 md:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden md:block">{t('global.table.pagination.page_selector.label_last')}</span>
|
||||
<ChevronsRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({ className, ...props }: React.ComponentProps<typeof PaginationLink>) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label={t('global.table.pagination.page_selector.label_next')}
|
||||
title={t('global.table.pagination.page_selector.label_next')}
|
||||
size="default"
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">{t('global.table.pagination.page_selector.label_next')}</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationFirst,
|
||||
PaginationLast,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue