✨ 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:
parent
9687351177
commit
f75e524565
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<h6>Test All Products API</h6>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={testApiConnection}
|
onClick={testApiConnection}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? 'Testing...' : 'Test API Connection'}
|
{loading ? 'Testing...' : 'Test Products List API'}
|
||||||
</button>
|
</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">
|
||||||
|
|||||||
@ -6,6 +6,9 @@ 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,
|
||||||
@ -44,16 +47,25 @@ const productReducer = (state = initialState, action) => {
|
|||||||
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 {
|
||||||
|
|||||||
@ -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're trying to edit doesn'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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user