W Metropolii Quantum, gdzie dane przepływają jak strumienie kwantowej informacji, poznasz TanStack Table - najpotężniejszą bibliotekę do tworzenia zaawansowanych tabel w React. Ta kombinacja z Next.js pozwala budować interaktywne, wydajne interfejsy do zarządzania dużymi zbiorami danych.
TanStack Table (wcześniej React Table) to headless biblioteka do budowania tabel, która oferuje:
1# Zainstaluj TanStack Table
2npm install @tanstack/react-table
3
4# Dla stylowania (opcjonalne)
5npm install @tanstack/table-core1// types/User.ts
2export interface User {
3 id: string;
4 name: string;
5 email: string;
6 role: 'admin' | 'user' | 'moderator';
7 status: 'active' | 'inactive' | 'pending';
8 createdAt: string;
9 lastLogin?: string;
10}1// components/UsersTable.tsx
2'use client';
3
4import {
5 createColumnHelper,
6 flexRender,
7 getCoreRowModel,
8 useReactTable,
9} from '@tanstack/react-table';
10import { User } from '@/types/User';
11
12const columnHelper = createColumnHelper<User>();
13
14const columns = [
15 columnHelper.accessor('name', {
16 header: 'Nazwa użytkownika',
17 cell: (info) => info.getValue(),
18 }),
19 columnHelper.accessor('email', {
20 header: 'Email',
21 cell: (info) => (
22 <a href={`mailto:${info.getValue()}`} className="text-blue-600 hover:underline">
23 {info.getValue()}
24 </a>
25 ),
26 }),
27 columnHelper.accessor('role', {
28 header: 'Rola',
29 cell: (info) => {
30 const role = info.getValue();
31 const roleColors = {
32 admin: 'bg-red-100 text-red-800',
33 moderator: 'bg-yellow-100 text-yellow-800',
34 user: 'bg-green-100 text-green-800',
35 };
36
37 return (
38 <span className={`px-2 py-1 rounded-full text-xs font-medium ${roleColors[role]}`}>
39 {role}
40 </span>
41 );
42 },
43 }),
44 columnHelper.accessor('status', {
45 header: 'Status',
46 cell: (info) => {
47 const status = info.getValue();
48 const statusColors = {
49 active: 'bg-green-100 text-green-800',
50 inactive: 'bg-gray-100 text-gray-800',
51 pending: 'bg-yellow-100 text-yellow-800',
52 };
53
54 return (
55 <span className={`px-2 py-1 rounded-full text-xs font-medium ${statusColors[status]}`}>
56 {status}
57 </span>
58 );
59 },
60 }),
61 columnHelper.accessor('createdAt', {
62 header: 'Data utworzenia',
63 cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
64 }),
65];
66
67interface UsersTableProps {
68 data: User[];
69}
70
71export default function UsersTable({ data }: UsersTableProps) {
72 const table = useReactTable({
73 data,
74 columns,
75 getCoreRowModel: getCoreRowModel(),
76 });
77
78 return (
79 <div className="overflow-x-auto">
80 <table className="min-w-full bg-white border border-gray-200">
81 <thead className="bg-gray-50">
82 {table.getHeaderGroups().map((headerGroup) => (
83 <tr key={headerGroup.id}>
84 {headerGroup.headers.map((header) => (
85 <th
86 key={header.id}
87 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
88 >
89 {header.isPlaceholder
90 ? null
91 : flexRender(header.column.columnDef.header, header.getContext())}
92 </th>
93 ))}
94 </tr>
95 ))}
96 </thead>
97 <tbody className="bg-white divide-y divide-gray-200">
98 {table.getRowModel().rows.map((row) => (
99 <tr key={row.id} className="hover:bg-gray-50">
100 {row.getVisibleCells().map((cell) => (
101 <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
102 {flexRender(cell.column.columnDef.cell, cell.getContext())}
103 </td>
104 ))}
105 </tr>
106 ))}
107 </tbody>
108 </table>
109 </div>
110 );
111}1// components/SortableUsersTable.tsx
2'use client';
3
4import {
5 createColumnHelper,
6 flexRender,
7 getCoreRowModel,
8 getSortedRowModel,
9 SortingState,
10 useReactTable,
11} from '@tanstack/react-table';
12import { useState } from 'react';
13import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
14import { User } from '@/types/User';
15
16const columnHelper = createColumnHelper<User>();
17
18const columns = [
19 columnHelper.accessor('name', {
20 header: 'Nazwa użytkownika',
21 enableSorting: true,
22 }),
23 columnHelper.accessor('email', {
24 header: 'Email',
25 enableSorting: true,
26 }),
27 columnHelper.accessor('role', {
28 header: 'Rola',
29 enableSorting: true,
30 }),
31 columnHelper.accessor('status', {
32 header: 'Status',
33 enableSorting: true,
34 }),
35 columnHelper.accessor('createdAt', {
36 header: 'Data utworzenia',
37 enableSorting: true,
38 sortingFn: 'datetime',
39 cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
40 }),
41];
42
43interface SortableUsersTableProps {
44 data: User[];
45}
46
47export default function SortableUsersTable({ data }: SortableUsersTableProps) {
48 const [sorting, setSorting] = useState<SortingState>([]);
49
50 const table = useReactTable({
51 data,
52 columns,
53 state: {
54 sorting,
55 },
56 onSortingChange: setSorting,
57 getCoreRowModel: getCoreRowModel(),
58 getSortedRowModel: getSortedRowModel(),
59 });
60
61 return (
62 <div className="overflow-x-auto">
63 <table className="min-w-full bg-white border border-gray-200">
64 <thead className="bg-gray-50">
65 {table.getHeaderGroups().map((headerGroup) => (
66 <tr key={headerGroup.id}>
67 {headerGroup.headers.map((header) => (
68 <th
69 key={header.id}
70 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
71 >
72 <div
73 className={
74 header.column.getCanSort()
75 ? 'flex items-center cursor-pointer select-none hover:text-gray-700'
76 : ''
77 }
78 onClick={header.column.getToggleSortingHandler()}
79 >
80 {flexRender(header.column.columnDef.header, header.getContext())}
81 {header.column.getCanSort() && (
82 <span className="ml-1">
83 {header.column.getIsSorted() === 'asc' ? (
84 <ChevronUpIcon className="w-4 h-4" />
85 ) : header.column.getIsSorted() === 'desc' ? (
86 <ChevronDownIcon className="w-4 h-4" />
87 ) : (
88 <div className="w-4 h-4" />
89 )}
90 </span>
91 )}
92 </div>
93 </th>
94 ))}
95 </tr>
96 ))}
97 </thead>
98 <tbody className="bg-white divide-y divide-gray-200">
99 {table.getRowModel().rows.map((row) => (
100 <tr key={row.id} className="hover:bg-gray-50">
101 {row.getVisibleCells().map((cell) => (
102 <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
103 {flexRender(cell.column.columnDef.cell, cell.getContext())}
104 </td>
105 ))}
106 </tr>
107 ))}
108 </tbody>
109 </table>
110 </div>
111 );
112}1// components/FilterableUsersTable.tsx
2'use client';
3
4import {
5 createColumnHelper,
6 flexRender,
7 getCoreRowModel,
8 getFilteredRowModel,
9 getSortedRowModel,
10 SortingState,
11 ColumnFiltersState,
12 useReactTable,
13 GlobalFilterTableState,
14} from '@tanstack/react-table';
15import { useState } from 'react';
16import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
17import { User } from '@/types/User';
18
19// Global filter function
20function globalFilterFn(row: any, columnId: string, value: string) {
21 const search = value.toLowerCase();
22 return (
23 row.getValue('name')?.toLowerCase().includes(search) ||
24 row.getValue('email')?.toLowerCase().includes(search) ||
25 row.getValue('role')?.toLowerCase().includes(search)
26 );
27}
28
29const columnHelper = createColumnHelper<User>();
30
31const columns = [
32 columnHelper.accessor('name', {
33 header: 'Nazwa użytkownika',
34 enableColumnFilter: true,
35 filterFn: 'includesString',
36 }),
37 columnHelper.accessor('email', {
38 header: 'Email',
39 enableColumnFilter: true,
40 }),
41 columnHelper.accessor('role', {
42 header: 'Rola',
43 enableColumnFilter: true,
44 filterFn: 'equals',
45 }),
46 columnHelper.accessor('status', {
47 header: 'Status',
48 enableColumnFilter: true,
49 filterFn: 'equals',
50 }),
51 columnHelper.accessor('createdAt', {
52 header: 'Data utworzenia',
53 enableColumnFilter: false,
54 cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
55 }),
56];
57
58interface FilterableUsersTableProps {
59 data: User[];
60}
61
62export default function FilterableUsersTable({ data }: FilterableUsersTableProps) {
63 const [sorting, setSorting] = useState<SortingState>([]);
64 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
65 const [globalFilter, setGlobalFilter] = useState('');
66
67 const table = useReactTable({
68 data,
69 columns,
70 state: {
71 sorting,
72 columnFilters,
73 globalFilter,
74 },
75 onSortingChange: setSorting,
76 onColumnFiltersChange: setColumnFilters,
77 onGlobalFilterChange: setGlobalFilter,
78 globalFilterFn,
79 getCoreRowModel: getCoreRowModel(),
80 getSortedRowModel: getSortedRowModel(),
81 getFilteredRowModel: getFilteredRowModel(),
82 });
83
84 return (
85 <div className="space-y-4">
86 {/* Global Search */}
87 <div className="flex items-center space-x-4">
88 <div className="relative flex-1 max-w-sm">
89 <MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
90 <input
91 value={globalFilter ?? ''}
92 onChange={(e) => setGlobalFilter(e.target.value)}
93 className="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full"
94 />
95 </div>
96
97 {/* Role Filter */}
98 <select
99 value={(table.getColumn('role')?.getFilterValue() as string) ?? ''}
100 onChange={(e) =>
101 table.getColumn('role')?.setFilterValue(e.target.value || undefined)
102 }
103 className="px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
104 >
105 <option value="">Wszystkie role</option>
106 <option value="admin">Admin</option>
107 <option value="moderator">Moderator</option>
108 <option value="user">User</option>
109 </select>
110
111 {/* Status Filter */}
112 <select
113 value={(table.getColumn('status')?.getFilterValue() as string) ?? ''}
114 onChange={(e) =>
115 table.getColumn('status')?.setFilterValue(e.target.value || undefined)
116 }
117 className="px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
118 >
119 <option value="">Wszystkie statusy</option>
120 <option value="active">Aktywny</option>
121 <option value="inactive">Nieaktywny</option>
122 <option value="pending">Oczekujący</option>
123 </select>
124 </div>
125
126 {/* Results Info */}
127 <div className="text-sm text-gray-600">
128 Wyświetlono {table.getFilteredRowModel().rows.length} z {data.length} użytkowników
129 </div>
130
131 {/* Table */}
132 <div className="overflow-x-auto">
133 <table className="min-w-full bg-white border border-gray-200">
134 <thead className="bg-gray-50">
135 {table.getHeaderGroups().map((headerGroup) => (
136 <tr key={headerGroup.id}>
137 {headerGroup.headers.map((header) => (
138 <th
139 key={header.id}
140 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
141 >
142 <div
143 className={
144 header.column.getCanSort()
145 ? 'flex items-center cursor-pointer select-none hover:text-gray-700'
146 : ''
147 }
148 onClick={header.column.getToggleSortingHandler()}
149 >
150 {flexRender(header.column.columnDef.header, header.getContext())}
151 </div>
152 </th>
153 ))}
154 </tr>
155 ))}
156 </thead>
157 <tbody className="bg-white divide-y divide-gray-200">
158 {table.getFilteredRowModel().rows.map((row) => (
159 <tr key={row.id} className="hover:bg-gray-50">
160 {row.getVisibleCells().map((cell) => (
161 <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
162 {flexRender(cell.column.columnDef.cell, cell.getContext())}
163 </td>
164 ))}
165 </tr>
166 ))}
167 </tbody>
168 </table>
169 </div>
170 </div>
171 );
172}1// components/PaginatedUsersTable.tsx
2'use client';
3
4import {
5 createColumnHelper,
6 flexRender,
7 getCoreRowModel,
8 getPaginationRowModel,
9 getSortedRowModel,
10 getFilteredRowModel,
11 PaginationState,
12 useReactTable,
13} from '@tanstack/react-table';
14import { useState } from 'react';
15import {
16 ChevronLeftIcon,
17 ChevronRightIcon,
18 ChevronDoubleLeftIcon,
19 ChevronDoubleRightIcon
20} from '@heroicons/react/24/outline';
21import { User } from '@/types/User';
22
23const columnHelper = createColumnHelper<User>();
24
25const columns = [
26 columnHelper.accessor('name', {
27 header: 'Nazwa użytkownika',
28 }),
29 columnHelper.accessor('email', {
30 header: 'Email',
31 }),
32 columnHelper.accessor('role', {
33 header: 'Rola',
34 cell: (info) => {
35 const role = info.getValue();
36 return (
37 <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
38 {role}
39 </span>
40 );
41 },
42 }),
43 columnHelper.accessor('status', {
44 header: 'Status',
45 cell: (info) => {
46 const status = info.getValue();
47 const colors = {
48 active: 'bg-green-100 text-green-800',
49 inactive: 'bg-gray-100 text-gray-800',
50 pending: 'bg-yellow-100 text-yellow-800',
51 };
52
53 return (
54 <span className={`px-2 py-1 rounded-full text-xs font-medium ${colors[status]}`}>
55 {status}
56 </span>
57 );
58 },
59 }),
60 columnHelper.accessor('createdAt', {
61 header: 'Data utworzenia',
62 cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
63 }),
64];
65
66interface PaginatedUsersTableProps {
67 data: User[];
68}
69
70export default function PaginatedUsersTable({ data }: PaginatedUsersTableProps) {
71 const [pagination, setPagination] = useState<PaginationState>({
72 pageIndex: 0,
73 pageSize: 10,
74 });
75
76 const table = useReactTable({
77 data,
78 columns,
79 state: {
80 pagination,
81 },
82 onPaginationChange: setPagination,
83 getCoreRowModel: getCoreRowModel(),
84 getSortedRowModel: getSortedRowModel(),
85 getFilteredRowModel: getFilteredRowModel(),
86 getPaginationRowModel: getPaginationRowModel(),
87 });
88
89 return (
90 <div className="space-y-4">
91 {/* Table */}
92 <div className="overflow-x-auto">
93 <table className="min-w-full bg-white border border-gray-200">
94 <thead className="bg-gray-50">
95 {table.getHeaderGroups().map((headerGroup) => (
96 <tr key={headerGroup.id}>
97 {headerGroup.headers.map((header) => (
98 <th
99 key={header.id}
100 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
101 >
102 {flexRender(header.column.columnDef.header, header.getContext())}
103 </th>
104 ))}
105 </tr>
106 ))}
107 </thead>
108 <tbody className="bg-white divide-y divide-gray-200">
109 {table.getRowModel().rows.map((row) => (
110 <tr key={row.id} className="hover:bg-gray-50">
111 {row.getVisibleCells().map((cell) => (
112 <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
113 {flexRender(cell.column.columnDef.cell, cell.getContext())}
114 </td>
115 ))}
116 </tr>
117 ))}
118 </tbody>
119 </table>
120 </div>
121
122 {/* Pagination */}
123 <div className="flex items-center justify-between">
124 <div className="flex items-center space-x-2">
125 <span className="text-sm text-gray-700">
126 Wyświetlono{' '}
127 <span className="font-medium">
128 {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
129 </span>{' '}
130 -{' '}
131 <span className="font-medium">
132 {Math.min(
133 (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
134 table.getFilteredRowModel().rows.length
135 )}
136 </span>{' '}
137 z{' '}
138 <span className="font-medium">{table.getFilteredRowModel().rows.length}</span> wyników
139 </span>
140
141 <select
142 value={table.getState().pagination.pageSize}
143 onChange={(e) => {
144 table.setPageSize(Number(e.target.value));
145 }}
146 className="px-3 py-1 border border-gray-300 rounded text-sm"
147 >
148 {[10, 20, 30, 40, 50].map((pageSize) => (
149 <option key={pageSize} value={pageSize}>
150 {pageSize} na stronę
151 </option>
152 ))}
153 </select>
154 </div>
155
156 <div className="flex items-center space-x-2">
157 <button
158 onClick={() => table.setPageIndex(0)}
159 disabled={!table.getCanPreviousPage()}
160 className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
161 >
162 <ChevronDoubleLeftIcon className="w-4 h-4" />
163 </button>
164
165 <button
166 onClick={() => table.previousPage()}
167 disabled={!table.getCanPreviousPage()}
168 className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
169 >
170 <ChevronLeftIcon className="w-4 h-4" />
171 </button>
172
173 <div className="flex items-center space-x-1">
174 <span className="text-sm text-gray-700">Strona</span>
175 <input
176 type="number"
177 value={table.getState().pagination.pageIndex + 1}
178 onChange={(e) => {
179 const page = e.target.value ? Number(e.target.value) - 1 : 0;
180 table.setPageIndex(page);
181 }}
182 className="w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center"
183 min={1}
184 max={table.getPageCount()}
185 />
186 <span className="text-sm text-gray-700">z {table.getPageCount()}</span>
187 </div>
188
189 <button
190 onClick={() => table.nextPage()}
191 disabled={!table.getCanNextPage()}
192 className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
193 >
194 <ChevronRightIcon className="w-4 h-4" />
195 </button>
196
197 <button
198 onClick={() => table.setPageIndex(table.getPageCount() - 1)}
199 disabled={!table.getCanNextPage()}
200 className="p-2 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50"
201 >
202 <ChevronDoubleRightIcon className="w-4 h-4" />
203 </button>
204 </div>
205 </div>
206 </div>
207 );
208}1// components/SelectableUsersTable.tsx
2'use client';
3
4import {
5 createColumnHelper,
6 flexRender,
7 getCoreRowModel,
8 useReactTable,
9 RowSelectionState,
10} from '@tanstack/react-table';
11import { useState } from 'react';
12import { TrashIcon, UserGroupIcon } from '@heroicons/react/24/outline';
13import { User } from '@/types/User';
14
15const columnHelper = createColumnHelper<User>();
16
17interface SelectableUsersTableProps {
18 data: User[];
19 onDeleteSelected?: (selectedUsers: User[]) => void;
20 onBulkAction?: (action: string, selectedUsers: User[]) => void;
21}
22
23export default function SelectableUsersTable({
24 data,
25 onDeleteSelected,
26 onBulkAction
27}: SelectableUsersTableProps) {
28 const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
29
30 const columns = [
31 // Selection column
32 columnHelper.display({
33 id: 'select',
34 header: ({ table }) => (
35 <input
36 type="checkbox"
37 checked={table.getIsAllRowsSelected()}
38 onChange={table.getToggleAllRowsSelectedHandler()}
39 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
40 />
41 ),
42 cell: ({ row }) => (
43 <input
44 type="checkbox"
45 checked={row.getIsSelected()}
46 onChange={row.getToggleSelectedHandler()}
47 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
48 />
49 ),
50 }),
51 columnHelper.accessor('name', {
52 header: 'Nazwa użytkownika',
53 }),
54 columnHelper.accessor('email', {
55 header: 'Email',
56 }),
57 columnHelper.accessor('role', {
58 header: 'Rola',
59 }),
60 columnHelper.accessor('status', {
61 header: 'Status',
62 }),
63 // Actions column
64 columnHelper.display({
65 id: 'actions',
66 header: 'Akcje',
67 cell: ({ row }) => (
68 <div className="flex space-x-2">
69 <button
70 onClick={() => console.log('Edit user:', row.original)}
71 className="text-blue-600 hover:text-blue-800 text-sm"
72 >
73 Edytuj
74 </button>
75 <button
76 onClick={() => console.log('Delete user:', row.original)}
77 className="text-red-600 hover:text-red-800 text-sm"
78 >
79 Usuń
80 </button>
81 </div>
82 ),
83 }),
84 ];
85
86 const table = useReactTable({
87 data,
88 columns,
89 state: {
90 rowSelection,
91 },
92 onRowSelectionChange: setRowSelection,
93 getCoreRowModel: getCoreRowModel(),
94 enableRowSelection: true,
95 });
96
97 const selectedUsers = table.getFilteredSelectedRowModel().rows.map(row => row.original);
98
99 return (
100 <div className="space-y-4">
101 {/* Bulk Actions */}
102 {Object.keys(rowSelection).length > 0 && (
103 <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
104 <div className="flex items-center justify-between">
105 <div className="flex items-center space-x-2">
106 <UserGroupIcon className="w-5 h-5 text-blue-600" />
107 <span className="text-sm font-medium text-blue-900">
108 Zaznaczono {selectedUsers.length} użytkowników
109 </span>
110 </div>
111
112 <div className="flex space-x-2">
113 <button
114 onClick={() => onBulkAction?.('activate', selectedUsers)}
115 className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700"
116 >
117 Aktywuj
118 </button>
119 <button
120 onClick={() => onBulkAction?.('deactivate', selectedUsers)}
121 className="px-3 py-1 bg-yellow-600 text-white text-sm rounded hover:bg-yellow-700"
122 >
123 Dezaktywuj
124 </button>
125 <button
126 onClick={() => onDeleteSelected?.(selectedUsers)}
127 className="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700 flex items-center space-x-1"
128 >
129 <TrashIcon className="w-4 h-4" />
130 <span>Usuń</span>
131 </button>
132 </div>
133 </div>
134 </div>
135 )}
136
137 {/* Table */}
138 <div className="overflow-x-auto">
139 <table className="min-w-full bg-white border border-gray-200">
140 <thead className="bg-gray-50">
141 {table.getHeaderGroups().map((headerGroup) => (
142 <tr key={headerGroup.id}>
143 {headerGroup.headers.map((header) => (
144 <th
145 key={header.id}
146 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
147 >
148 {flexRender(header.column.columnDef.header, header.getContext())}
149 </th>
150 ))}
151 </tr>
152 ))}
153 </thead>
154 <tbody className="bg-white divide-y divide-gray-200">
155 {table.getRowModel().rows.map((row) => (
156 <tr
157 key={row.id}
158 className={`hover:bg-gray-50 ${row.getIsSelected() ? 'bg-blue-50' : ''}`}
159 >
160 {row.getVisibleCells().map((cell) => (
161 <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
162 {flexRender(cell.column.columnDef.cell, cell.getContext())}
163 </td>
164 ))}
165 </tr>
166 ))}
167 </tbody>
168 </table>
169 </div>
170 </div>
171 );
172}1// hooks/useUsersData.ts
2import { useQuery } from '@tanstack/react-query';
3import { useState } from 'react';
4
5interface UseUsersDataParams {
6 page: number;
7 pageSize: number;
8 sorting?: Array<{ id: string; desc: boolean }>;
9 filters?: Array<{ id: string; value: any }>;
10 globalFilter?: string;
11}
12
13interface UsersResponse {
14 data: User[];
15 totalCount: number;
16 pageCount: number;
17}
18
19export function useUsersData(params: UseUsersDataParams) {
20 return useQuery({
21 queryKey: ['users', params],
22 queryFn: async (): Promise<UsersResponse> => {
23 const searchParams = new URLSearchParams({
24 page: params.page.toString(),
25 pageSize: params.pageSize.toString(),
26 ...(params.globalFilter && { search: params.globalFilter }),
27 ...(params.sorting?.length && {
28 sort: JSON.stringify(params.sorting)
29 }),
30 ...(params.filters?.length && {
31 filters: JSON.stringify(params.filters)
32 }),
33 });
34
35 const response = await fetch(`/api/users?${searchParams}`);
36
37 if (!response.ok) {
38 throw new Error('Failed to fetch users');
39 }
40
41 return response.json();
42 },
43 keepPreviousData: true,
44 });
45}1// components/ServerSideUsersTable.tsx
2'use client';
3
4import {
5 createColumnHelper,
6 flexRender,
7 getCoreRowModel,
8 useReactTable,
9 PaginationState,
10 SortingState,
11 ColumnFiltersState,
12} from '@tanstack/react-table';
13import { useState } from 'react';
14import { useUsersData } from '@/hooks/useUsersData';
15import { User } from '@/types/User';
16
17const columnHelper = createColumnHelper<User>();
18
19const columns = [
20 columnHelper.accessor('name', {
21 header: 'Nazwa użytkownika',
22 enableSorting: true,
23 }),
24 columnHelper.accessor('email', {
25 header: 'Email',
26 enableSorting: true,
27 }),
28 columnHelper.accessor('role', {
29 header: 'Rola',
30 enableSorting: true,
31 }),
32 columnHelper.accessor('status', {
33 header: 'Status',
34 enableSorting: true,
35 }),
36 columnHelper.accessor('createdAt', {
37 header: 'Data utworzenia',
38 enableSorting: true,
39 cell: (info) => new Date(info.getValue()).toLocaleDateString('pl-PL'),
40 }),
41];
42
43export default function ServerSideUsersTable() {
44 const [pagination, setPagination] = useState<PaginationState>({
45 pageIndex: 0,
46 pageSize: 10,
47 });
48 const [sorting, setSorting] = useState<SortingState>([]);
49 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
50 const [globalFilter, setGlobalFilter] = useState('');
51
52 const { data, isLoading, error } = useUsersData({
53 page: pagination.pageIndex,
54 pageSize: pagination.pageSize,
55 sorting,
56 filters: columnFilters,
57 globalFilter,
58 });
59
60 const table = useReactTable({
61 data: data?.data ?? [],
62 columns,
63 pageCount: data?.pageCount ?? -1,
64 state: {
65 pagination,
66 sorting,
67 columnFilters,
68 globalFilter,
69 },
70 onPaginationChange: setPagination,
71 onSortingChange: setSorting,
72 onColumnFiltersChange: setColumnFilters,
73 onGlobalFilterChange: setGlobalFilter,
74 getCoreRowModel: getCoreRowModel(),
75 manualPagination: true,
76 manualSorting: true,
77 manualFiltering: true,
78 });
79
80 if (isLoading) {
81 return (
82 <div className="flex items-center justify-center h-64">
83 <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
84 </div>
85 );
86 }
87
88 if (error) {
89 return (
90 <div className="text-center text-red-600 p-8">
91 Wystąpił błąd podczas ładowania danych
92 </div>
93 );
94 }
95
96 return (
97 <div className="space-y-4">
98 {/* Search */}
99 <div className="flex items-center space-x-4">
100 <input
101 value={globalFilter ?? ''}
102 onChange={(e) => setGlobalFilter(e.target.value)}
103 className="px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 flex-1 max-w-sm"
104 />
105 </div>
106
107 {/* Table */}
108 <div className="overflow-x-auto">
109 <table className="min-w-full bg-white border border-gray-200">
110 <thead className="bg-gray-50">
111 {table.getHeaderGroups().map((headerGroup) => (
112 <tr key={headerGroup.id}>
113 {headerGroup.headers.map((header) => (
114 <th
115 key={header.id}
116 className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
117 >
118 <div
119 className={
120 header.column.getCanSort()
121 ? 'flex items-center cursor-pointer select-none hover:text-gray-700'
122 : ''
123 }
124 onClick={header.column.getToggleSortingHandler()}
125 >
126 {flexRender(header.column.columnDef.header, header.getContext())}
127 </div>
128 </th>
129 ))}
130 </tr>
131 ))}
132 </thead>
133 <tbody className="bg-white divide-y divide-gray-200">
134 {table.getRowModel().rows.map((row) => (
135 <tr key={row.id} className="hover:bg-gray-50">
136 {row.getVisibleCells().map((cell) => (
137 <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
138 {flexRender(cell.column.columnDef.cell, cell.getContext())}
139 </td>
140 ))}
141 </tr>
142 ))}
143 </tbody>
144 </table>
145 </div>
146
147 {/* Pagination */}
148 <div className="flex items-center justify-between">
149 <div className="text-sm text-gray-700">
150 Wyświetlono {data?.data.length ?? 0} z {data?.totalCount ?? 0} użytkowników
151 </div>
152
153 <div className="flex items-center space-x-2">
154 <button
155 onClick={() => table.setPageIndex(0)}
156 disabled={!table.getCanPreviousPage()}
157 className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
158 >
159 Pierwsza
160 </button>
161
162 <button
163 onClick={() => table.previousPage()}
164 disabled={!table.getCanPreviousPage()}
165 className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
166 >
167 Poprzednia
168 </button>
169
170 <span className="text-sm">
171 Strona {table.getState().pagination.pageIndex + 1} z{' '}
172 {table.getPageCount()}
173 </span>
174
175 <button
176 onClick={() => table.nextPage()}
177 disabled={!table.getCanNextPage()}
178 className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
179 >
180 Następna
181 </button>
182
183 <button
184 onClick={() => table.setPageIndex(table.getPageCount() - 1)}
185 disabled={!table.getCanNextPage()}
186 className="px-3 py-1 border border-gray-300 rounded disabled:opacity-50"
187 >
188 Ostatnia
189 </button>
190 </div>
191 </div>
192 </div>
193 );
194}1// app/api/users/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { User } from '@/types/User';
4
5// Symulowane dane
6const mockUsers: User[] = [
7 // ... dane użytkowników
8];
9
10export async function GET(request: NextRequest) {
11 const searchParams = request.nextUrl.searchParams;
12
13 const page = parseInt(searchParams.get('page') || '0');
14 const pageSize = parseInt(searchParams.get('pageSize') || '10');
15 const search = searchParams.get('search') || '';
16 const sortParam = searchParams.get('sort');
17 const filtersParam = searchParams.get('filters');
18
19 let filteredUsers = [...mockUsers];
20
21 // Global search
22 if (search) {
23 filteredUsers = filteredUsers.filter(user =>
24 user.name.toLowerCase().includes(search.toLowerCase()) ||
25 user.email.toLowerCase().includes(search.toLowerCase()) ||
26 user.role.toLowerCase().includes(search.toLowerCase())
27 );
28 }
29
30 // Column filters
31 if (filtersParam) {
32 const filters = JSON.parse(filtersParam);
33 filters.forEach((filter: { id: string; value: any }) => {
34 filteredUsers = filteredUsers.filter(user => {
35 const value = user[filter.id as keyof User];
36 return value === filter.value;
37 });
38 });
39 }
40
41 // Sorting
42 if (sortParam) {
43 const sorting = JSON.parse(sortParam);
44 sorting.forEach((sort: { id: string; desc: boolean }) => {
45 filteredUsers.sort((a, b) => {
46 const aVal = a[sort.id as keyof User];
47 const bVal = b[sort.id as keyof User];
48
49 if (aVal < bVal) return sort.desc ? 1 : -1;
50 if (aVal > bVal) return sort.desc ? -1 : 1;
51 return 0;
52 });
53 });
54 }
55
56 // Pagination
57 const totalCount = filteredUsers.length;
58 const pageCount = Math.ceil(totalCount / pageSize);
59 const startIndex = page * pageSize;
60 const endIndex = startIndex + pageSize;
61 const paginatedUsers = filteredUsers.slice(startIndex, endIndex);
62
63 return NextResponse.json({
64 data: paginatedUsers,
65 totalCount,
66 pageCount,
67 });
68}1// components/ResponsiveUsersTable.tsx
2'use client';
3
4import { useState } from 'react';
5import { User } from '@/types/User';
6
7interface ResponsiveUsersTableProps {
8 data: User[];
9}
10
11export default function ResponsiveUsersTable({ data }: ResponsiveUsersTableProps) {
12 const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
13
14 const toggleRow = (userId: string) => {
15 const newExpanded = new Set(expandedRows);
16 if (newExpanded.has(userId)) {
17 newExpanded.delete(userId);
18 } else {
19 newExpanded.add(userId);
20 }
21 setExpandedRows(newExpanded);
22 };
23
24 return (
25 <div className="w-full">
26 {/* Desktop Table */}
27 <div className="hidden md:block overflow-x-auto">
28 <table className="min-w-full bg-white border border-gray-200">
29 {/* Standardowa tabela dla desktop */}
30 </table>
31 </div>
32
33 {/* Mobile Cards */}
34 <div className="md:hidden space-y-4">
35 {data.map((user) => (
36 <div key={user.id} className="bg-white border border-gray-200 rounded-lg shadow">
37 <div className="p-4">
38 <div className="flex items-center justify-between">
39 <div className="flex-1">
40 <h3 className="font-medium text-gray-900">{user.name}</h3>
41 <p className="text-sm text-gray-600">{user.email}</p>
42 </div>
43
44 <button
45 onClick={() => toggleRow(user.id)}
46 className="ml-4 text-blue-600 text-sm"
47 >
48 {expandedRows.has(user.id) ? 'Mniej' : 'Więcej'}
49 </button>
50 </div>
51
52 {expandedRows.has(user.id) && (
53 <div className="mt-4 pt-4 border-t border-gray-200">
54 <div className="grid grid-cols-2 gap-4 text-sm">
55 <div>
56 <span className="font-medium text-gray-700">Rola:</span>
57 <span className="ml-1">{user.role}</span>
58 </div>
59 <div>
60 <span className="font-medium text-gray-700">Status:</span>
61 <span className="ml-1">{user.status}</span>
62 </div>
63 <div className="col-span-2">
64 <span className="font-medium text-gray-700">Utworzono:</span>
65 <span className="ml-1">
66 {new Date(user.createdAt).toLocaleDateString('pl-PL')}
67 </span>
68 </div>
69 </div>
70 </div>
71 )}
72 </div>
73 </div>
74 ))}
75 </div>
76 </div>
77 );
78}1// hooks/useTableExport.ts
2import { User } from '@/types/User';
3
4export function useTableExport() {
5 const exportToCSV = (data: User[], filename = 'users.csv') => {
6 const headers = ['ID', 'Nazwa', 'Email', 'Rola', 'Status', 'Data utworzenia'];
7 const csvContent = [
8 headers.join(','),
9 ...data.map(user => [
10 user.id,
11 user.name,
12 user.email,
13 user.role,
14 user.status,
15 new Date(user.createdAt).toLocaleDateString('pl-PL')
16 ].join(','))
17 ].join('
18');
19
20 const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
21 const link = document.createElement('a');
22 const url = URL.createObjectURL(blob);
23 link.setAttribute('href', url);
24 link.setAttribute('download', filename);
25 link.style.visibility = 'hidden';
26 document.body.appendChild(link);
27 link.click();
28 document.body.removeChild(link);
29 };
30
31 const exportToJSON = (data: User[], filename = 'users.json') => {
32 const jsonContent = JSON.stringify(data, null, 2);
33 const blob = new Blob([jsonContent], { type: 'application/json' });
34 const link = document.createElement('a');
35 const url = URL.createObjectURL(blob);
36 link.setAttribute('href', url);
37 link.setAttribute('download', filename);
38 link.style.visibility = 'hidden';
39 document.body.appendChild(link);
40 link.click();
41 document.body.removeChild(link);
42 };
43
44 return { exportToCSV, exportToJSON };
45}1// ✅ Dobrze - używaj memo dla kolumn
2const columns = useMemo(() => [
3 // definicje kolumn
4], []);
5
6// ✅ Dobrze - memo dla data
7const data = useMemo(() => users, [users]);1// ✅ Dobrze - aria labels
2<table role="table" aria-label="Tabela użytkowników">
3 <thead>
4 <tr role="row">
5 <th role="columnheader" aria-sort="ascending">
6 Nazwa
7 </th>
8 </tr>
9 </thead>
10</table>1// ✅ Dobrze - silne typowanie
2const columnHelper = createColumnHelper<User>();
3
4const columns = [
5 columnHelper.accessor('name', {
6 // Pełne wsparcie IntelliSense
7 }),
8];TanStack Table z Next.js tworzy potężną kombinację do budowania zaawansowanych tabel:
TanStack Table to najlepsza biblioteka do tworzenia zaawansowanych, interaktywnych tabel w React aplikacjach.