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

View File

@ -842,6 +842,13 @@ export const publicRoutes = [
element: <EditProduct />, element: <EditProduct />,
route: Route, route: Route,
}, },
{
id: 65.1,
path: `${routes.editproduct}/:id`,
name: "editproductwithid",
element: <EditProduct />,
route: Route,
},
{ {
id: 63, id: 63,
path: routes.videocall, path: routes.videocall,

View File

@ -4,6 +4,7 @@ const ApiTest = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null); const [result, setResult] = useState(null);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [productId, setProductId] = useState('1'); // Default test ID
const testApiConnection = async () => { const testApiConnection = async () => {
setLoading(true); setLoading(true);
@ -51,6 +52,50 @@ const ApiTest = () => {
} }
}; };
const testProductDetail = async () => {
if (!productId) {
setError('Please enter a product ID');
return;
}
setLoading(true);
setError(null);
setResult(null);
try {
console.log('Testing Product Detail API for ID:', productId);
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}Products/${productId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
setResult(data);
console.log('Product Detail Response:', data);
} catch (err) {
console.error('Product Detail API Error:', err);
let errorMessage = err.message || 'Failed to fetch product details';
if (err.name === 'TypeError' && err.message.includes('Failed to fetch')) {
errorMessage = 'CORS Error or Network issue';
}
setError(errorMessage);
} finally {
setLoading(false);
}
};
return ( return (
<div className="container mt-4"> <div className="container mt-4">
<div className="card"> <div className="card">
@ -60,13 +105,38 @@ const ApiTest = () => {
<div className="card-body"> <div className="card-body">
<p><strong>API Base URL:</strong> {process.env.REACT_APP_API_BASE_URL}</p> <p><strong>API Base URL:</strong> {process.env.REACT_APP_API_BASE_URL}</p>
<button <div className="row">
className="btn btn-primary" <div className="col-md-6">
onClick={testApiConnection} <h6>Test All Products API</h6>
disabled={loading} <button
> className="btn btn-primary"
{loading ? 'Testing...' : 'Test API Connection'} onClick={testApiConnection}
</button> disabled={loading}
>
{loading ? 'Testing...' : 'Test Products List API'}
</button>
</div>
<div className="col-md-6">
<h6>Test Product Detail API</h6>
<div className="input-group mb-2">
<input
type="text"
className="form-control"
placeholder="Enter Product ID"
value={productId}
onChange={(e) => setProductId(e.target.value)}
/>
<button
className="btn btn-success"
onClick={testProductDetail}
disabled={loading || !productId}
>
{loading ? 'Testing...' : 'Test Product Detail'}
</button>
</div>
</div>
</div>
{loading && ( {loading && (
<div className="mt-3"> <div className="mt-3">

View File

@ -6,28 +6,31 @@ const initialState = {
totalProducts: 0, totalProducts: 0,
currentPage: 1, currentPage: 1,
totalPages: 1, totalPages: 1,
pageSize: 20,
hasPrevious: false,
hasNext: false,
// Current product (for edit/view) // Current product (for edit/view)
currentProduct: null, currentProduct: null,
// Search results // Search results
searchResults: [], searchResults: [],
searchQuery: '', searchQuery: '',
// Categories and brands // Categories and brands
categories: [], categories: [],
brands: [], brands: [],
// Loading states // Loading states
loading: false, loading: false,
productLoading: false, productLoading: false,
searchLoading: false, searchLoading: false,
// Error states // Error states
error: null, error: null,
productError: null, productError: null,
searchError: null, searchError: null,
// Operation states // Operation states
creating: false, creating: false,
updating: false, updating: false,
@ -43,18 +46,27 @@ const productReducer = (state = initialState, action) => {
loading: true, loading: true,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS: case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS: {
// Handle different API response structures
const isArrayResponse = Array.isArray(action.payload);
const products = isArrayResponse ? action.payload : (action.payload.data || action.payload.items || []);
const pagination = action.payload.pagination || {};
return { return {
...state, ...state,
loading: false, loading: false,
products: action.payload.data || action.payload, products: products,
totalProducts: action.payload.total || action.payload.length, totalProducts: pagination.totalCount || action.payload.total || products.length,
currentPage: action.payload.currentPage || 1, currentPage: pagination.currentPage || action.payload.currentPage || 1,
totalPages: action.payload.totalPages || 1, totalPages: pagination.totalPages || action.payload.totalPages || 1,
pageSize: pagination.pageSize || 20,
hasPrevious: pagination.hasPrevious || false,
hasNext: pagination.hasNext || false,
error: null, error: null,
}; };
}
case PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE: case PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE:
return { return {
...state, ...state,
@ -69,7 +81,7 @@ const productReducer = (state = initialState, action) => {
productLoading: true, productLoading: true,
productError: null, productError: null,
}; };
case PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS: case PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS:
return { return {
...state, ...state,
@ -77,7 +89,7 @@ const productReducer = (state = initialState, action) => {
currentProduct: action.payload, currentProduct: action.payload,
productError: null, productError: null,
}; };
case PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE: case PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE:
return { return {
...state, ...state,
@ -92,7 +104,7 @@ const productReducer = (state = initialState, action) => {
creating: true, creating: true,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS: case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS:
return { return {
...state, ...state,
@ -101,7 +113,7 @@ const productReducer = (state = initialState, action) => {
totalProducts: state.totalProducts + 1, totalProducts: state.totalProducts + 1,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE: case PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE:
return { return {
...state, ...state,
@ -116,7 +128,7 @@ const productReducer = (state = initialState, action) => {
updating: true, updating: true,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS: case PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS:
return { return {
...state, ...state,
@ -127,7 +139,7 @@ const productReducer = (state = initialState, action) => {
currentProduct: action.payload.data, currentProduct: action.payload.data,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE: case PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE:
return { return {
...state, ...state,
@ -142,7 +154,7 @@ const productReducer = (state = initialState, action) => {
deleting: true, deleting: true,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS: case PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS:
return { return {
...state, ...state,
@ -151,7 +163,7 @@ const productReducer = (state = initialState, action) => {
totalProducts: state.totalProducts - 1, totalProducts: state.totalProducts - 1,
error: null, error: null,
}; };
case PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE: case PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE:
return { return {
...state, ...state,
@ -166,7 +178,7 @@ const productReducer = (state = initialState, action) => {
searchLoading: true, searchLoading: true,
searchError: null, searchError: null,
}; };
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS: case PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS:
return { return {
...state, ...state,
@ -175,7 +187,7 @@ const productReducer = (state = initialState, action) => {
searchQuery: action.payload.query || '', searchQuery: action.payload.query || '',
searchError: null, searchError: null,
}; };
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE: case PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE:
return { return {
...state, ...state,
@ -189,7 +201,7 @@ const productReducer = (state = initialState, action) => {
...state, ...state,
categories: action.payload, categories: action.payload,
}; };
case PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS: case PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS:
return { return {
...state, ...state,
@ -204,7 +216,7 @@ const productReducer = (state = initialState, action) => {
productError: null, productError: null,
searchError: null, searchError: null,
}; };
case PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT: case PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT:
return { return {
...state, ...state,

View File

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Link } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import Select from "react-select"; import Select from "react-select";
import { all_routes } from "../../Router/all_routes"; import { all_routes } from "../../Router/all_routes";
import { DatePicker } from "antd"; import { DatePicker } from "antd";
@ -20,15 +20,90 @@ import {
} from "feather-icons-react/build/IconComponents"; } from "feather-icons-react/build/IconComponents";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { setToogleHeader } from "../../core/redux/action"; import { setToogleHeader } from "../../core/redux/action";
import { fetchProduct } from "../../core/redux/actions/productActions";
import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { OverlayTrigger, Tooltip } from "react-bootstrap";
import ImageWithBasePath from "../../core/img/imagewithbasebath"; import ImageWithBasePath from "../../core/img/imagewithbasebath";
const EditProduct = () => { const EditProduct = () => {
const route = all_routes; const route = all_routes;
const dispatch = useDispatch(); const dispatch = useDispatch();
const { id } = useParams(); // Get product ID from URL
const data = useSelector((state) => state.toggle_header); 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 [selectedDate, setSelectedDate] = useState(new Date());
const handleDateChange = (date) => { const handleDateChange = (date) => {
setSelectedDate(date); setSelectedDate(date);
@ -58,6 +133,9 @@ const EditProduct = () => {
{ value: "choose", label: "Choose" }, { value: "choose", label: "Choose" },
{ value: "lenovo", label: "Lenovo" }, { value: "lenovo", label: "Lenovo" },
{ value: "electronics", label: "Electronics" }, { value: "electronics", label: "Electronics" },
{ value: "laptop", label: "Laptop" },
{ value: "computer", label: "Computer" },
{ value: "mobile", label: "Mobile" },
]; ];
const subcategory = [ const subcategory = [
{ value: "choose", label: "Choose" }, { value: "choose", label: "Choose" },
@ -78,6 +156,11 @@ const EditProduct = () => {
{ value: "choose", label: "Choose" }, { value: "choose", label: "Choose" },
{ value: "kg", label: "Kg" }, { value: "kg", label: "Kg" },
{ value: "pc", label: "Pc" }, { 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 = [ const sellingtype = [
{ value: "choose", label: "Choose" }, { value: "choose", label: "Choose" },
@ -114,13 +197,74 @@ const EditProduct = () => {
const handleRemoveProduct1 = () => { const handleRemoveProduct1 = () => {
setIsImageVisible1(false); 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 ( return (
<div className="page-wrapper"> <div className="page-wrapper">
<div className="content"> <div className="content">
<div className="page-header"> <div className="page-header">
<div className="add-item d-flex"> <div className="add-item d-flex">
<div className="page-title"> <div className="page-title">
<h4>Edit Product</h4> <h4>Edit Product {currentProduct?.name ? `- ${currentProduct.name}` : ''}</h4>
</div> </div>
</div> </div>
<ul className="table-top-head"> <ul className="table-top-head">
@ -211,13 +355,33 @@ const EditProduct = () => {
<div className="col-lg-4 col-sm-6 col-12"> <div className="col-lg-4 col-sm-6 col-12">
<div className="mb-3 add-product"> <div className="mb-3 add-product">
<label className="form-label">Product Name</label> <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> </div>
<div className="col-lg-4 col-sm-6 col-12"> <div className="col-lg-4 col-sm-6 col-12">
<div className="mb-3 add-product"> <div className="mb-3 add-product">
<label className="form-label">Slug</label> <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> </div>
<div className="col-lg-4 col-sm-6 col-12"> <div className="col-lg-4 col-sm-6 col-12">
@ -227,6 +391,13 @@ const EditProduct = () => {
type="text" type="text"
className="form-control list" className="form-control list"
placeholder="Enter SKU" placeholder="Enter SKU"
value={formData.sku}
onChange={(e) => {
setFormData(prev => ({
...prev,
sku: e.target.value
}));
}}
/> />
<Link <Link
to={route.addproduct} to={route.addproduct}
@ -255,7 +426,15 @@ const EditProduct = () => {
<Select <Select
className="select" className="select"
options={category} options={category}
placeholder="Lenovo" placeholder="Choose Category"
value={formData.category}
onChange={(selectedOption) => {
setFormData(prev => ({
...prev,
category: selectedOption
}));
}}
isClearable
/> />
</div> </div>
</div> </div>
@ -301,7 +480,15 @@ const EditProduct = () => {
<Select <Select
className="select" className="select"
options={brand} options={brand}
placeholder="Nike" placeholder="Choose Brand"
value={formData.brand}
onChange={(selectedOption) => {
setFormData(prev => ({
...prev,
brand: selectedOption
}));
}}
isClearable
/> />
</div> </div>
</div> </div>
@ -321,7 +508,15 @@ const EditProduct = () => {
<Select <Select
className="select" className="select"
options={unit} options={unit}
placeholder="Kg" placeholder="Choose Unit"
value={formData.unit}
onChange={(selectedOption) => {
setFormData(prev => ({
...prev,
unit: selectedOption
}));
}}
isClearable
/> />
</div> </div>
</div> </div>
@ -374,7 +569,13 @@ const EditProduct = () => {
<textarea <textarea
className="form-control h-100" className="form-control h-100"
rows={5} rows={5}
defaultValue={""} value={formData.description}
onChange={(e) => {
setFormData(prev => ({
...prev,
description: e.target.value
}));
}}
/> />
<p className="mt-1">Maximum 60 Characters</p> <p className="mt-1">Maximum 60 Characters</p>
</div> </div>
@ -474,13 +675,33 @@ const EditProduct = () => {
<div className="col-lg-4 col-sm-6 col-12"> <div className="col-lg-4 col-sm-6 col-12">
<div className="input-blocks add-product"> <div className="input-blocks add-product">
<label>Quantity</label> <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> </div>
<div className="col-lg-4 col-sm-6 col-12"> <div className="col-lg-4 col-sm-6 col-12">
<div className="input-blocks add-product"> <div className="input-blocks add-product">
<label>Price</label> <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> </div>
<div className="col-lg-4 col-sm-6 col-12"> <div className="col-lg-4 col-sm-6 col-12">

View File

@ -31,12 +31,54 @@ import {
clearProductError clearProductError
} from "../../core/redux/actions/productActions"; } 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 = () => { const ProductList = () => {
// Use new Redux structure for API data, fallback to legacy for existing functionality // Use new Redux structure for API data, fallback to legacy for existing functionality
const { const {
products: apiProducts, products: apiProducts,
loading, loading,
error error,
totalProducts,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage
} = useSelector((state) => state.products); } = useSelector((state) => state.products);
// Fallback to legacy data if API data is not available // Fallback to legacy data if API data is not available
@ -49,26 +91,46 @@ const ProductList = () => {
const [isFilterVisible, setIsFilterVisible] = useState(false); const [isFilterVisible, setIsFilterVisible] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); 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 = () => { const toggleFilterVisibility = () => {
setIsFilterVisible((prevVisibility) => !prevVisibility); setIsFilterVisible((prevVisibility) => !prevVisibility);
}; };
const route = all_routes; 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(() => { useEffect(() => {
const loadProducts = async () => { const loadProducts = async () => {
try { try {
await dispatch(fetchProducts()); const searchParams = {
// Only fetch products - categories/brands may be included in response page: currentPage,
// or can be extracted from products data pageSize: pageSize,
searchTerm: debouncedSearchTerm
};
await dispatch(fetchProducts(searchParams));
} catch (error) { } catch (error) {
console.error('Failed to load products:', error); console.error('Failed to load products:', error);
} }
}; };
loadProducts(); loadProducts();
}, [dispatch]); }, [dispatch, currentPage, pageSize, debouncedSearchTerm]);
// Handle product deletion // Handle product deletion
const handleDeleteProduct = async (productId) => { const handleDeleteProduct = async (productId) => {
@ -102,10 +164,25 @@ const ProductList = () => {
const handleSearch = (e) => { const handleSearch = (e) => {
const value = e.target.value; const value = e.target.value;
setSearchTerm(value); setSearchTerm(value);
// You can implement debounced search here // Reset to first page when searching
// For now, we'll just update the search term 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 // Clear error when component unmounts
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -163,34 +240,83 @@ const ProductList = () => {
{ {
title: "SKU", title: "SKU",
dataIndex: "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", title: "Category",
dataIndex: "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", title: "Brand",
dataIndex: "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", title: "Price",
dataIndex: "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", title: "Unit",
dataIndex: "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", title: "Qty",
dataIndex: "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> </button>
</div> </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>
</div> </div>