This commit is contained in:
tuanOts
2025-05-25 17:15:22 +07:00
parent 6b6e2bbc8a
commit 9687351177
11 changed files with 1099 additions and 38 deletions
+7 -2
View File
@@ -6,7 +6,12 @@ const ImageWithBasePath = (props) => {
// Combine the base path and the provided src to create the full image source URL
// Handle both relative and absolute paths
let fullSrc;
if (props.src.startsWith('http')) {
// Check if src is provided and is a string
if (!props.src || typeof props.src !== 'string') {
// Use a default placeholder image if src is undefined or invalid
fullSrc = `${base_path}assets/img/placeholder.png`;
} else if (props.src.startsWith('http')) {
fullSrc = props.src;
} else {
// Ensure there's always a slash between base_path and src
@@ -38,7 +43,7 @@ const ImageWithBasePath = (props) => {
// Add PropTypes validation
ImageWithBasePath.propTypes = {
className: PropTypes.string,
src: PropTypes.string.isRequired, // Make 'src' required
src: PropTypes.string, // Allow src to be optional
alt: PropTypes.string,
height: PropTypes.number,
width: PropTypes.number,
+204
View File
@@ -0,0 +1,204 @@
import { productsApi } from '../../../services/productsApi';
// Action Types
export const PRODUCT_ACTIONS = {
// Fetch Products
FETCH_PRODUCTS_REQUEST: 'FETCH_PRODUCTS_REQUEST',
FETCH_PRODUCTS_SUCCESS: 'FETCH_PRODUCTS_SUCCESS',
FETCH_PRODUCTS_FAILURE: 'FETCH_PRODUCTS_FAILURE',
// Fetch Single Product
FETCH_PRODUCT_REQUEST: 'FETCH_PRODUCT_REQUEST',
FETCH_PRODUCT_SUCCESS: 'FETCH_PRODUCT_SUCCESS',
FETCH_PRODUCT_FAILURE: 'FETCH_PRODUCT_FAILURE',
// Create Product
CREATE_PRODUCT_REQUEST: 'CREATE_PRODUCT_REQUEST',
CREATE_PRODUCT_SUCCESS: 'CREATE_PRODUCT_SUCCESS',
CREATE_PRODUCT_FAILURE: 'CREATE_PRODUCT_FAILURE',
// Update Product
UPDATE_PRODUCT_REQUEST: 'UPDATE_PRODUCT_REQUEST',
UPDATE_PRODUCT_SUCCESS: 'UPDATE_PRODUCT_SUCCESS',
UPDATE_PRODUCT_FAILURE: 'UPDATE_PRODUCT_FAILURE',
// Delete Product
DELETE_PRODUCT_REQUEST: 'DELETE_PRODUCT_REQUEST',
DELETE_PRODUCT_SUCCESS: 'DELETE_PRODUCT_SUCCESS',
DELETE_PRODUCT_FAILURE: 'DELETE_PRODUCT_FAILURE',
// Search Products
SEARCH_PRODUCTS_REQUEST: 'SEARCH_PRODUCTS_REQUEST',
SEARCH_PRODUCTS_SUCCESS: 'SEARCH_PRODUCTS_SUCCESS',
SEARCH_PRODUCTS_FAILURE: 'SEARCH_PRODUCTS_FAILURE',
// Categories and Brands
FETCH_CATEGORIES_SUCCESS: 'FETCH_CATEGORIES_SUCCESS',
FETCH_BRANDS_SUCCESS: 'FETCH_BRANDS_SUCCESS',
// Clear States
CLEAR_PRODUCT_ERROR: 'CLEAR_PRODUCT_ERROR',
CLEAR_CURRENT_PRODUCT: 'CLEAR_CURRENT_PRODUCT',
};
// Action Creators
// Fetch all products
export const fetchProducts = (params = {}) => async (dispatch) => {
dispatch({ type: PRODUCT_ACTIONS.FETCH_PRODUCTS_REQUEST });
try {
const data = await productsApi.getAllProducts(params);
dispatch({
type: PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch products',
});
throw error;
}
};
// Fetch single product
export const fetchProduct = (id) => async (dispatch) => {
dispatch({ type: PRODUCT_ACTIONS.FETCH_PRODUCT_REQUEST });
try {
const data = await productsApi.getProductById(id);
dispatch({
type: PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch product',
});
throw error;
}
};
// Create product
export const createProduct = (productData) => async (dispatch) => {
dispatch({ type: PRODUCT_ACTIONS.CREATE_PRODUCT_REQUEST });
try {
const data = await productsApi.createProduct(productData);
dispatch({
type: PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to create product',
});
throw error;
}
};
// Update product
export const updateProduct = (id, productData) => async (dispatch) => {
dispatch({ type: PRODUCT_ACTIONS.UPDATE_PRODUCT_REQUEST });
try {
const data = await productsApi.updateProduct(id, productData);
dispatch({
type: PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to update product',
});
throw error;
}
};
// Delete product
export const deleteProduct = (id) => async (dispatch) => {
dispatch({ type: PRODUCT_ACTIONS.DELETE_PRODUCT_REQUEST });
try {
await productsApi.deleteProduct(id);
dispatch({
type: PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to delete product',
});
throw error;
}
};
// Search products
export const searchProducts = (query, params = {}) => async (dispatch) => {
dispatch({ type: PRODUCT_ACTIONS.SEARCH_PRODUCTS_REQUEST });
try {
const data = await productsApi.searchProducts(query, params);
dispatch({
type: PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to search products',
});
throw error;
}
};
// Fetch categories
export const fetchCategories = () => async (dispatch) => {
try {
const data = await productsApi.getCategories();
dispatch({
type: PRODUCT_ACTIONS.FETCH_CATEGORIES_SUCCESS,
payload: data,
});
return data;
} catch (error) {
console.error('Failed to fetch categories:', error);
throw error;
}
};
// Fetch brands
export const fetchBrands = () => async (dispatch) => {
try {
const data = await productsApi.getBrands();
dispatch({
type: PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
console.error('Failed to fetch brands:', error);
throw error;
}
};
// Clear error
export const clearProductError = () => ({
type: PRODUCT_ACTIONS.CLEAR_PRODUCT_ERROR,
});
// Clear current product
export const clearCurrentProduct = () => ({
type: PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT,
});
+10 -1
View File
@@ -1,6 +1,9 @@
import { combineReducers } from '@reduxjs/toolkit';
import initialState from "./initial.value";
import productReducer from './reducers/productReducer';
const rootReducer = (state = initialState, action) => {
// Legacy reducer for existing functionality
const legacyReducer = (state = initialState, action) => {
switch (action.type) {
case "Product_list":
return { ...state, product_list: action.payload };
@@ -66,4 +69,10 @@ const rootReducer = (state = initialState, action) => {
}
};
// Combine reducers
const rootReducer = combineReducers({
legacy: legacyReducer,
products: productReducer,
});
export default rootReducer;
+220
View File
@@ -0,0 +1,220 @@
import { PRODUCT_ACTIONS } from '../actions/productActions';
const initialState = {
// Products list
products: [],
totalProducts: 0,
currentPage: 1,
totalPages: 1,
// Current product (for edit/view)
currentProduct: null,
// Search results
searchResults: [],
searchQuery: '',
// Categories and brands
categories: [],
brands: [],
// Loading states
loading: false,
productLoading: false,
searchLoading: false,
// Error states
error: null,
productError: null,
searchError: null,
// Operation states
creating: false,
updating: false,
deleting: false,
};
const productReducer = (state = initialState, action) => {
switch (action.type) {
// Fetch Products
case PRODUCT_ACTIONS.FETCH_PRODUCTS_REQUEST:
return {
...state,
loading: true,
error: null,
};
case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS:
return {
...state,
loading: false,
products: action.payload.data || action.payload,
totalProducts: action.payload.total || action.payload.length,
currentPage: action.payload.currentPage || 1,
totalPages: action.payload.totalPages || 1,
error: null,
};
case PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
// Fetch Single Product
case PRODUCT_ACTIONS.FETCH_PRODUCT_REQUEST:
return {
...state,
productLoading: true,
productError: null,
};
case PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS:
return {
...state,
productLoading: false,
currentProduct: action.payload,
productError: null,
};
case PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE:
return {
...state,
productLoading: false,
productError: action.payload,
};
// Create Product
case PRODUCT_ACTIONS.CREATE_PRODUCT_REQUEST:
return {
...state,
creating: true,
error: null,
};
case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS:
return {
...state,
creating: false,
products: [action.payload, ...state.products],
totalProducts: state.totalProducts + 1,
error: null,
};
case PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE:
return {
...state,
creating: false,
error: action.payload,
};
// Update Product
case PRODUCT_ACTIONS.UPDATE_PRODUCT_REQUEST:
return {
...state,
updating: true,
error: null,
};
case PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS:
return {
...state,
updating: false,
products: state.products.map(product =>
product.id === action.payload.id ? action.payload.data : product
),
currentProduct: action.payload.data,
error: null,
};
case PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE:
return {
...state,
updating: false,
error: action.payload,
};
// Delete Product
case PRODUCT_ACTIONS.DELETE_PRODUCT_REQUEST:
return {
...state,
deleting: true,
error: null,
};
case PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS:
return {
...state,
deleting: false,
products: state.products.filter(product => product.id !== action.payload),
totalProducts: state.totalProducts - 1,
error: null,
};
case PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE:
return {
...state,
deleting: false,
error: action.payload,
};
// Search Products
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_REQUEST:
return {
...state,
searchLoading: true,
searchError: null,
};
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS:
return {
...state,
searchLoading: false,
searchResults: action.payload.data || action.payload,
searchQuery: action.payload.query || '',
searchError: null,
};
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE:
return {
...state,
searchLoading: false,
searchError: action.payload,
};
// Categories and Brands
case PRODUCT_ACTIONS.FETCH_CATEGORIES_SUCCESS:
return {
...state,
categories: action.payload,
};
case PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS:
return {
...state,
brands: action.payload,
};
// Clear States
case PRODUCT_ACTIONS.CLEAR_PRODUCT_ERROR:
return {
...state,
error: null,
productError: null,
searchError: null,
};
case PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT:
return {
...state,
currentProduct: null,
productError: null,
};
default:
return state;
}
};
export default productReducer;