π¨ Add Custom Pagination Component to Project Tracker
π Custom Pagination Features: - Beautiful custom pagination UI cloned from Product component - Dark/Light mode support with dynamic styling - Animated pagination buttons with hover effects - Page size selector (10, 20, 50, 100 entries) - Real-time pagination info display π¨ Visual Enhancements: - Gradient backgrounds with glassmorphism effects - Smooth animations and transitions - Hover effects with scale and shadow animations - Active page highlighting with orange gradient - Loading states with disabled styling π§ Technical Implementation: - Hide default Ant Design pagination completely - Custom pagination state management - Proper page change and page size change handlers - Redux integration for dark mode theme - Responsive design with flexbox layout π Styling Features: - Light mode: Clean white/gray gradients - Dark mode: Dark blue/gray gradients with neon accents - Animated hover effects on all interactive elements - Glassmorphism backdrop blur effects - Color-coded pagination info (blue for range, red for total) π User Experience: - Smooth page transitions - Visual feedback for all interactions - Disabled states during loading - Intuitive page size selection - Clear pagination information display π± Responsive Design: - Flexible layout that adapts to screen size - Proper spacing and alignment - Mobile-friendly button sizes - Wrap-friendly pagination info layout
This commit is contained in:
parent
704251d57f
commit
536fe5f3cc
@ -8,11 +8,15 @@ import {
|
|||||||
Plus
|
Plus
|
||||||
} from 'feather-icons-react';
|
} from 'feather-icons-react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
const ProjectTracker = () => {
|
const ProjectTracker = () => {
|
||||||
|
// Get theme from Redux
|
||||||
|
const isDarkMode = useSelector((state) => state.theme?.isDarkMode);
|
||||||
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
const [filterStatus, setFilterStatus] = useState('All Status');
|
const [filterStatus, setFilterStatus] = useState('All Status');
|
||||||
const [filterManager, setFilterManager] = useState('All Managers');
|
const [filterManager, setFilterManager] = useState('All Managers');
|
||||||
@ -26,21 +30,21 @@ const ProjectTracker = () => {
|
|||||||
// API data state
|
// API data state
|
||||||
const [projectData, setProjectData] = useState([]);
|
const [projectData, setProjectData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [pagination, setPagination] = useState({
|
|
||||||
currentPage: 1,
|
// Pagination state
|
||||||
pageSize: 10,
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
totalCount: 0,
|
const [pageSize, setPageSize] = useState(10);
|
||||||
totalPages: 1
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
});
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
|
||||||
// Load projects from API
|
// Load projects from API
|
||||||
const loadProjects = async (page = 1, pageSize = 10) => {
|
const loadProjects = async (page = currentPage, size = pageSize) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || '';
|
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL || '';
|
||||||
console.log('Loading projects from:', `${apiBaseUrl}Projects`);
|
console.log('Loading projects from:', `${apiBaseUrl}Projects`);
|
||||||
|
|
||||||
const response = await fetch(`${apiBaseUrl}Projects?page=${page}&pageSize=${pageSize}`, {
|
const response = await fetch(`${apiBaseUrl}Projects?page=${page}&pageSize=${size}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -78,28 +82,30 @@ const ProjectTracker = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setProjectData(mappedData);
|
setProjectData(mappedData);
|
||||||
setPagination(result.pagination || {
|
|
||||||
currentPage: 1,
|
// Update pagination state
|
||||||
pageSize: 10,
|
if (result.pagination) {
|
||||||
totalCount: result.data.length,
|
setCurrentPage(result.pagination.currentPage);
|
||||||
totalPages: 1
|
setTotalCount(result.pagination.totalCount);
|
||||||
});
|
setTotalPages(result.pagination.totalPages);
|
||||||
|
} else {
|
||||||
|
setTotalCount(result.data.length);
|
||||||
|
setTotalPages(Math.ceil(result.data.length / size));
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Mapped data:', mappedData);
|
console.log('Mapped data:', mappedData);
|
||||||
} else {
|
} else {
|
||||||
console.warn('No data found in API response');
|
console.warn('No data found in API response');
|
||||||
setProjectData([]);
|
setProjectData([]);
|
||||||
|
setTotalCount(0);
|
||||||
|
setTotalPages(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading projects:', error);
|
console.error('Error loading projects:', error);
|
||||||
// Set empty data on error
|
// Set empty data on error
|
||||||
setProjectData([]);
|
setProjectData([]);
|
||||||
setPagination({
|
setTotalCount(0);
|
||||||
currentPage: 1,
|
setTotalPages(1);
|
||||||
pageSize: 10,
|
|
||||||
totalCount: 0,
|
|
||||||
totalPages: 1
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -148,10 +154,32 @@ const ProjectTracker = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle pagination change
|
// Handle pagination change
|
||||||
const handleTableChange = (paginationInfo) => {
|
const handlePageChange = (page) => {
|
||||||
loadProjects(paginationInfo.current, paginationInfo.pageSize);
|
setCurrentPage(page);
|
||||||
|
loadProjects(page, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page size change
|
||||||
|
const handlePageSizeChange = (newPageSize) => {
|
||||||
|
setPageSize(newPageSize);
|
||||||
|
setCurrentPage(1); // Reset to first page when changing page size
|
||||||
|
loadProjects(1, newPageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle table change (for Ant Design Table)
|
||||||
|
const handleTableChange = (paginationInfo) => {
|
||||||
|
if (paginationInfo.current !== currentPage) {
|
||||||
|
handlePageChange(paginationInfo.current);
|
||||||
|
}
|
||||||
|
if (paginationInfo.pageSize !== pageSize) {
|
||||||
|
handlePageSizeChange(paginationInfo.pageSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 = [
|
||||||
{
|
{
|
||||||
@ -168,13 +196,13 @@ const ProjectTracker = () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: 'Mα»©c Δα»',
|
||||||
dataIndex: 'priority',
|
dataIndex: 'priority',
|
||||||
width: 20,
|
width: 30,
|
||||||
render: (priority) => (
|
render: (priority) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '4px',
|
width: '50px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
backgroundColor: priority === 'high' ? '#dc3545' : priority === 'medium' ? '#ffc107' : '#28a745',
|
backgroundColor: priority === 'high' ? '#dc3545' : priority === 'medium' ? '#ffc107' : '#28a745',
|
||||||
borderRadius: '2px'
|
borderRadius: '2px'
|
||||||
@ -191,8 +219,9 @@ const ProjectTracker = () => {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Category',
|
title: 'Danh mα»₯c',
|
||||||
dataIndex: 'category',
|
dataIndex: 'category',
|
||||||
|
width: 100,
|
||||||
key: 'category',
|
key: 'category',
|
||||||
render: (category, record) => (
|
render: (category, record) => (
|
||||||
<Tag color={record.categoryColor} style={{ borderRadius: '12px', fontSize: '12px' }}>
|
<Tag color={record.categoryColor} style={{ borderRadius: '12px', fontSize: '12px' }}>
|
||||||
@ -408,6 +437,94 @@ const ProjectTracker = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
/* Hide default Ant Design pagination */
|
||||||
|
.ant-pagination-total-text,
|
||||||
|
.ant-table-wrapper .ant-pagination,
|
||||||
|
.ant-spin-container .ant-pagination {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: -9999px !important;
|
||||||
|
top: -9999px !important;
|
||||||
|
z-index: -1 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure our custom pagination is visible */
|
||||||
|
.custom-pagination-container {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode pagination styling */
|
||||||
|
.custom-pagination-container.light-mode {
|
||||||
|
background: linear-gradient(135deg, #ffffff, #f8f9fa) !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 16px 20px !important;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
margin-top: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode text styling */
|
||||||
|
.custom-pagination-container.light-mode .pagination-info {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode select styling */
|
||||||
|
.custom-pagination-container.light-mode select {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1px solid #dee2e6 !important;
|
||||||
|
color: #495057 !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination-container.light-mode select:focus {
|
||||||
|
border-color: #80bdff !important;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode pagination buttons */
|
||||||
|
.custom-pagination-container.light-mode button {
|
||||||
|
background: linear-gradient(135deg, #ffffff, #f8f9fa) !important;
|
||||||
|
border: 1px solid #dee2e6 !important;
|
||||||
|
color: #495057 !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination-container.light-mode button:hover {
|
||||||
|
background: linear-gradient(135deg, #e9ecef, #f8f9fa) !important;
|
||||||
|
border-color: #adb5bd !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination-container.light-mode button.active {
|
||||||
|
background: linear-gradient(135deg, #007bff, #0056b3) !important;
|
||||||
|
border-color: #007bff !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 123, 255, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination-container.light-mode button:disabled {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border-color: #dee2e6 !important;
|
||||||
|
color: #6c757d !important;
|
||||||
|
opacity: 0.6 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Table
|
<Table
|
||||||
rowSelection={rowSelection}
|
rowSelection={rowSelection}
|
||||||
@ -415,18 +532,168 @@ const ProjectTracker = () => {
|
|||||||
dataSource={projectData}
|
dataSource={projectData}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
pagination={false}
|
||||||
current: pagination.currentPage,
|
|
||||||
pageSize: pagination.pageSize,
|
|
||||||
total: pagination.totalCount,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
showTotal: (total, range) =>
|
|
||||||
`Row Per Page: ${range[1] - range[0] + 1} Entries | Showing ${range[0]} to ${range[1]} of ${total} entries`
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Pagination */}
|
||||||
|
<div
|
||||||
|
className={`custom-pagination-container ${isDarkMode ? '' : 'light-mode'}`}
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)',
|
||||||
|
border: '1px solid rgba(52, 152, 219, 0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '16px 24px',
|
||||||
|
margin: '16px 0'
|
||||||
|
}}
|
||||||
|
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)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Pagination Info */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '16px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '12px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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'}}>{totalCount}</strong> entries
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Buttons */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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}
|
||||||
|
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.6 : 1
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (!loading && !isActive) {
|
||||||
|
e.target.style.background = 'linear-gradient(45deg, #3498db, #2980b9)';
|
||||||
|
e.target.style.transform = 'scale(1.1)';
|
||||||
|
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!loading && !isActive) {
|
||||||
|
e.target.style.background = 'linear-gradient(45deg, #34495e, #2c3e50)';
|
||||||
|
e.target.style.transform = 'scale(1)';
|
||||||
|
e.target.style.boxShadow = '0 2px 8px rgba(52, 73, 94, 0.3)';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loadingβ¦
x
Reference in New Issue
Block a user