πŸ”„ Create Reusable CustomPagination Component

🎯 Component Architecture:
- Created standalone CustomPagination component in src/components/
- Fully reusable across Product and Project Tracker
- Props-based configuration for maximum flexibility
- Built-in theme detection and switching

🎨 Feature-Rich Implementation:
- Compact and full-size modes (compact prop)
- Configurable page size options
- Show/hide info and page size selector
- Custom class names for styling
- Loading states and disabled handling

πŸ“‹ Props Interface:
- currentPage, pageSize, totalCount, totalPages
- loading, onPageChange, onPageSizeChange
- pageSizeOptions, showInfo, showPageSizeSelector
- compact, className for customization

πŸŒ“ Theme Integration:
- Automatic dark/light mode detection
- Redux state, localStorage, and document attribute fallbacks
- Real-time theme switching with MutationObserver
- Force re-render mechanism for theme changes

πŸ”§ Technical Features:
- Built-in theme change listeners
- Proper event cleanup
- Dynamic styling based on theme and compact mode
- Hover effects and animations
- Professional button styling

πŸš€ Usage Examples:
- Project Tracker: compact={true} for smaller size
- Product List: compact={false} for full size
- Consistent API across all components
- Easy to maintain and extend

βœ… Code Quality:
- Clean component separation
- No code duplication
- Proper prop validation
- Consistent styling patterns
- Reusable across entire application
This commit is contained in:
tuan.cna 2025-05-30 17:44:47 +07:00
parent 4784fc8bf5
commit be9da50fc5
3 changed files with 304 additions and 411 deletions

View File

