[BUG] Why when I change the sort direction, the pagination is affected too.
When I was in a page 4 (for example), when I change the sort order, the pagination back to page 1.
@jbetancur any updates regarding this issue?
I was facing this same issue from past 2 days and it been really annoying to tackle it. I tried a lot of things but whenever I try to sort on any next page, it sets the page back to 1.
Finally I solved this issue by creating a custom pagination component and handling the whole pagination and sorting myself so that I don't have to provide onChangePage to the library component for it to exploit the function when I try to sort.
Here is the code of my full page using this table and custom pagination logic and component:
import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';
import { motion } from 'framer-motion';
import DataTable from 'react-data-table-component';
import { Plus, Edit, Trash, MapPin, Users, Calendar, ArrowDownNarrowWide, Dices, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import useVenues from '../../../hooks/useVenues';
import useUser from '../../../hooks/useUser';
import EditVenueDialog from '../../../components/venues/EditVenueDialog';
// Custom Pagination Component
const CustomPagination = ({
rowsPerPage,
rowCount,
currentPage,
onChangePage,
onChangeRowsPerPage,
paginationRowsPerPageOptions = [10, 20, 50]
}) => {
const totalPages = Math.ceil(rowCount / rowsPerPage);
const startRow = ((currentPage - 1) * rowsPerPage) + 1;
const endRow = Math.min(currentPage * rowsPerPage, rowCount);
const handlePageChange = (page) => {
if (page >= 1 && page <= totalPages && page !== currentPage) {
onChangePage(page);
}
};
const handlePerPageChange = (e) => {
const newPerPage = Number(e.target.value);
onChangeRowsPerPage(newPerPage);
};
return (
<div className="flex flex-col sm:flex-row items-center justify-between px-4 py-3 border-t">
<div className="flex items-center space-x-2 mb-2 sm:mb-0">
<span className="text-sm text-gray-700">Rows per page:</span>
<select
value={rowsPerPage}
onChange={handlePerPageChange}
className="px-2 py-1 text-sm border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{paginationRowsPerPageOptions.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-700 mr-2">
{startRow}-{endRow} of {rowCount}
</span>
<div className="flex items-center space-x-1">
<button
onClick={() => handlePageChange(1)}
disabled={currentPage === 1}
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="First page"
>
<ChevronsLeft className="w-4 h-4" />
</button>
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="px-2 py-1 text-sm">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page"
>
<ChevronRight className="w-4 h-4" />
</button>
<button
onClick={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
className="p-1 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page"
>
<ChevronsRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
};
export default function Venues() {
const { venues, pagination, fetchVenues, deleteVenue, bulkDeleteVenues, updateVenue, loading } = useVenues();
const { isLoggedIn } = useUser();
const [selectedRows, setSelectedRows] = useState([]);
const router = useRouter();
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [currentVenue, setCurrentVenue] = useState(null);
const [isMobile, setIsMobile] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [sortColumn, setSortColumn] = useState(null);
const [sortDirection, setSortDirection] = useState('asc');
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
if (isLoggedIn && !isLoggedIn()) {
router.push('/auth/login');
return;
}
if (fetchVenues) fetchVenues(true, currentPage, perPage);
}, [isLoggedIn, currentPage, perPage]);
const handleDelete = async (venueId) => {
if (!confirm('Are you sure you want to delete this venue?')) return;
await deleteVenue(venueId);
await fetchVenues(true, currentPage, perPage);
};
const handleBulkDelete = async () => {
const venueIds = selectedRows.map(row => row._id);
if (!confirm(`Delete ${venueIds.length} venue${venueIds.length > 1 ? 's' : ''}?`)) return;
await bulkDeleteVenues(venueIds);
setSelectedRows([]);
await fetchVenues(true, currentPage, perPage);
};
const handleEdit = (venue) => {
setCurrentVenue(venue);
setEditDialogOpen(true);
};
const handleSaveVenue = async (values) => {
if (!currentVenue) return;
await updateVenue(currentVenue._id, values);
setEditDialogOpen(false);
await fetchVenues(true, currentPage, perPage);
};
const handleSort = (column, direction) => {
setSortColumn(column.selector);
setSortDirection(direction);
};
const sortedData = useMemo(() => {
if (!sortColumn || !venues) return venues;
return [...venues].sort((a, b) => {
const aField = sortColumn(a);
const bField = sortColumn(b);
let comparison = 0;
if (aField === null || aField === undefined) return 1;
if (bField === null || bField === undefined) return -1;
if (aField > bField) {
comparison = 1;
} else if (aField < bField) {
comparison = -1;
}
return sortDirection === 'desc' ? comparison * -1 : comparison;
});
}, [venues, sortColumn, sortDirection]);
const handlePageChange = (page) => {
setCurrentPage(page);
};
const handlePerRowsChange = (newPerPage) => {
setPerPage(newPerPage);
setCurrentPage(1);
};
// Desktop columns
const desktopColumns = [
{
name: 'Name',
selector: row => row?.name,
sortable: true,
cell: row => (
<div className="py-2">
<div className="font-medium text-gray-900">{row.name}</div>
</div>
),
width: '10%'
},
{
name: 'Address',
selector: row => row?.address,
sortable: true,
cell: row => (
<div className="py-2">
<div className="flex items-start text-gray-600">
<MapPin className="w-4 h-4 mr-1 mt-0.5 flex-shrink-0" />
<span className="text-sm">
{row.address ?
`${row.address.street}, ${row.address.city}, ${row.address.state} ${row.address.zipCode}` :
'No address'
}
</span>
</div>
</div>
),
width: '15%'
},
{
name: 'Leagues',
selector: row => row?.leagues?.length || 0,
sortable: true,
cell: row => (
<div className="flex items-center text-gray-600">
<Calendar className="w-4 h-4 mr-1" />
<span>{row.leagues?.length || 0}</span>
</div>
),
width: '10%'
},
{
name: 'Status',
selector: row => row?.isActive,
sortable: true,
cell: row => (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${row.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{row.isActive ? 'Active' : 'Inactive'}
</span>
),
width: '10%'
},
{
name: 'Arachnid Boards',
selector: row => row?.arachnidBoardsCount || 0,
sortable: true,
cell: row => (
<div className="flex items-center text-gray-600">
<Dices className="w-4 h-4 mr-1" />
<span>{row.arachnidBoardsCount || 0}</span>
</div>
),
width: '10%'
},
{
name: 'Phoenix Boards',
selector: row => row?.phoenixBoardsCount || 0,
sortable: true,
cell: row => (
<div className="flex items-center text-gray-600">
<Dices className="w-4 h-4 mr-1" />
<span>{row.phoenixBoardsCount || 0}</span>
</div>
),
width: '10%'
},
{
name: 'Diamond Tables',
selector: row => row?.diamondTablesCount || 0,
sortable: true,
cell: row => (
<div className="flex items-center text-gray-600">
<Dices className="w-4 h-4 mr-1" />
<span>{row.diamondTablesCount || 0}</span>
</div>
),
width: '10%'
},
{
name: 'Valley Tables',
selector: row => row?.valleyTablesCount || 0,
sortable: true,
cell: row => (
<div className="flex items-center text-gray-600">
<Dices className="w-4 h-4 mr-1" />
<span>{row.valleyTablesCount || 0}</span>
</div>
),
width: '10%'
},
{
name: 'Actions',
cell: row => (
<div className="flex items-center space-x-2">
<button
onClick={() => handleEdit(row)}
className="p-1 text-blue-600 hover:text-blue-800 transition-colors"
title="Edit venue"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(row._id)}
className="p-1 text-red-600 hover:text-red-800 transition-colors"
title="Delete venue"
>
<Trash className="w-4 h-4" />
</button>
</div>
),
ignoreRowClick: true,
allowOverflow: true,
button: true,
width: '10%'
},
];
// Mobile columns
const mobileColumns = [
{
name: 'Venue Details',
cell: row => (
<div className="py-3 w-full">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900 text-sm mb-1 truncate">{row.name}</div>
<div className="flex items-center text-gray-600 mb-1">
<MapPin className="w-3 h-3 mr-1 flex-shrink-0" />
<span className="text-xs truncate">
{row.address ?
`${row.address.city}, ${row.address.state}` :
'No address'
}
</span>
</div>
<div className="flex items-center space-x-3 text-xs text-gray-500">
<div className="flex items-center">
<Users className="w-3 h-3 mr-1" />
<span>{row.contacts?.length || 0}</span>
</div>
<div className="flex items-center">
<Calendar className="w-3 h-3 mr-1" />
<span>{row.leagues?.length || 0}</span>
</div>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${row.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{row.isActive ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div className="flex items-center space-x-1 ml-2">
<button
onClick={() => handleEdit(row)}
className="p-2 text-blue-600 hover:text-blue-800 transition-colors"
title="Edit venue"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(row._id)}
className="p-2 text-red-600 hover:text-red-800 transition-colors"
title="Delete venue"
>
<Trash className="w-4 h-4" />
</button>
</div>
</div>
</div>
),
ignoreRowClick: true,
allowOverflow: true,
button: true,
}
];
const columns = isMobile ? mobileColumns : desktopColumns;
return (
<div className="w-full flex flex-col space-y-4 md:space-y-6 px-4 sm:px-0">
{/* Header */}
<div className="flex flex-col space-y-3 sm:space-y-0 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 flex-1">
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-gray-900 truncate">Venues</h1>
<p className="text-gray-600 mt-1 text-sm sm:text-base">Manage venue locations and information</p>
</div>
<div className="flex-shrink-0">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => router.push('/portal/venues/new')}
className="w-full sm:w-auto flex items-center justify-center space-x-2 bg-gradient-to-r from-red-500 to-orange-500 text-white px-4 py-2.5 rounded-xl hover:from-red-600 hover:to-orange-600 transition-all duration-200 shadow-lg text-sm font-medium"
>
<Plus className="w-4 h-4" />
<span>Add Venue</span>
</motion.button>
</div>
</div>
{/* Data Table */}
<div className="bg-white border rounded-lg md:rounded-xl shadow-lg scrollbar_x -mx-4 sm:mx-0 overflow-hidden">
<DataTable
className='scrollbar_x'
columns={columns}
data={sortedData || []}
progressPending={loading}
pagination={false}
onSort={handleSort}
sortServer={false}
sortIcon={<ArrowDownNarrowWide className='ml-1 !text-slate-600' />}
selectableRows
onSelectedRowsChange={({ selectedRows }) => setSelectedRows(selectedRows)}
responsive={true}
dense={isMobile}
customStyles={{
table: {
style: {
minWidth: isMobile ? '100%' : '150%',
}
},
tableWrapper: {
style: {
paddingBottom: selectedRows.length > 0 ? "8rem" : "2rem",
minHeight: '400px',
}
},
expanderCell: {
style: {
borderRadius: "0.75rem"
}
},
head: {
style: {
fontSize: isMobile ? '12px' : '13px',
fontWeight: "600",
paddingLeft: '16px',
paddingRight: '16px'
}
},
rows: {
style: {
minHeight: isMobile ? "auto" : "60px",
borderRadius: "14px",
paddingLeft: '16px',
paddingRight: '16px',
fontSize: isMobile ? '12px' : '14px'
}
},
cells: {
style: {
paddingLeft: isMobile ? '8px' : '16px',
paddingRight: isMobile ? '8px' : '16px',
}
}
}}
noDataComponent={
<div className="py-8 md:py-12 text-center px-4">
<div className="text-gray-400 mb-4">
<MapPin className="w-8 h-8 md:w-12 md:h-12 mx-auto" />
</div>
<p className="text-gray-600 text-base md:text-lg font-medium">No venues found</p>
<p className="text-gray-500 mt-1 text-sm md:text-base">Get started by adding your first venue</p>
<button
onClick={() => router.push('/portal/venues/new')}
className="mt-4 bg-gradient-to-r from-red-500 to-orange-500 text-white px-4 md:px-6 py-2 rounded-xl hover:from-red-600 hover:to-orange-600 transition-all duration-200 text-sm md:text-base"
>
Add First Venue
</button>
</div>
}
/>
<CustomPagination
rowsPerPage={perPage}
rowCount={pagination?.total_items || 0}
currentPage={currentPage}
onChangePage={handlePageChange}
onChangeRowsPerPage={handlePerRowsChange}
paginationRowsPerPageOptions={isMobile ? [5, 10, 20] : [10, 20, 50]}
/>
</div>
<EditVenueDialog
isOpen={editDialogOpen}
onClose={() => setEditDialogOpen(false)}
venue={currentVenue}
onSave={handleSaveVenue}
loading={loading}
/>
{selectedRows.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="fixed bottom-4 left-4 right-4 sm:left-1/2 sm:right-auto sm:transform sm:-translate-x-1/2 sm:w-auto bg-white rounded-xl shadow-2xl border p-3 md:p-4 z-50"
>
<div className="flex items-center justify-between sm:justify-center sm:space-x-4">
<span className="text-xs md:text-sm font-medium text-gray-700 flex-1 sm:flex-none">
{selectedRows.length} venue{selectedRows.length > 1 ? 's' : ''} selected
</span>
<button
onClick={handleBulkDelete}
className="flex items-center space-x-1 text-red-600 hover:text-red-800 transition-colors px-2 py-1 rounded hover:bg-red-50"
>
<Trash className="w-4 h-4" />
<span className="text-xs md:text-sm">Delete</span>
</button>
</div>
</motion.div>
)}
</div>
);
}