From 9cc83958482968f8fe157bc9ac196a3576fa2fac Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 30 Aug 2026 21:02:36 +0700 Subject: [PATCH] feat: enhance table styling with background color and text color adjustments --- resources/js/components/data-table.tsx | 972 ++++++++++++------------- resources/js/components/ui/table.tsx | 4 +- 2 files changed, 488 insertions(+), 488 deletions(-) diff --git a/resources/js/components/data-table.tsx b/resources/js/components/data-table.tsx index 3f7863f..fcea6e3 100644 --- a/resources/js/components/data-table.tsx +++ b/resources/js/components/data-table.tsx @@ -1,30 +1,30 @@ import type { DragEndEvent } from '@dnd-kit/core'; import { - DndContext, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, + DndContext, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, } from '@dnd-kit/core'; import { - SortableContext, - useSortable, - verticalListSortingStrategy, + SortableContext, + useSortable, + verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import type { ColumnDef, ExpandedState, Row } from '@tanstack/react-table'; import { - flexRender, - getCoreRowModel, - getExpandedRowModel, - useReactTable, + flexRender, + getCoreRowModel, + getExpandedRowModel, + useReactTable, } from '@tanstack/react-table'; import { - ChevronLeft, - ChevronRight, - ChevronsLeft, - ChevronsRight, - GripVertical, + ChevronLeft, + ChevronRight, + ChevronsLeft, + ChevronsRight, + GripVertical, } from 'lucide-react'; import * as React from 'react'; @@ -32,528 +32,528 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@/components/ui/select'; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from '@/components/ui/table'; export interface PaginationState { - current_page: number; - last_page: number; - per_page: number; - total: number; + current_page: number; + last_page: number; + per_page: number; + total: number; } interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - searchKey?: string; - searchPlaceholder?: string; - emptyText?: string; - onReorder?: (items: TData[]) => void; - getRowId?: (item: TData) => string | number; - toolbar?: React.ReactNode; - renderSubRow?: (row: Row, searchValue?: string) => React.ReactNode; - defaultExpanded?: boolean; - pagination?: PaginationState; - onPageChange?: (page: number) => void; - onPerPageChange?: (perPage: number) => void; - onSearchChange?: (search: string) => void; - searchValue?: string; + columns: ColumnDef[]; + data: TData[]; + searchKey?: string; + searchPlaceholder?: string; + emptyText?: string; + onReorder?: (items: TData[]) => void; + getRowId?: (item: TData) => string | number; + toolbar?: React.ReactNode; + renderSubRow?: (row: Row, searchValue?: string) => React.ReactNode; + defaultExpanded?: boolean; + pagination?: PaginationState; + onPageChange?: (page: number) => void; + onPerPageChange?: (perPage: number) => void; + onSearchChange?: (search: string) => void; + searchValue?: string; } const DragHandleContext = React.createContext<{ - listeners?: Record; - attributes?: Record; + listeners?: Record; + attributes?: Record; }>({}); export function DragHandleTrigger() { - const { listeners, attributes } = React.useContext(DragHandleContext); + const { listeners, attributes } = React.useContext(DragHandleContext); - return ( - - ); + return ( + + ); } function SortableTableRow({ - id, - children, + id, + children, }: { - id: string; - children: React.ReactNode; + id: string; + children: React.ReactNode; }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id }); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.4 : undefined, - }; + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.4 : undefined, + }; - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } function useDebounce(callback: (value: string) => void, delay: number) { - const timeoutRef = React.useRef | null>(null); + const timeoutRef = React.useRef | null>(null); - return React.useCallback( - (value: string) => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } + return React.useCallback( + (value: string) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } - timeoutRef.current = setTimeout(() => { - callback(value); - }, delay); - }, - [callback, delay], - ); + timeoutRef.current = setTimeout(() => { + callback(value); + }, delay); + }, + [callback, delay], + ); } export function DataTable({ - columns, - data, - searchKey, - searchPlaceholder = 'Ketikkan sesuatu...', - emptyText = 'Tidak ada data.', - onReorder, - getRowId, - toolbar, - renderSubRow, - defaultExpanded = false, - pagination, - onPageChange, - onPerPageChange, - onSearchChange, - searchValue, + columns, + data, + searchKey, + searchPlaceholder = 'Ketikkan sesuatu...', + emptyText = 'Tidak ada data.', + onReorder, + getRowId, + toolbar, + renderSubRow, + defaultExpanded = false, + pagination, + onPageChange, + onPerPageChange, + onSearchChange, + searchValue, }: DataTableProps) { - const [expanded, setExpanded] = React.useState(() => { - if (!defaultExpanded || !data.length) { - return {}; - } + const [expanded, setExpanded] = React.useState(() => { + if (!defaultExpanded || !data.length) { + return {}; + } - const initial: Record = {}; - data.forEach((item, index) => { - initial[String(index)] = true; + const initial: Record = {}; + data.forEach((item, index) => { + initial[String(index)] = true; + }); + + return initial; }); - return initial; - }); + const [localSearch, setLocalSearch] = React.useState(searchValue ?? ''); - const [localSearch, setLocalSearch] = React.useState(searchValue ?? ''); + React.useEffect(() => { + setLocalSearch(searchValue ?? ''); + }, [searchValue]); - React.useEffect(() => { - setLocalSearch(searchValue ?? ''); - }, [searchValue]); + const isServerMode = !!pagination && !!onPageChange; + const isSortable = !!onReorder && !!getRowId; - const isServerMode = !!pagination && !!onPageChange; - const isSortable = !!onReorder && !!getRowId; + const handleSearchDebounced = useDebounce( + (value: string) => onSearchChange?.(value), + 300, + ); - const handleSearchDebounced = useDebounce( - (value: string) => onSearchChange?.(value), - 300, - ); + const totalPages = pagination?.last_page ?? 1; + const currentPage = pagination?.current_page ?? 1; - const totalPages = pagination?.last_page ?? 1; - const currentPage = pagination?.current_page ?? 1; - - const visibleColumns = [ - { - id: 'no', - header: () => No, - cell: ({ row }) => ( - - {isServerMode - ? (currentPage - 1) * (pagination?.per_page ?? 25) + - row.index + - 1 - : row.index + 1} - - ), - meta: { - className: 'w-[50px] text-center', - headerClassName: 'w-[50px] text-center', - }, - } as ColumnDef, - ...(isSortable - ? [ + const visibleColumns = [ { - id: 'drag', - header: '', - cell: () => , - meta: { - className: 'w-[40px]', - headerClassName: 'w-[40px]', - }, + id: 'no', + header: () => No, + cell: ({ row }) => ( + + {isServerMode + ? (currentPage - 1) * (pagination?.per_page ?? 25) + + row.index + + 1 + : row.index + 1} + + ), + meta: { + className: 'w-[50px] text-center', + headerClassName: 'w-[50px] text-center', + }, } as ColumnDef, - ] - : []), - ...columns, - ]; + ...(isSortable + ? [ + { + id: 'drag', + header: '', + cell: () => , + meta: { + className: 'w-[40px]', + headerClassName: 'w-[40px]', + }, + } as ColumnDef, + ] + : []), + ...columns, + ]; - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { distance: 8 }, - }), - useSensor(KeyboardSensor), - ); + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 8 }, + }), + useSensor(KeyboardSensor), + ); - const table = useReactTable({ - data, - columns: visibleColumns, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: renderSubRow ? getExpandedRowModel() : undefined, - onExpandedChange: setExpanded, - state: { - expanded, - }, - }); + const table = useReactTable({ + data, + columns: visibleColumns, + getCoreRowModel: getCoreRowModel(), + getExpandedRowModel: renderSubRow ? getExpandedRowModel() : undefined, + onExpandedChange: setExpanded, + state: { + expanded, + }, + }); - const itemIds = React.useMemo( - () => data.map((item) => String(getRowId ? getRowId(item) : '')), - [data, getRowId], - ); + const itemIds = React.useMemo( + () => data.map((item) => String(getRowId ? getRowId(item) : '')), + [data, getRowId], + ); - function handleDragEnd(event: DragEndEvent) { - const { active, over } = event; + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; - if (!over || active.id === over.id) { - return; + if (!over || active.id === over.id) { + return; + } + + const oldIndex = itemIds.indexOf(String(active.id)); + const newIndex = itemIds.indexOf(String(over.id)); + + if (oldIndex === -1 || newIndex === -1) { + return; + } + + const reordered = [...data]; + const [moved] = reordered.splice(oldIndex, 1); + reordered.splice(newIndex, 0, moved); + + onReorder?.(reordered); } - const oldIndex = itemIds.indexOf(String(active.id)); - const newIndex = itemIds.indexOf(String(over.id)); + function handleSearchChange(value: string) { + setLocalSearch(value); - if (oldIndex === -1 || newIndex === -1) { - return; + if (isServerMode) { + handleSearchDebounced(value); + } } - const reordered = [...data]; - const [moved] = reordered.splice(oldIndex, 1); - reordered.splice(newIndex, 0, moved); - - onReorder?.(reordered); - } - - function handleSearchChange(value: string) { - setLocalSearch(value); - - if (isServerMode) { - handleSearchDebounced(value); - } - } - - return ( - <> - {(searchKey || toolbar || isServerMode) && ( -
- {searchKey && ( - - handleSearchChange(event.target.value) - } - className="max-w-sm" - /> - )} - {toolbar} -
- {isServerMode && onPerPageChange && ( - - )} -
-
- )} - - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {(searchKey || toolbar || isServerMode) && ( +
+ {searchKey && ( + + handleSearchChange(event.target.value) } - )?.headerClassName - } - > - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext(), + className="max-w-sm" + /> + )} + {toolbar} +
+ {isServerMode && onPerPageChange && ( + )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - isSortable ? ( - - - {table - .getRowModel() - .rows.map((row) => ( - +
+ )} + + +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + isSortable ? ( + + + {table + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext(), + )} + + ))} + + ))} + + + ) : ( + table.getRowModel().rows.map((row) => ( + + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell.column + .columnDef + .cell, + cell.getContext(), + )} + + ))} + + {renderSubRow && + row.getIsExpanded() && ( + + +
+ {renderSubRow( + row, + localSearch, + )} +
+
+
+ )} +
+ )) + ) + ) : ( + + + {emptyText} + + )} - > - {row - .getVisibleCells() - .map((cell) => ( - - {flexRender( - cell - .column - .columnDef - .cell, - cell.getContext(), - )} - - ))} - - ))} - - - ) : ( - table.getRowModel().rows.map((row) => ( - - - {row - .getVisibleCells() - .map((cell) => ( - - {flexRender( - cell.column - .columnDef - .cell, - cell.getContext(), - )} - - ))} - - {renderSubRow && - row.getIsExpanded() && ( - - -
- {renderSubRow( - row, - localSearch, - )} -
-
-
- )} -
- )) - ) - ) : ( - - - {emptyText} - - - )} -
-
-
-
+ + + + -
- - {isServerMode - ? `Halaman ${currentPage} dari ${totalPages}` - : `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`} - -
- {isServerMode ? ( - <> - - - - - - ) : ( - <> - - - - - - )} -
-
- - ); +
+ + {isServerMode + ? `Halaman ${currentPage} dari ${totalPages}` + : `Halaman ${table.getState().pagination.pageIndex + 1} dari ${table.getPageCount()}`} + +
+ {isServerMode ? ( + <> + + + + + + ) : ( + <> + + + + + + )} +
+
+ + ); } diff --git a/resources/js/components/ui/table.tsx b/resources/js/components/ui/table.tsx index fcfa0a2..668571e 100644 --- a/resources/js/components/ui/table.tsx +++ b/resources/js/components/ui/table.tsx @@ -21,7 +21,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { return ( ) @@ -68,7 +68,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) { [role=checkbox]]:translate-y-[2px]", + "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-primary-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} -- 2.45.2