Add beautiful single-row pagination with total records

- � Beautiful gradient design with animations
- � Total records display with search filtering
- � Single-row layout with Ant Design pagination classes
- � Responsive compact design for all devices
-  Smooth hover animations and transitions
- � Ultra compact buttons and optimized spacing
- � Real-time search integration with debouncing
- � Glass morphism effects with shimmer animations
This commit is contained in:
tuanOts
2025-05-25 23:58:48 +07:00
parent 9687351177
commit f75e524565
5 changed files with 737 additions and 58 deletions
+232 -11
View File
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import React, { useState, useEffect, useRef } from "react";
import { Link, useParams } from "react-router-dom";
import Select from "react-select";
import { all_routes } from "../../Router/all_routes";
import { DatePicker } from "antd";
@@ -20,15 +20,90 @@ import {
} from "feather-icons-react/build/IconComponents";
import { useDispatch, useSelector } from "react-redux";
import { setToogleHeader } from "../../core/redux/action";
import { fetchProduct } from "../../core/redux/actions/productActions";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
const EditProduct = () => {
const route = all_routes;
const dispatch = useDispatch();
const { id } = useParams(); // Get product ID from URL
const data = useSelector((state) => state.toggle_header);
// Get product data from Redux store
const { currentProduct, productLoading, productError } = useSelector((state) => state.products || {});
// Track if we've already fetched this product
const fetchedProductId = useRef(null);
// Form state for editing
const [formData, setFormData] = useState({
name: '',
slug: '',
sku: '',
description: '',
price: '',
category: null, // Change to null for Select component
brand: null, // Change to null for Select component
quantity: '',
unit: null, // Change to null for Select component
status: '',
images: []
});
// Load product data if ID is provided and product not already loaded
useEffect(() => {
if (id && fetchedProductId.current !== id) {
console.log('Fetching product details for ID:', id);
fetchedProductId.current = id;
dispatch(fetchProduct(id));
}
}, [id, dispatch]);
// Helper function to find option by value or label
const findSelectOption = (options, value) => {
if (!value) return null;
return options.find(option =>
option.value === value ||
option.label === value ||
option.value.toLowerCase() === value.toLowerCase() ||
option.label.toLowerCase() === value.toLowerCase()
) || null;
};
// Update form data when currentProduct changes
useEffect(() => {
if (currentProduct) {
console.log('Product data loaded:', currentProduct);
// Find matching options for select fields
const categoryOption = findSelectOption(category, currentProduct.category || currentProduct.categoryName);
const brandOption = findSelectOption(brand, currentProduct.brand || currentProduct.brandName);
const unitOption = findSelectOption(unit, currentProduct.unit);
setFormData({
name: currentProduct.name || currentProduct.productName || '',
slug: currentProduct.slug || '',
sku: currentProduct.sku || currentProduct.code || '',
description: currentProduct.description || '',
price: currentProduct.price || currentProduct.salePrice || '',
category: categoryOption,
brand: brandOption,
quantity: currentProduct.quantity || currentProduct.stock || '',
unit: unitOption,
status: currentProduct.status || 'active',
images: currentProduct.images || []
});
console.log('Form data updated:', {
category: categoryOption,
brand: brandOption,
unit: unitOption
});
}
}, [currentProduct]);
const [selectedDate, setSelectedDate] = useState(new Date());
const handleDateChange = (date) => {
setSelectedDate(date);
@@ -58,6 +133,9 @@ const EditProduct = () => {
{ value: "choose", label: "Choose" },
{ value: "lenovo", label: "Lenovo" },
{ value: "electronics", label: "Electronics" },
{ value: "laptop", label: "Laptop" },
{ value: "computer", label: "Computer" },
{ value: "mobile", label: "Mobile" },
];
const subcategory = [
{ value: "choose", label: "Choose" },
@@ -78,6 +156,11 @@ const EditProduct = () => {
{ value: "choose", label: "Choose" },
{ value: "kg", label: "Kg" },
{ value: "pc", label: "Pc" },
{ value: "pcs", label: "Pcs" },
{ value: "piece", label: "Piece" },
{ value: "gram", label: "Gram" },
{ value: "liter", label: "Liter" },
{ value: "meter", label: "Meter" },
];
const sellingtype = [
{ value: "choose", label: "Choose" },
@@ -114,13 +197,74 @@ const EditProduct = () => {
const handleRemoveProduct1 = () => {
setIsImageVisible1(false);
};
// Show loading state
if (productLoading) {
return (
<div className="page-wrapper">
<div className="content">
<div className="text-center p-4">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-2">Loading product details...</p>
</div>
</div>
</div>
);
}
// Show error state
if (productError) {
return (
<div className="page-wrapper">
<div className="content">
<div className="alert alert-danger" role="alert">
<strong>Error:</strong> {productError}
<div className="mt-2">
<Link to={route.productlist} className="btn btn-secondary me-2">
Back to Product List
</Link>
<button
className="btn btn-primary"
onClick={() => {
console.log('Retrying fetch for ID:', id);
dispatch(fetchProduct(id));
}}
>
Retry
</button>
</div>
</div>
</div>
</div>
);
}
// Show not found state
if (id && !currentProduct) {
return (
<div className="page-wrapper">
<div className="content">
<div className="alert alert-warning" role="alert">
<strong>Product not found!</strong> The product you&apos;re trying to edit doesn&apos;t exist.
<div className="mt-2">
<Link to={route.productlist} className="btn btn-secondary">
Back to Product List
</Link>
</div>
</div>
</div>
</div>
);
}
return (
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Edit Product</h4>
<h4>Edit Product {currentProduct?.name ? `- ${currentProduct.name}` : ''}</h4>
</div>
</div>
<ul className="table-top-head">
@@ -211,13 +355,33 @@ const EditProduct = () => {
<div className="col-lg-4 col-sm-6 col-12">
<div className="mb-3 add-product">
<label className="form-label">Product Name</label>
<input type="text" className="form-control" />
<input
type="text"
className="form-control"
value={formData.name}
onChange={(e) => {
setFormData(prev => ({
...prev,
name: e.target.value
}));
}}
/>
</div>
</div>
<div className="col-lg-4 col-sm-6 col-12">
<div className="mb-3 add-product">
<label className="form-label">Slug</label>
<input type="text" className="form-control" />
<input
type="text"
className="form-control"
value={formData.slug}
onChange={(e) => {
setFormData(prev => ({
...prev,
slug: e.target.value
}));
}}
/>
</div>
</div>
<div className="col-lg-4 col-sm-6 col-12">
@@ -227,6 +391,13 @@ const EditProduct = () => {
type="text"
className="form-control list"
placeholder="Enter SKU"
value={formData.sku}
onChange={(e) => {
setFormData(prev => ({
...prev,
sku: e.target.value
}));
}}
/>
<Link
to={route.addproduct}
@@ -255,7 +426,15 @@ const EditProduct = () => {
<Select
className="select"
options={category}
placeholder="Lenovo"
placeholder="Choose Category"
value={formData.category}
onChange={(selectedOption) => {
setFormData(prev => ({
...prev,
category: selectedOption
}));
}}
isClearable
/>
</div>
</div>
@@ -301,7 +480,15 @@ const EditProduct = () => {
<Select
className="select"
options={brand}
placeholder="Nike"
placeholder="Choose Brand"
value={formData.brand}
onChange={(selectedOption) => {
setFormData(prev => ({
...prev,
brand: selectedOption
}));
}}
isClearable
/>
</div>
</div>
@@ -321,7 +508,15 @@ const EditProduct = () => {
<Select
className="select"
options={unit}
placeholder="Kg"
placeholder="Choose Unit"
value={formData.unit}
onChange={(selectedOption) => {
setFormData(prev => ({
...prev,
unit: selectedOption
}));
}}
isClearable
/>
</div>
</div>
@@ -374,7 +569,13 @@ const EditProduct = () => {
<textarea
className="form-control h-100"
rows={5}
defaultValue={""}
value={formData.description}
onChange={(e) => {
setFormData(prev => ({
...prev,
description: e.target.value
}));
}}
/>
<p className="mt-1">Maximum 60 Characters</p>
</div>
@@ -474,13 +675,33 @@ const EditProduct = () => {
<div className="col-lg-4 col-sm-6 col-12">
<div className="input-blocks add-product">
<label>Quantity</label>
<input type="text" className="form-control" />
<input
type="text"
className="form-control"
value={formData.quantity}
onChange={(e) => {
setFormData(prev => ({
...prev,
quantity: e.target.value
}));
}}
/>
</div>
</div>
<div className="col-lg-4 col-sm-6 col-12">
<div className="input-blocks add-product">
<label>Price</label>
<input type="text" className="form-control" />
<input
type="text"
className="form-control"
value={formData.price}
onChange={(e) => {
setFormData(prev => ({
...prev,
price: e.target.value
}));
}}
/>
</div>
</div>
<div className="col-lg-4 col-sm-6 col-12">
+384 -15
View File
@@ -31,12 +31,54 @@ import {
clearProductError
} from "../../core/redux/actions/productActions";
// Add CSS animations for beautiful UI
const shimmerKeyframes = `
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 5px rgba(52, 152, 219, 0.3); }
50% { box-shadow: 0 0 20px rgba(52, 152, 219, 0.6); }
}
`;
// Inject CSS into head if not already present
if (typeof document !== 'undefined' && !document.getElementById('beautiful-pagination-styles')) {
const styleSheet = document.createElement('style');
styleSheet.id = 'beautiful-pagination-styles';
styleSheet.type = 'text/css';
styleSheet.innerText = shimmerKeyframes;
document.head.appendChild(styleSheet);
}
const ProductList = () => {
// Use new Redux structure for API data, fallback to legacy for existing functionality
const {
products: apiProducts,
loading,
error
error,
totalProducts,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage
} = useSelector((state) => state.products);
// Fallback to legacy data if API data is not available
@@ -49,26 +91,46 @@ const ProductList = () => {
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
// State for pagination - sync with Redux
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const pageSize = reduxPageSize || 20;
// Debounced search term
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const toggleFilterVisibility = () => {
setIsFilterVisible((prevVisibility) => !prevVisibility);
};
const route = all_routes;
// Fetch products on component mount
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500); // 500ms delay
return () => clearTimeout(timer);
}, [searchTerm]);
// Fetch products when debounced search term or pagination changes
useEffect(() => {
const loadProducts = async () => {
try {
await dispatch(fetchProducts());
// Only fetch products - categories/brands may be included in response
// or can be extracted from products data
const searchParams = {
page: currentPage,
pageSize: pageSize,
searchTerm: debouncedSearchTerm
};
await dispatch(fetchProducts(searchParams));
} catch (error) {
console.error('Failed to load products:', error);
}
};
loadProducts();
}, [dispatch]);
}, [dispatch, currentPage, pageSize, debouncedSearchTerm]);
// Handle product deletion
const handleDeleteProduct = async (productId) => {
@@ -102,10 +164,25 @@ const ProductList = () => {
const handleSearch = (e) => {
const value = e.target.value;
setSearchTerm(value);
// You can implement debounced search here
// For now, we'll just update the search term
// Reset to first page when searching
setCurrentPage(1);
};
// Handle pagination
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Calculate pagination info
const totalRecords = totalProducts || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
const startRecord = totalRecords > 0 ? (currentPage - 1) * pageSize + 1 : 0;
const endRecord = Math.min(currentPage * pageSize, totalRecords);
// Debug logs removed for production
// Clear error when component unmounts
useEffect(() => {
return () => {
@@ -163,34 +240,83 @@ const ProductList = () => {
{
title: "SKU",
dataIndex: "sku",
sorter: (a, b) => a.sku.length - b.sku.length,
render: (_, record) => {
const sku = record.sku || record.code || record.productCode || '-';
return <span>{sku}</span>;
},
sorter: (a, b) => {
const skuA = a.sku || a.code || a.productCode || '';
const skuB = b.sku || b.code || b.productCode || '';
return skuA.length - skuB.length;
},
},
{
title: "Category",
dataIndex: "category",
sorter: (a, b) => a.category.length - b.category.length,
render: (_, record) => {
const category = record.category || record.categoryName || '-';
return <span>{category}</span>;
},
sorter: (a, b) => {
const catA = a.category || a.categoryName || '';
const catB = b.category || b.categoryName || '';
return catA.length - catB.length;
},
},
{
title: "Brand",
dataIndex: "brand",
sorter: (a, b) => a.brand.length - b.brand.length,
render: (_, record) => {
const brand = record.brand || record.brandName || '-';
return <span>{brand}</span>;
},
sorter: (a, b) => {
const brandA = a.brand || a.brandName || '';
const brandB = b.brand || b.brandName || '';
return brandA.length - brandB.length;
},
},
{
title: "Price",
dataIndex: "price",
sorter: (a, b) => a.price.length - b.price.length,
render: (_, record) => {
const price = record.price || record.salePrice || record.unitPrice || 0;
return <span>${Number(price).toFixed(2)}</span>;
},
sorter: (a, b) => {
const priceA = Number(a.price || a.salePrice || a.unitPrice || 0);
const priceB = Number(b.price || b.salePrice || b.unitPrice || 0);
return priceA - priceB;
},
},
{
title: "Unit",
dataIndex: "unit",
sorter: (a, b) => a.unit.length - b.unit.length,
render: (_, record) => {
const unit = record.unit || record.unitOfMeasure || '-';
return <span>{unit}</span>;
},
sorter: (a, b) => {
const unitA = a.unit || a.unitOfMeasure || '';
const unitB = b.unit || b.unitOfMeasure || '';
return unitA.length - unitB.length;
},
},
{
title: "Qty",
dataIndex: "qty",
sorter: (a, b) => a.qty.length - b.qty.length,
render: (_, record) => {
// Try multiple possible field names for quantity
const quantity = record.qty || record.quantity || record.stock || record.stockQuantity || 0;
return <span>{quantity}</span>;
},
sorter: (a, b) => {
const qtyA = a.qty || a.quantity || a.stock || a.stockQuantity || 0;
const qtyB = b.qty || b.quantity || b.stock || b.stockQuantity || 0;
return Number(qtyA) - Number(qtyB);
},
},
{
@@ -509,7 +635,250 @@ const ProductList = () => {
</button>
</div>
) : (
<Table columns={columns} dataSource={dataSource} />
<>
<Table
columns={columns}
dataSource={dataSource}
pagination={false} // Disable Ant Design pagination
/>
{/* Ant Design Pagination Structure with Beautiful Design */}
<div
className="ant-pagination ant-table-pagination ant-table-pagination-right css-dev-only-do-not-override-vrrzze"
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',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
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'
}}
/>
{/* Left side - Total Records Info */}
<div
className="ant-pagination-total-text"
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
alignItems: 'center',
gap: '12px'
}}
>
<div
style={{
background: 'linear-gradient(45deg, #3498db, #2ecc71)',
borderRadius: '50%',
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
boxShadow: '0 4px 12px rgba(52, 152, 219, 0.3)'
}}
>
📊
</div>
<div>
<span style={{color: '#bdc3c7', fontSize: '14px', lineHeight: '1.4'}}>
Showing <strong style={{color: '#3498db', fontWeight: '700'}}>{startRecord}</strong> to <strong style={{color: '#3498db', fontWeight: '700'}}>{endRecord}</strong> of <strong style={{color: '#e74c3c', fontWeight: '700'}}>{totalRecords}</strong> entries
{debouncedSearchTerm && (
<div style={{color: '#2ecc71', fontSize: '12px', marginTop: '2px'}}>
🔍 Filtered from <strong style={{color: '#f39c12'}}>{totalProducts || totalRecords}</strong> total products
</div>
)}
</span>
</div>
</div>
{/* Right side - Pagination Controls */}
{actualTotalPages > 1 && (
<div
className="ant-pagination-options"
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<span style={{color: '#bdc3c7', fontSize: '12px', marginRight: '8px'}}>
Page {currentPage} of {actualTotalPages}
</span>
<ul
className="ant-pagination-list"
style={{
display: 'flex',
listStyle: 'none',
margin: 0,
padding: 0,
gap: '4px'
}}
>
<li className={`ant-pagination-prev ${currentPage === 1 ? 'ant-pagination-disabled' : ''}`}>
<button
className="ant-pagination-item-link"
style={{
background: currentPage === 1
? 'rgba(52, 73, 94, 0.5)'
: 'linear-gradient(45deg, #3498db, #2980b9)',
border: 'none',
borderRadius: '8px',
color: currentPage === 1 ? '#7f8c8d' : '#ffffff',
padding: '6px 12px',
fontSize: '12px',
fontWeight: '600',
transition: 'all 0.3s ease',
boxShadow: currentPage === 1
? 'none'
: '0 2px 8px rgba(52, 152, 219, 0.3)',
cursor: currentPage === 1 ? 'not-allowed' : 'pointer'
}}
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
onMouseEnter={(e) => {
if (currentPage !== 1) {
e.target.style.transform = 'translateY(-1px)';
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
}
}}
onMouseLeave={(e) => {
if (currentPage !== 1) {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 2px 8px rgba(52, 152, 219, 0.3)';
}
}}
>
Prev
</button>
</li>
{Array.from({ length: Math.min(3, actualTotalPages) }, (_, i) => {
let pageNum = i + 1;
if (actualTotalPages > 3 && currentPage > 2) {
pageNum = currentPage - 1 + i;
}
const isActive = currentPage === pageNum;
return (
<li key={pageNum} className={`ant-pagination-item ${isActive ? 'ant-pagination-item-active' : ''}`}>
<button
className="ant-pagination-item-link"
style={{
background: isActive
? 'linear-gradient(45deg, #e74c3c, #c0392b)'
: 'linear-gradient(45deg, #34495e, #2c3e50)',
border: isActive
? '2px solid #e74c3c'
: '1px solid rgba(52, 152, 219, 0.3)',
borderRadius: '8px',
color: '#ffffff',
padding: '6px 12px',
fontSize: '12px',
fontWeight: '700',
minWidth: '32px',
transition: 'all 0.3s ease',
boxShadow: isActive
? '0 4px 12px rgba(231, 76, 60, 0.4)'
: '0 2px 8px rgba(52, 73, 94, 0.3)',
cursor: 'pointer'
}}
onClick={() => handlePageChange(pageNum)}
onMouseEnter={(e) => {
if (!isActive) {
e.target.style.background = 'linear-gradient(45deg, #3498db, #2980b9)';
e.target.style.transform = 'translateY(-1px)';
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>
</li>
);
})}
<li className={`ant-pagination-next ${currentPage === actualTotalPages ? 'ant-pagination-disabled' : ''}`}>
<button
className="ant-pagination-item-link"
style={{
background: currentPage === actualTotalPages
? 'rgba(52, 73, 94, 0.5)'
: 'linear-gradient(45deg, #3498db, #2980b9)',
border: 'none',
borderRadius: '8px',
color: currentPage === actualTotalPages ? '#7f8c8d' : '#ffffff',
padding: '6px 12px',
fontSize: '12px',
fontWeight: '600',
transition: 'all 0.3s ease',
boxShadow: currentPage === actualTotalPages
? 'none'
: '0 2px 8px rgba(52, 152, 219, 0.3)',
cursor: currentPage === actualTotalPages ? 'not-allowed' : 'pointer'
}}
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === actualTotalPages}
onMouseEnter={(e) => {
if (currentPage !== actualTotalPages) {
e.target.style.transform = 'translateY(-1px)';
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
}
}}
onMouseLeave={(e) => {
if (currentPage !== actualTotalPages) {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 2px 8px rgba(52, 152, 219, 0.3)';
}
}}
>
Next
</button>
</li>
</ul>
</div>
)}
</div>
</>
)}
</div>
</div>