Hotline:
08.8803.8803
T2-T7:
8h30 - 20h30
| CN:
8h30 - 17h00

Components: Product List – (Redux Toolkit & Ant Design )

admin . 2:59 am

I’ll help you create a demo for ListProduct with the steps you requested. Let me start by examining the current structure and then implement the changes.

Read file: package.json
Great! I can see that Redux Toolkit and Ant Design are already installed. Now let me check if there’s a Redux store setup and then implement the required functionality.

Ran tool
Read file: src/index.tsx
Now I’ll implement the complete solution step by step:

Step 1: Create Redux store and slice for products

import { configureStore } from '@reduxjs/toolkit';
import productsReducer from './slices/productsSlice';

export const store = configureStore({
  reducer: {
    products: productsReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

export interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  image: string;
  rating: {
    rate: number;
    count: number;
  };
}

interface ProductsState {
  products: Product[];
  loading: boolean;
  error: string | null;
  filteredProducts: Product[];
  filters: {
    category: string;
    minPrice: number;
    maxPrice: number;
  };
}

const initialState: ProductsState = {
  products: [],
  loading: false,
  error: null,
  filteredProducts: [],
  filters: {
    category: '',
    minPrice: 0,
    maxPrice: 1000,
  },
};

export const fetchProducts = createAsyncThunk(
  'products/fetchProducts',
  async () => {
    const response = await fetch('https://fakestoreapi.com/products');
    const data = await response.json();
    return data as Product[];
  }
);

const productsSlice = createSlice({
  name: 'products',
  initialState,
  reducers: {
    setCategoryFilter: (state, action: PayloadAction<string>) => {
      state.filters.category = action.payload;
      state.filteredProducts = state.products.filter(product => {
        const categoryMatch = !state.filters.category || 
          product.category.toLowerCase().includes(state.filters.category.toLowerCase());
        const priceMatch = product.price >= state.filters.minPrice && 
          product.price <= state.filters.maxPrice;
        return categoryMatch && priceMatch;
      });
    },
    setPriceFilter: (state, action: PayloadAction<{ min: number; max: number }>) => {
      state.filters.minPrice = action.payload.min;
      state.filters.maxPrice = action.payload.max;
      state.filteredProducts = state.products.filter(product => {
        const categoryMatch = !state.filters.category || 
          product.category.toLowerCase().includes(state.filters.category.toLowerCase());
        const priceMatch = product.price >= state.filters.minPrice && 
          product.price <= state.filters.maxPrice;
        return categoryMatch && priceMatch;
      });
    },
    clearFilters: (state) => {
      state.filters = {
        category: '',
        minPrice: 0,
        maxPrice: 1000,
      };
      state.filteredProducts = state.products;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchProducts.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchProducts.fulfilled, (state, action) => {
        state.loading = false;
        state.products = action.payload;
        state.filteredProducts = action.payload;
      })
      .addCase(fetchProducts.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message || 'Failed to fetch products';
      });
  },
});

export const { setCategoryFilter, setPriceFilter, clearFilters } = productsSlice.actions;
export default productsSlice.reducer;

Step 2: Create Redux hooks for TypeScript

import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './index';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

I need to install react-redux first. Let me check if it’s already installed and then install it if needed.

cd ./mitrade-admin && npm install react-redux @types/react-redux

Step 3: Update the main index.tsx to include Redux Provider

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import { store } from './store';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Step 4: Update ListProduct component with Redux integration and filters

import React, { useEffect, useState } from 'react';
import { Table, Input, Select, Slider, Button, Card, Row, Col, Image, Tag, Spin, Alert } from 'antd';
import { SearchOutlined, ClearOutlined } from '@ant-design/icons';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import { fetchProducts, setCategoryFilter, setPriceFilter, clearFilters, Product } from '../store/slices/productsSlice';

const { Search } = Input;
const { Option } = Select;

const ListProduct = () => {
  const dispatch = useAppDispatch();
  const { products, loading, error, filteredProducts, filters } = useAppSelector((state) => state.products);
  const [searchText, setSearchText] = useState('');

  useEffect(() => {
    dispatch(fetchProducts());
  }, [dispatch]);

  // Get unique categories for filter dropdown
  const categories = Array.from(new Set(products.map(product => product.category)));

  const handleCategoryChange = (value: string) => {
    dispatch(setCategoryFilter(value));
  };

  const handlePriceChange = (value: number[]) => {
    dispatch(setPriceFilter({ min: value[0], max: value[1] }));
  };

  const handleClearFilters = () => {
    dispatch(clearFilters());
    setSearchText('');
  };

  const handleSearch = (value: string) => {
    setSearchText(value);
  };

  // Filter products based on search text
  const searchFilteredProducts = filteredProducts.filter(product =>
    product.title.toLowerCase().includes(searchText.toLowerCase()) ||
    product.description.toLowerCase().includes(searchText.toLowerCase())
  );

  const columns = [
    {
      title: 'Image',
      dataIndex: 'image',
      key: 'image',
      width: 100,
      render: (image: string) => (
        <Image
          src={image}
          alt="Product"
          width={60}
          height={60}
          style={{ objectFit: 'cover' }}
          fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3Ik1RnG4W+FgYxN"
        />
      ),
    },
    {
      title: 'Title',
      dataIndex: 'title',
      key: 'title',
      render: (title: string) => (
        <div style={{ maxWidth: 300 }}>
          <div style={{ fontWeight: 'bold', marginBottom: 4 }}>{title}</div>
        </div>
      ),
    },
    {
      title: 'Category',
      dataIndex: 'category',
      key: 'category',
      render: (category: string) => (
        <Tag color="blue">{category}</Tag>
      ),
    },
    {
      title: 'Price',
      dataIndex: 'price',
      key: 'price',
      sorter: (a: Product, b: Product) => a.price - b.price,
      render: (price: number) => (
        <span style={{ fontWeight: 'bold', color: '#1890ff' }}>
          ${price.toFixed(2)}
        </span>
      ),
    },
    {
      title: 'Rating',
      dataIndex: 'rating',
      key: 'rating',
      sorter: (a: Product, b: Product) => a.rating.rate - b.rating.rate,
      render: (rating: { rate: number; count: number }) => (
        <div>
          <div style={{ fontWeight: 'bold' }}>{rating.rate}</div>
          <div style={{ fontSize: '12px', color: '#666' }}>({rating.count} reviews)</div>
        </div>
      ),
    },
    {
      title: 'Description',
      dataIndex: 'description',
      key: 'description',
      render: (description: string) => (
        <div style={{ maxWidth: 400 }}>
          {description.length > 100 
            ? `${description.substring(0, 100)}...` 
            : description
          }
        </div>
      ),
    },
  ];

  if (loading) {
    return (
      <div style={{ textAlign: 'center', padding: '50px' }}>
        <Spin size="large" />
        <div style={{ marginTop: 16 }}>Loading products...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div style={{ padding: '20px' }}>
        <Alert
          message="Error"
          description={error}
          type="error"
          showIcon
        />
      </div>
    );
  }

  return (
    <div style={{ padding: '20px' }}>
      <Card title="Product List" style={{ marginBottom: 20 }}>
        <Row gutter={[16, 16]} style={{ marginBottom: 20 }}>
          <Col xs={24} sm={12} md={8} lg={6}>
            <Search
              placeholder="Search products..."
              allowClear
              onSearch={handleSearch}
              style={{ width: '100%' }}
            />
          </Col>
          <Col xs={24} sm={12} md={8} lg={6}>
            <Select
              placeholder="Filter by category"
              style={{ width: '100%' }}
              allowClear
              value={filters.category || undefined}
              onChange={handleCategoryChange}
            >
              {categories.map(category => (
                <Option key={category} value={category}>
                  {category}
                </Option>
              ))}
            </Select>
          </Col>
          <Col xs={24} sm={12} md={8} lg={6}>
            <div>
              <div style={{ marginBottom: 8 }}>Price Range: ${filters.minPrice} - ${filters.maxPrice}</div>
              <Slider
                range
                min={0}
                max={1000}
                value={[filters.minPrice, filters.maxPrice]}
                onChange={handlePriceChange}
                style={{ width: '100%' }}
              />
            </div>
          </Col>
          <Col xs={24} sm={12} md={8} lg={6}>
            <Button 
              icon={<ClearOutlined />} 
              onClick={handleClearFilters}
              style={{ width: '100%' }}
            >
              Clear Filters
            </Button>
          </Col>
        </Row>
        
        <div style={{ marginBottom: 16 }}>
          <strong>Total Products:</strong> {searchFilteredProducts.length} 
          {searchFilteredProducts.length !== products.length && (
            <span style={{ color: '#666', marginLeft: 8 }}>
              (filtered from {products.length})
            </span>
          )}
        </div>
      </Card>

      <Table
        dataSource={searchFilteredProducts}
        columns={columns}
        rowKey="id"
        pagination={{
          pageSize: 10,
          showSizeChanger: true,
          showQuickJumper: true,
          showTotal: (total, range) => 
            `${range[0]}-${range[1]} of ${total} items`,
        }}
        scroll={{ x: 1200 }}
      />
    </div>
  );
};

export default ListProduct;

Let me fix the TypeScript errors:

Now let’s test the application to make sure everything works:

npm start

Perfect! I’ve successfully implemented all the requirements you requested. Here’s a summary of what I’ve created:

Step 1: Sample Ant Design Table with Product Data Structure

  • Created a comprehensive table with columns for:
  • Image: Product images with fallback
  • Title: Product titles with proper formatting
  • Category: Displayed as colored tags
  • Price: Formatted with dollar signs and sorting
  • Rating: Shows star rating and review count with sorting
  • Description: Truncated for better display

Step 2: Redux Toolkit Integration with API

  • Redux Store Setup: Created src/store/index.ts with store configuration
  • Products Slice: Created src/store/slices/productsSlice.ts with:
  • Async thunk to fetch from https://fakestoreapi.com/products
  • State management for loading, error, and data
  • TypeScript interfaces for Product data structure
  • Redux Provider: Updated src/index.tsx to wrap the app with Redux Provider
  • TypeScript Hooks: Created src/store/hooks.ts for type-safe Redux usage

Step 3: Data Binding and Filtering

  • Search Functionality: Text search across product titles and descriptions
  • Category Filter: Dropdown to filter by product category
  • Price Range Filter: Slider to filter products by price range
  • Clear Filters: Button to reset all filters
  • Real-time Filtering: Combined filters work together
  • Product Count: Shows total and filtered product counts

🎨 Additional Features Added:

  • Loading States: Spinner while fetching data
  • Error Handling: Alert component for API errors
  • Responsive Design: Mobile-friendly layout with Ant Design Grid
  • Pagination: Configurable table pagination
  • Sorting: Price and rating columns are sortable
  • Image Fallback: Handles broken image URLs gracefully

How to Use:

  1. The app will automatically fetch products when the ListProduct page loads
  2. Use the search box to find products by title or description
  3. Use the category dropdown to filter by specific categories
  4. Use the price slider to set minimum and maximum price ranges
  5. Click “Clear Filters” to reset all filters
  6. Sort by price or rating by clicking the column headers
  7. Navigate through pages using the pagination controls

The application is now running in the background and should be accessible at http://localhost:3000. Navigate to the ListProduct page to see the fully functional product listing with all the filtering capabilities you requested!