Skip to content

Commit

Permalink
Merge branch 'dev' of github.com:betfinio/components into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
bf-san committed Feb 20, 2025
2 parents 614902d + d74ad12 commit 8e13035
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 78 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@betfinio/components",

"version": "1.5.12",
"version": "1.6.0-dev.2",
"type": "module",
"exports": {
".": {
Expand Down
125 changes: 84 additions & 41 deletions src/components/shared/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { InitialTableState, Row, Table as TanstackTable } from '@tanstack/react-table';
import type { InitialTableState, OnChangeFn, Row, Table as TanstackTable } from '@tanstack/react-table';
import { type ColumnDef, type TableMeta, flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
import { cva } from 'class-variance-authority';
import { ArrowDown, ArrowDownIcon, ArrowUp, ArrowUpDown, ArrowUpIcon, ChevronsUpDown, Loader, MoveDown } from 'lucide-react';
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, Loader } from 'lucide-react';
import * as React from 'react';
import { cn, cn as cx } from '../../lib/utils';
import { DataTablePagination, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { DataTablePagination, DataTablePaginationProps, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';

interface DataTableProps<TData, TValue> {
interface PaginationState {
pageIndex: number;
pageSize: number;
}

type BaseDataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
isLoading?: boolean;
Expand All @@ -20,7 +25,24 @@ interface DataTableProps<TData, TValue> {
enableSorting?: boolean;
withZebra?: boolean;
className?: string;
}
serverPagination?: boolean;
};

type TableWithClientPaginationProps<TData, TValue> = BaseDataTableProps<TData, TValue> & {
serverPagination?: false | undefined;
totalCount?: number;
pagination?: PaginationState;
onPaginationChange?: (pagination: PaginationState) => void;
};

type TableWithServerPaginationProps<TData, TValue> = BaseDataTableProps<TData, TValue> & {
serverPagination: true;
totalCount: number;
pagination: PaginationState;
onPaginationChange: (pagination: PaginationState) => void;
};

export type DataTableProps<TData, TValue> = TableWithClientPaginationProps<TData, TValue> | TableWithServerPaginationProps<TData, TValue>;

export function DataTable<TData, TValue>({
columns,
Expand All @@ -32,73 +54,94 @@ export function DataTable<TData, TValue>({
onRowClick,
loaderClassName,
noResultsClassName,
tableRef, // Accept the tableRef prop
enableSorting = false, // Add default value
tableRef,
enableSorting = false,
withZebra = true,
className = '',
serverPagination = false,
totalCount,
pagination: controlledPagination,
onPaginationChange: controlledOnPaginationChange,
}: DataTableProps<TData, TValue>) {
React.useImperativeHandle(tableRef, () => table);
const [internalPagination, setInternalPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 5,
});
const effectivePagination = controlledPagination || internalPagination;

const handlePaginationChange = React.useCallback(
(updater: (prev: PaginationState) => PaginationState) => {
const newPagination = updater(effectivePagination);
if (serverPagination && controlledOnPaginationChange) {
controlledOnPaginationChange(newPagination);
} else {
setInternalPagination(newPagination);
}
},
[effectivePagination, serverPagination, controlledOnPaginationChange],
);

const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getPaginationRowModel: serverPagination ? undefined : getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
enableSorting,
meta: meta,
initialState: {
pagination: {
pageSize: 5,
pageIndex: 0,
},
...state,
},
meta,
manualPagination: serverPagination,
rowCount: serverPagination && totalCount !== undefined ? totalCount : undefined,
state: { pagination: effectivePagination },
onPaginationChange: handlePaginationChange as OnChangeFn<PaginationState>,
initialState: { pagination: { pageIndex: 0, pageSize: 5 }, ...state },
});

React.useImperativeHandle(tableRef, () => table, [table]);

return (
<div className={cn(cva('w-full')({ className }))}>
<div className="rounded-lg border border-border w-full">
<Table className="">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead
key={header.id}
className={cx(header.column.columnDef.meta?.className, header.column.getCanSort() && 'cursor-pointer select-none')}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center gap-1">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() && (
<div className="w-4 h-4">
{header.column.getIsSorted() === 'asc' && <ArrowUp className="w-4 h-4" />}
{header.column.getIsSorted() === 'desc' && <ArrowDown className="w-4 h-4" />}
{header.column.getIsSorted() === false && <ArrowUpDown className="w-4 h-4 opacity-50" />}
</div>
)}
</div>
</TableHead>
);
})}
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className={cx(header.column.columnDef.meta?.className, header.column.getCanSort() && 'cursor-pointer select-none')}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center gap-1">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanSort() && (
<div className="w-4 h-4">
{header.column.getIsSorted() === 'asc' && <ArrowUpIcon className="w-4 h-4" />}
{header.column.getIsSorted() === 'desc' && <ArrowDownIcon className="w-4 h-4" />}
{header.column.getIsSorted() === false && <ArrowUpDownIcon className="w-4 h-4 opacity-50" />}
</div>
)}
</div>
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className={cx('h-[200px]', loaderClassName)}>
<div className={'flex items-center justify-center'}>
<Loader className={'animate-spin'} />
<div className="flex items-center justify-center">
<Loader className="animate-spin" />
</div>
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row, index) => (
<TableRow
className={cx('cursor-pointer', { 'bg-background-lighter': withZebra && index % 2 === 0 && !row.getIsSelected() })}
key={row.id}
className={cx('cursor-pointer', {
'bg-background-lighter': withZebra && index % 2 === 0 && !row.getIsSelected(),
})}
data-state={row.getIsSelected() && 'selected'}
onClick={() => onRowClick?.(row.original, row)}
data-row-id={row.id}
Expand All @@ -120,7 +163,7 @@ export function DataTable<TData, TValue>({
</TableBody>
</Table>
</div>
{!hidePagination && <DataTablePagination table={table} />}
{!hidePagination && <DataTablePagination table={table} isLoading={isLoading} />}
</div>
);
}
56 changes: 42 additions & 14 deletions src/components/ui/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,51 @@ interface CustomProps {
trackClassName?: string;
rangeClassName?: string;
thumbClassName?: string;
invertBorder?: boolean;
}

const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & CustomProps>(
({ className, trackClassName, rangeClassName, thumbClassName, ...props }, ref) => (
<SliderPrimitive.Root ref={ref} className={cn('relative flex w-full touch-none select-none items-center', className)} {...props}>
<SliderPrimitive.Track className={cn('relative h-[2px] w-full grow overflow-hidden rounded-full bg-secondary', trackClassName)}>
<SliderPrimitive.Range className={cn('absolute h-full bg-primary', rangeClassName)} />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
className={cn(
'block h-3 w-3 cursor-pointer rounded-full border-2 border-primary bg-primary transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
thumbClassName,
)}
/>
<div className={'absolute left-0 top-[-10px] bottom-[-20px] w-full '} />
</SliderPrimitive.Root>
),
({ className, trackClassName, rangeClassName, thumbClassName, invertBorder = false, value, min = 0, max = 100, onValueChange, ...props }, ref) => {
// Ajusta o valor para manter a mesma posição visual quando invertido
const adjustedValue = React.useMemo(() => {
return invertBorder ? value?.map((v) => max + min - v) : value;
}, [value, invertBorder, max, min]);

// Handler que re-ajusta o valor antes de enviá-lo para o callback original
const handleValueChange = React.useCallback(
(newValue: number[]) => {
if (onValueChange) {
const readjustedValue = invertBorder ? newValue.map((v) => max + min - v) : newValue;
onValueChange(readjustedValue);
}
},
[onValueChange, invertBorder, max, min],
);

return (
<SliderPrimitive.Root
ref={ref}
inverted={invertBorder}
value={adjustedValue}
min={min}
max={max}
onValueChange={handleValueChange}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className={cn('relative h-[2px] w-full grow overflow-hidden rounded-full bg-secondary', trackClassName)}>
<SliderPrimitive.Range className={cn('absolute h-full', 'border-2 border-primary', rangeClassName)} />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
className={cn(
'block h-3 w-3 cursor-pointer rounded-full border-2 border-primary bg-primary transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
thumbClassName,
)}
/>
<div className={'absolute left-0 top-[-10px] bottom-[-20px] w-full '} />
</SliderPrimitive.Root>
);
},
);
Slider.displayName = SliderPrimitive.Root.displayName;

Expand Down
57 changes: 35 additions & 22 deletions src/components/ui/table.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { Table as ReactTable } from '@tanstack/react-table';
import * as React from 'react';

import { Button } from './button.tsx';

import { motion } from 'framer-motion';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { cn } from '../../lib/utils';
import { Button } from './button.tsx';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';

const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
Expand Down Expand Up @@ -54,13 +53,32 @@ TableCaption.displayName = 'TableCaption';
interface DataTablePaginationProps<TData> {
table: ReactTable<TData>;
className?: string;
isLoading?: boolean;
}

function DataTablePagination<TData>({ table, className = '' }: DataTablePaginationProps<TData>) {
function DataTablePagination<TData>({ table, className = '', isLoading = false }: DataTablePaginationProps<TData>) {
const { t } = useTranslation('shared', { keyPrefix: 'tables' });
const { pageIndex, pageSize } = table.getState().pagination;

const computedPageCount = table.getPageCount();
const canPreviousPage = pageIndex > 0;
const canNextPage = table.getCanNextPage();

const handlePageChange = (newPageIndex: number) => {
table.setPageIndex(newPageIndex);
};

if (table.getFilteredRowModel().rows.length === 0) {
return <div className={'h-12 mt-2'} />;
const handlePageSizeChange = (value: string) => {
table.setPageSize(Number(value));
};

const totalCount = table.options.rowCount as number | undefined;

// rowCount is 'undfined' on client-side pagination, we can use it as a flag to pick rows count from filtered model
const resultsCount = totalCount !== undefined ? totalCount : table.getFilteredRowModel().rows.length;

if (resultsCount === 0) {
return <div className="h-12 mt-2" />;
}
return (
<motion.div
Expand All @@ -70,19 +88,14 @@ function DataTablePagination<TData>({ table, className = '' }: DataTablePaginati
className={cn('flex items-center justify-between py-2 mt-2', className)}
>
<div className="flex-1 text-xs text-muted-foreground">
{table.getFilteredRowModel().rows.length} {t('results')}.
{resultsCount} {t('results')}.
</div>
<div className="flex items-center space-x-1 lg:space-x-4">
<div className="flex items-center space-x-1">
<p className="hidden sm:block text-xs font-medium">{t('resultsPerPage')}</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value));
}}
>
<Select value={`${pageSize}`} onValueChange={handlePageSizeChange}>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
<SelectValue placeholder={pageSize} />
</SelectTrigger>
<SelectContent side="top">
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
Expand All @@ -94,28 +107,28 @@ function DataTablePagination<TData>({ table, className = '' }: DataTablePaginati
</Select>
</div>
<div className="flex items-center justify-center text-xs font-medium">
<span className={'capitalize pr-1'}>{t('page')}</span> {table.getState().pagination.pageIndex + 1}
<span className={'px-1'}>{t('of')}</span>
{table.getPageCount()}
<span className="capitalize pr-1">{t('page')}</span> {pageIndex + 1}
<span className="px-1">{t('of')}</span>
{computedPageCount}
</div>
<div className="flex items-center space-x-2">
<Button variant="outline" className="hidden h-8 w-8 p-0 lg:flex" onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>
<Button variant="outline" className="hidden h-8 w-8 p-0 lg:flex" onClick={() => handlePageChange(0)} disabled={!canPreviousPage || isLoading}>
<span className="sr-only">{t('goTo.first')}</span>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button variant="outline" className="h-8 w-8 p-0" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
<Button variant="outline" className="h-8 w-8 p-0" onClick={() => handlePageChange(pageIndex - 1)} disabled={!canPreviousPage || isLoading}>
<span className="sr-only">{t('goTo.previous')}</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button variant="outline" className="h-8 w-8 p-0" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
<Button variant="outline" className="h-8 w-8 p-0" onClick={() => handlePageChange(pageIndex + 1)} disabled={!canNextPage || isLoading}>
<span className="sr-only">{t('goTo.next')}</span>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
onClick={() => handlePageChange(computedPageCount - 1)}
disabled={!canNextPage || isLoading}
>
<span className="sr-only">{t('goTo.last')}</span>
<ChevronsRight className="h-4 w-4" />
Expand All @@ -126,4 +139,4 @@ function DataTablePagination<TData>({ table, className = '' }: DataTablePaginati
);
}

export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, DataTablePagination };
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, DataTablePagination, type DataTablePaginationProps };

0 comments on commit 8e13035

Please sign in to comment.