@ -0,0 +1,271 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
const CustomPagination = ({
currentPage = 1,
pageSize = 10,
totalCount = 0,
totalPages = 1,
loading = false,
onPageChange,
onPageSizeChange,
pageSizeOptions = [10, 20, 50, 100],
showInfo = true,
showPageSizeSelector = true,
compact = false,
className = ''
}) => {
// Theme state for force re-render
const [themeKey, setThemeKey] = useState(0);
// Get theme from Redux and localStorage fallback
const reduxTheme = useSelector((state) => state.theme?.isDarkMode);
const localStorageTheme = localStorage.getItem('colorschema') === 'dark_mode';
const documentTheme = document.documentElement.getAttribute('data-layout-mode') === 'dark_mode';
const isDarkMode = reduxTheme || localStorageTheme || documentTheme;
// Listen for theme changes
useEffect(() => {
const handleThemeChange = () => {
setThemeKey(prev => prev + 1);
};
// Listen for data-layout-mode attribute changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-layout-mode') {
handleThemeChange();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-layout-mode']
});
// Also listen for localStorage changes
const handleStorageChange = (e) => {
if (e.key === 'colorschema') {
handleThemeChange();
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
observer.disconnect();
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// Calculate pagination info
const startRecord = totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const endRecord = Math.min(currentPage * pageSize, totalCount);
// Handle page change
const handlePageClick = (page) => {
if (!loading && page !== currentPage && onPageChange) {
onPageChange(page);
}
};
// Handle page size change
const handlePageSizeClick = (newPageSize) => {
if (!loading && newPageSize !== pageSize && onPageSizeChange) {
onPageSizeChange(newPageSize);
}
};
// Container styles based on compact mode
const containerStyles = {
background: isDarkMode
? 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)'
: 'linear-gradient(135deg, #ffffff, #f8f9fa)',
border: isDarkMode
? '1px solid rgba(52, 152, 219, 0.3)'
: '1px solid rgba(0, 0, 0, 0.1)',
borderRadius: compact ? '8px' : '12px',
boxShadow: isDarkMode
? (compact ? '0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(52, 152, 219, 0.1)' : '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)')
: (compact ? '0 1px 6px rgba(0, 0, 0, 0.06)' : '0 2px 12px rgba(0, 0, 0, 0.08)'),
backdropFilter: isDarkMode ? (compact ? 'blur(8px)' : 'blur(10px)') : 'none',
transition: compact ? 'all 0.2s ease' : 'all 0.3s ease',
position: 'relative',
overflow: 'hidden',
padding: compact ? '8px 16px' : '16px 24px',
margin: compact ? '8px 0' : '16px 0',
fontSize: compact ? '13px' : '14px'
};
// Button styles
const getButtonStyles = (isActive) => ({
background: loading
? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)')
: isActive
? (isDarkMode ? 'linear-gradient(45deg, #f39c12, #e67e22)' : 'linear-gradient(135deg, #007bff, #0056b3)')
: (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'),
border: isActive
? (isDarkMode ? (compact ? '1px solid #f39c12' : '2px solid #f39c12') : (compact ? '1px solid #007bff' : '2px solid #007bff'))
: (isDarkMode ? '1px solid rgba(52, 152, 219, 0.3)' : '1px solid #dee2e6'),
borderRadius: '50%',
width: compact ? '24px' : '32px',
height: compact ? '24px' : '32px',
color: isActive
? '#ffffff'
: (isDarkMode ? '#ffffff' : '#495057'),
fontSize: compact ? '11px' : '14px',
fontWeight: compact ? '600' : '700',
cursor: loading ? 'not-allowed' : 'pointer',
transition: compact ? 'all 0.2s ease' : 'all 0.3s ease',
boxShadow: loading
? 'none'
: isActive
? (isDarkMode ? (compact ? '0 2px 6px rgba(243, 156, 18, 0.3)' : '0 4px 12px rgba(243, 156, 18, 0.4)') : (compact ? '0 2px 4px rgba(0, 123, 255, 0.2)' : '0 3px 8px rgba(0, 123, 255, 0.3)'))
: (isDarkMode ? (compact ? '0 1px 4px rgba(52, 73, 94, 0.2)' : '0 2px 8px rgba(52, 73, 94, 0.3)') : (compact ? '0 1px 2px rgba(0, 0, 0, 0.08)' : '0 1px 3px rgba(0, 0, 0, 0.1)')),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: loading ? 0.6 : 1
});
return (
<div
key={`pagination-${themeKey}`}
className={`custom-pagination-container ${isDarkMode ? '' : 'light-mode'} ${className}`}
style={containerStyles}
>
{/* Pagination Info */}
{showInfo && (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: compact ? '8px' : '16px',
flexWrap: 'wrap',
gap: compact ? '8px' : '12px'
}}
>
{showPageSizeSelector && (
<div style={{display: 'flex', alignItems: 'center', gap: compact ? '6px' : '12px'}}>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>Row Per Page</span>
<select
value={pageSize}
onChange={(e) => handlePageSizeClick(parseInt(e.target.value))}
disabled={loading}
style={{
background: loading
? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)')
: (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'),
border: isDarkMode
? '1px solid rgba(52, 152, 219, 0.3)'
: '1px solid #dee2e6',
borderRadius: compact ? '4px' : '6px',
color: isDarkMode ? '#ffffff' : '#495057',
padding: compact ? '2px 6px' : '4px 8px',
fontSize: compact ? '12px' : '14px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1,
boxShadow: isDarkMode ? 'none' : (compact ? '0 1px 2px rgba(0, 0, 0, 0.05)' : '0 1px 3px rgba(0, 0, 0, 0.1)')
}}
>
{pageSizeOptions.map(option => (
<option
key={option}
value={option}
style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}}
>
{option}
</option>
))}
</select>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>Entries</span>
</div>
)}
<div style={{display: 'flex', alignItems: 'center', gap: compact ? '6px' : '12px'}}>
<div
style={{
background: isDarkMode
? 'linear-gradient(45deg, #3498db, #2ecc71)'
: 'linear-gradient(45deg, #007bff, #28a745)',
borderRadius: '50%',
width: compact ? '16px' : '24px',
height: compact ? '16px' : '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: compact ? '8px' : '12px',
boxShadow: isDarkMode
? (compact ? '0 1px 4px rgba(52, 152, 219, 0.3)' : '0 2px 8px rgba(52, 152, 219, 0.3)')
: (compact ? '0 1px 4px rgba(0, 123, 255, 0.2)' : '0 2px 8px rgba(0, 123, 255, 0.2)'),
transition: compact ? 'all 0.2s ease' : 'all 0.3s ease'
}}
>
πŸ“Š
</div>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>
Showing <strong style={{color: isDarkMode ? '#3498db' : '#007bff'}}>{startRecord}</strong> to <strong style={{color: isDarkMode ? '#3498db' : '#007bff'}}>{endRecord}</strong> of <strong style={{color: isDarkMode ? '#e74c3c' : '#dc3545'}}>{totalCount}</strong> entries
</span>
</div>
</div>
)}
{/* Pagination Buttons */}
<div
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: compact ? '4px' : '8px'
}}
>
{Array.from({ length: totalPages }, (_, i) => {
const pageNum = i + 1;
const isActive = currentPage === pageNum;
return (
<button
key={pageNum}
onClick={() => handlePageClick(pageNum)}
disabled={loading}
style={getButtonStyles(isActive)}
onMouseEnter={(e) => {
if (!loading && !isActive) {
e.target.style.background = isDarkMode
? 'linear-gradient(45deg, #3498db, #2980b9)'
: 'linear-gradient(135deg, #e9ecef, #f8f9fa)';
e.target.style.transform = isDarkMode ? (compact ? 'scale(1.05)' : 'scale(1.1)') : (compact ? 'translateY(-1px) scale(1.02)' : 'translateY(-1px) scale(1.05)');
e.target.style.boxShadow = isDarkMode
? (compact ? '0 2px 6px rgba(52, 152, 219, 0.3)' : '0 4px 12px rgba(52, 152, 219, 0.4)')
: (compact ? '0 2px 4px rgba(0, 0, 0, 0.12)' : '0 3px 8px rgba(0, 0, 0, 0.15)');
e.target.style.borderColor = isDarkMode ? '#3498db' : '#adb5bd';
}
}}
onMouseLeave={(e) => {
if (!loading && !isActive) {
e.target.style.background = isDarkMode
? 'linear-gradient(45deg, #34495e, #2c3e50)'
: 'linear-gradient(135deg, #ffffff, #f8f9fa)';
e.target.style.transform = 'scale(1)';
e.target.style.boxShadow = isDarkMode
? (compact ? '0 1px 4px rgba(52, 73, 94, 0.2)' : '0 2px 8px rgba(52, 73, 94, 0.3)')
: (compact ? '0 1px 2px rgba(0, 0, 0, 0.08)' : '0 1px 3px rgba(0, 0, 0, 0.1)');
e.target.style.borderColor = isDarkMode ? 'rgba(52, 152, 219, 0.3)' : '#dee2e6';
}
}}
>
{pageNum}
</button>
);
})}
</div>
</div>
);
};
export default CustomPagination;

