react-data-table-component icon indicating copy to clipboard operation
react-data-table-component copied to clipboard

[BUG] Why when I change the sort direction, the pagination is affected too.

Open emanuelvictor opened this issue 1 year ago • 2 comments

When I was in a page 4 (for example), when I change the sort order, the pagination back to page 1.

emanuelvictor avatar Oct 18 '24 01:10 emanuelvictor

@jbetancur any updates regarding this issue?

andrewhamili avatar Mar 03 '25 03:03 andrewhamili

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>
  );
}

saad-devx avatar Oct 07 '25 17:10 saad-devx