View File

@ -30,6 +30,7 @@ import {
deleteProduct, deleteProduct,
clearProductError clearProductError
} from "../../core/redux/actions/productActions"; } from "../../core/redux/actions/productActions";
import CustomPagination from '../../components/CustomPagination';
// Add CSS animations for beautiful UI // Add CSS animations for beautiful UI
const shimmerKeyframes = ` const shimmerKeyframes = `
@ -1252,182 +1253,21 @@ const ProductList = () => {
pagination={false} // Disable Ant Design pagination pagination={false} // Disable Ant Design pagination
/> />
{/* Table Pagination with Theme Integration */} {/* Reusable Custom Pagination Component */}
<div <CustomPagination
className={`custom-pagination-container ${isDarkMode ? '' : 'light-mode'}`} currentPage={currentPage}
style={{ pageSize={pageSize}
background: 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)', totalCount={totalRecords}
border: '1px solid rgba(52, 152, 219, 0.3)', totalPages={actualTotalPages}
borderRadius: '12px', loading={loading}
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)', onPageChange={handlePageChange}
backdropFilter: 'blur(10px)', onPageSizeChange={handlePageSizeChange}
transition: 'all 0.3s ease', pageSizeOptions={[10, 20, 50, 100]}
position: 'relative', showInfo={true}
overflow: 'hidden', showPageSizeSelector={true}
padding: '16px 24px', compact={false}
margin: '16px 0' className="product-list-pagination"
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 12px 40px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(52, 152, 219, 0.2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)';
}}
>
{/* Animated background glow */}
<div
style={{
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.1), transparent)',
animation: 'shimmer 3s infinite',
pointerEvents: 'none'
}}
/> />
{/* Row Per Page Section */}
<div
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px'
}}
>
<div style={{display: 'flex', alignItems: 'center', gap: '12px'}}>
<span className="pagination-info" style={{color: '#bdc3c7', fontSize: '14px'}}>Row Per Page</span>
<select
value={pageSize}
onChange={(e) => {
const newPageSize = parseInt(e.target.value);
handlePageSizeChange(newPageSize);
}}
disabled={loading}
style={{
background: loading
? 'linear-gradient(45deg, #7f8c8d, #95a5a6)'
: 'linear-gradient(45deg, #34495e, #2c3e50)',
border: '1px solid rgba(52, 152, 219, 0.3)',
borderRadius: '6px',
color: '#ffffff',
padding: '4px 8px',
fontSize: '14px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1
}}
>
<option value={10} style={{background: '#2c3e50', color: '#ffffff'}}>10</option>
<option value={20} style={{background: '#2c3e50', color: '#ffffff'}}>20</option>
<option value={50} style={{background: '#2c3e50', color: '#ffffff'}}>50</option>
<option value={100} style={{background: '#2c3e50', color: '#ffffff'}}>100</option>
</select>
<span className="pagination-info" style={{color: '#bdc3c7', fontSize: '14px'}}>Entries</span>
</div>
<div style={{display: 'flex', alignItems: 'center', gap: '12px'}}>
<div
style={{
background: 'linear-gradient(45deg, #3498db, #2ecc71)',
borderRadius: '50%',
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
boxShadow: '0 2px 8px rgba(52, 152, 219, 0.3)'
}}
>
πŸ“Š
</div>
<span className="pagination-info" style={{color: '#bdc3c7', fontSize: '14px'}}>
Showing <strong style={{color: '#3498db'}}>{startRecord}</strong> to <strong style={{color: '#3498db'}}>{endRecord}</strong> of <strong style={{color: '#e74c3c'}}>{totalRecords}</strong> entries
{debouncedSearchTerm && (
<span style={{color: '#2ecc71', marginLeft: '8px'}}>
(filtered from <strong style={{color: '#f39c12'}}>{totalProducts || totalRecords}</strong> total)
</span>
)}
</span>
</div>
</div>
{/* Pagination Section like the image */}
<div
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '8px'
}}
>
{/* Numbered Pagination Buttons */}
{Array.from({ length: actualTotalPages }, (_, i) => {
const pageNum = i + 1;
const isActive = currentPage === pageNum;
return (
<button
key={pageNum}
onClick={() => !loading && handlePageChange(pageNum)}
disabled={loading}
className={isActive ? 'active' : ''}
style={{
background: loading
? 'linear-gradient(45deg, #7f8c8d, #95a5a6)'
: isActive
? 'linear-gradient(45deg, #f39c12, #e67e22)'
: 'linear-gradient(45deg, #34495e, #2c3e50)',
border: isActive
? '2px solid #f39c12'
: '1px solid rgba(52, 152, 219, 0.3)',
borderRadius: '50%',
width: '32px',
height: '32px',
color: '#ffffff',
fontSize: '14px',
fontWeight: '700',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.3s ease',
boxShadow: loading
? 'none'
: isActive
? '0 4px 12px rgba(243, 156, 18, 0.4)'
: '0 2px 8px rgba(52, 73, 94, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: loading ? 0.7 : 1
}}
onMouseEnter={(e) => {
if (!isActive) {
e.target.style.background = 'linear-gradient(45deg, #3498db, #2980b9)';
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.target.style.background = 'linear-gradient(45deg, #34495e, #2c3e50)';
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 2px 8px rgba(52, 73, 94, 0.3)';
}
}}
>
{pageNum}
</button>
);
})}
</div>
</div>
</> </>
)} )}
</div> </div>

View File

@ -8,32 +8,12 @@ import {
Plus Plus
} from 'feather-icons-react'; } from 'feather-icons-react';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useSelector } from 'react-redux'; import CustomPagination from '../../components/CustomPagination';
const { Option } = Select; const { Option } = Select;
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
const ProjectTracker = () => { const ProjectTracker = () => {
// Theme state for force re-render
const [themeKey, setThemeKey] = useState(0);
// Get theme from Redux
const reduxTheme = useSelector((state) => state.theme?.isDarkMode);
// Get theme from multiple sources with real-time detection
const localStorageTheme = localStorage.getItem('colorschema') === 'dark_mode';
const documentTheme = document.documentElement.getAttribute('data-layout-mode') === 'dark_mode';
const isDarkMode = reduxTheme || localStorageTheme || documentTheme;
// Debug theme state
console.log('Theme Debug:', {
reduxTheme: reduxTheme,
localStorage: localStorage.getItem('colorschema'),
documentAttribute: document.documentElement.getAttribute('data-layout-mode'),
isDarkMode: isDarkMode,
themeKey: themeKey
});
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [filterStatus, setFilterStatus] = useState('All Status'); const [filterStatus, setFilterStatus] = useState('All Status');
@ -171,42 +151,7 @@ const ProjectTracker = () => {
loadProjects(); loadProjects();
}, []); }, []);
// Listen for theme changes
useEffect(() => {
const handleThemeChange = () => {
// Force re-render when theme changes
console.log('Theme changed, forcing re-render');
setThemeKey(prev => prev + 1);
};
// Listen for data-layout-mode attribute changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-layout-mode') {
handleThemeChange();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-layout-mode']
});
// Also listen for localStorage changes
const handleStorageChange = (e) => {
if (e.key === 'colorschema') {
handleThemeChange();
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
observer.disconnect();
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// Handle pagination change // Handle pagination change
const handlePageChange = (page) => { const handlePageChange = (page) => {
@ -231,9 +176,7 @@ const ProjectTracker = () => {
} }
}; };
// Calculate pagination info
const startRecord = totalCount > 0 ? (currentPage - 1) * pageSize + 1 : 0;
const endRecord = Math.min(currentPage * pageSize, totalCount);
// Table columns configuration // Table columns configuration
const columns = [ const columns = [
@ -710,182 +653,21 @@ const ProjectTracker = () => {
</Spin> </Spin>
</div> </div>
{/* Custom Pagination - Compact Size */} {/* Reusable Custom Pagination Component */}
<div <CustomPagination
key={`pagination-${themeKey}`} currentPage={currentPage}
className={`custom-pagination-container ${isDarkMode ? '' : 'light-mode'}`} pageSize={pageSize}
style={{ totalCount={totalCount}
background: isDarkMode totalPages={totalPages}
? 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)' loading={loading}
: 'linear-gradient(135deg, #ffffff, #f8f9fa)', onPageChange={handlePageChange}
border: isDarkMode onPageSizeChange={handlePageSizeChange}
? '1px solid rgba(52, 152, 219, 0.3)' pageSizeOptions={[10, 20, 50, 100]}
: '1px solid rgba(0, 0, 0, 0.1)', showInfo={true}
borderRadius: '8px', showPageSizeSelector={true}
boxShadow: isDarkMode compact={true}
? '0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(52, 152, 219, 0.1)' className="project-tracker-pagination"
: '0 1px 6px rgba(0, 0, 0, 0.06)', />
backdropFilter: isDarkMode ? 'blur(8px)' : 'none',
transition: 'all 0.2s ease',
position: 'relative',
overflow: 'hidden',
padding: '8px 16px',
margin: '8px 0',
fontSize: '13px'
}}
>
{/* Pagination Info - Compact */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '8px',
flexWrap: 'wrap',
gap: '8px'
}}
>
<div style={{display: 'flex', alignItems: 'center', gap: '6px'}}>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: '12px', fontWeight: '500'}}>Row Per Page</span>
<select
value={pageSize}
onChange={(e) => {
const newPageSize = parseInt(e.target.value);
handlePageSizeChange(newPageSize);
}}
disabled={loading}
style={{
background: loading
? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)')
: (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'),
border: isDarkMode
? '1px solid rgba(52, 152, 219, 0.3)'
: '1px solid #dee2e6',
borderRadius: '4px',
color: isDarkMode ? '#ffffff' : '#495057',
padding: '2px 6px',
fontSize: '12px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.7 : 1,
boxShadow: isDarkMode ? 'none' : '0 1px 2px rgba(0, 0, 0, 0.05)'
}}
>
<option value={10} style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}}>10</option>
<option value={20} style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}}>20</option>
<option value={50} style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}}>50</option>
<option value={100} style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}}>100</option>
</select>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: '12px', fontWeight: '500'}}>Entries</span>
</div>
<div style={{display: 'flex', alignItems: 'center', gap: '6px'}}>
<div
style={{
background: isDarkMode
? 'linear-gradient(45deg, #3498db, #2ecc71)'
: 'linear-gradient(45deg, #007bff, #28a745)',
borderRadius: '50%',
width: '16px',
height: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '8px',
boxShadow: isDarkMode
? '0 1px 4px rgba(52, 152, 219, 0.3)'
: '0 1px 4px rgba(0, 123, 255, 0.2)',
transition: 'all 0.2s ease'
}}
>
πŸ“Š
</div>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: '12px', fontWeight: '500'}}>
Showing <strong style={{color: isDarkMode ? '#3498db' : '#007bff'}}>{startRecord}</strong> to <strong style={{color: isDarkMode ? '#3498db' : '#007bff'}}>{endRecord}</strong> of <strong style={{color: isDarkMode ? '#e74c3c' : '#dc3545'}}>{totalCount}</strong> entries
</span>
</div>
</div>
{/* Pagination Buttons - Compact */}
<div
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '4px'
}}
>
{/* Numbered Pagination Buttons */}
{Array.from({ length: totalPages }, (_, i) => {
const pageNum = i + 1;
const isActive = currentPage === pageNum;
return (
<button
key={pageNum}
onClick={() => !loading && handlePageChange(pageNum)}
disabled={loading}
style={{
background: loading
? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)')
: isActive
? (isDarkMode ? 'linear-gradient(45deg, #f39c12, #e67e22)' : 'linear-gradient(135deg, #007bff, #0056b3)')
: (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'),
border: isActive
? (isDarkMode ? '1px solid #f39c12' : '1px solid #007bff')
: (isDarkMode ? '1px solid rgba(52, 152, 219, 0.3)' : '1px solid #dee2e6'),
borderRadius: '50%',
width: '24px',
height: '24px',
color: isActive
? '#ffffff'
: (isDarkMode ? '#ffffff' : '#495057'),
fontSize: '11px',
fontWeight: '600',
cursor: loading ? 'not-allowed' : 'pointer',
transition: 'all 0.2s ease',
boxShadow: loading
? 'none'
: isActive
? (isDarkMode ? '0 2px 6px rgba(243, 156, 18, 0.3)' : '0 2px 4px rgba(0, 123, 255, 0.2)')
: (isDarkMode ? '0 1px 4px rgba(52, 73, 94, 0.2)' : '0 1px 2px rgba(0, 0, 0, 0.08)'),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: loading ? 0.6 : 1
}}
onMouseEnter={(e) => {
if (!loading && !isActive) {
e.target.style.background = isDarkMode
? 'linear-gradient(45deg, #3498db, #2980b9)'
: 'linear-gradient(135deg, #e9ecef, #f8f9fa)';
e.target.style.transform = isDarkMode ? 'scale(1.05)' : 'translateY(-1px) scale(1.02)';
e.target.style.boxShadow = isDarkMode
? '0 2px 6px rgba(52, 152, 219, 0.3)'
: '0 2px 4px rgba(0, 0, 0, 0.12)';
e.target.style.borderColor = isDarkMode ? '#3498db' : '#adb5bd';
}
}}
onMouseLeave={(e) => {
if (!loading && !isActive) {
e.target.style.background = isDarkMode
? 'linear-gradient(45deg, #34495e, #2c3e50)'
: 'linear-gradient(135deg, #ffffff, #f8f9fa)';
e.target.style.transform = 'scale(1)';
e.target.style.boxShadow = isDarkMode
? '0 1px 4px rgba(52, 73, 94, 0.2)'
: '0 1px 2px rgba(0, 0, 0, 0.08)';
e.target.style.borderColor = isDarkMode ? 'rgba(52, 152, 219, 0.3)' : '#dee2e6';
}
}}
>
{pageNum}
</button>
);
})}
</div>
</div>
</div> </div>
</div> </div>