Overview
DADDO’s product management system allows you to create detailed product listings with multiple images, variants (colors/sizes), pricing tiers, and real-time stock tracking. Products are organized by categories and support Cloudinary image uploads.
Creating Products
Create Product Action
The product creation is handled by the createProduct action in src/Redux/actions/Products/create_product.js:
export const createProduct = ( formData , token ) => {
return async ( dispatch ) => {
try {
const response = await api . post ( "/products" , formData , {
headers: {
"Content-Type" : "multipart/form-data" ,
},
});
dispatch ({ type: CREATE_PRODUCT , payload: response . data });
return response . data ;
} catch ( error ) {
console . error ( "Error creando producto:" , error . response ?. data || error . message );
throw error ;
}
};
};
Products are created using FormData to support multi-file image uploads with proper encoding for the API.
The Create_Product component (src/Views/createProduct.jsx) provides a comprehensive form with the following fields:
Basic Information
Product name (required)
Description
Color
Category selection
Pricing & Stock
Sale price (required)
Purchase price (required)
Stock quantity (required)
Images
Multiple image uploads
Image preview gallery
Remove individual images
Variants
Color variations
Size options
Per-variant pricing
Per-variant images
Product Fields
Required Fields
const [ product , setProduct ] = useState ({
name: "" ,
category: "" ,
description: "" ,
color: "" ,
images: [],
price: "" ,
stock: "" ,
buyPrice: "" ,
variants: [],
});
Validation:
if ( ! product . name || ! product . price || ! product . stock || ! product . buyPrice ) {
toast . error ( "Faltan campos obligatorios" );
return ;
}
The form data is constructed with all product details before submission:
const formData = new FormData ();
formData . append ( "name" , product . name );
formData . append ( "description" , product . description || "" );
formData . append ( "color" , product . color || "" );
formData . append ( "price" , Number ( product . price ));
formData . append ( "buyPrice" , Number ( product . buyPrice ));
formData . append ( "stock" , Number ( product . stock ));
formData . append ( "categoryId" , categoryId );
// Attach main product images
product . images . forEach (( file ) => formData . append ( "images" , file ));
Image Management
Image Upload Handler
The system supports multiple image uploads with preview functionality:
const handleChange = ( e ) => {
const { name , value , files } = e . target ;
if ( name === "images" ) {
const newFiles = Array . from ( files );
const combinedFiles = [ ... product . images , ... newFiles ]. filter (
( file , index , self ) => index === self . findIndex (( f ) => f . name === file . name )
);
setProduct (( prev ) => ({ ... prev , images: combinedFiles }));
setImagePreviews ( combinedFiles . map (( f ) => URL . createObjectURL ( f )));
} else {
setProduct (( prev ) => ({ ... prev , [name]: value }));
}
};
Image Preview Gallery
Images are displayed in a grid with removal options:
{ imagePreviews . length > 0 && (
< div className = "grid grid-cols-3 sm:grid-cols-4 gap-2 mt-3 mb-2" >
{ imagePreviews . map (( src , index ) => (
< div key = { index } className = "relative" >
< img src = { src } alt = { `preview- ${ index } ` } className = "w-full h-24 object-cover rounded-md border" />
< button
type = "button"
onClick = { () => handleRemoveImage ( index ) }
className = "absolute top-1.5 right-1.5 bg-red-600 text-white rounded-full w-5 h-5"
>
×
</ button >
</ div >
)) }
</ div >
)}
Images are uploaded to Cloudinary when the product is created. The API handles the cloud upload and returns the secure URLs.
Product Variants
Adding Variants
Variants allow products to have different colors, sizes, prices, and stock levels:
const addVariant = () => {
setProduct (( prev ) => ({
... prev ,
variants: [ ... prev . variants , {
color: "" ,
size: "" ,
stock: "" ,
price: "" ,
buyPrice: "" ,
images: []
}],
}));
setVariantPreviews (( prev ) => [ ... prev , []]);
};
Variant Data Structure
Each variant can have its own attributes:
color : Color name or description
size : Size designation (S, M, L, XL, etc.)
stock : Quantity available for this specific variant
price : Override the base product price (optional)
buyPrice : Purchase cost for this variant (optional)
images : Variant-specific product images
Variant Payload Construction
const variantsPayload = product . variants . map (( v ) => ({
color: v . color || "" ,
size: v . size || "" ,
stock: v . stock ? Number ( v . stock ) : 0 ,
price: v . price ? Number ( v . price ) : null ,
buyPrice: v . buyPrice ? Number ( v . buyPrice ) : null ,
}));
formData . append ( "variants" , JSON . stringify ( variantsPayload ));
// Attach variant images
product . variants . forEach (( v , index ) => {
if ( v . images && v . images . length > 0 ) {
v . images . forEach (( file ) => {
formData . append ( `variantImages_ ${ index } ` , file );
});
}
});
{ product . variants . map (( variant , idx ) => (
< div key = { idx } className = "border p-3 mb-3 rounded bg-gray-50 relative" >
< input
type = "text"
placeholder = "Color"
value = { variant . color }
onChange = { ( e ) => handleVariantChange ( idx , "color" , e . target . value ) }
className = "w-full mb-2 p-2 border rounded"
/>
< input
type = "text"
placeholder = "Talla"
value = { variant . size }
onChange = { ( e ) => handleVariantChange ( idx , "size" , e . target . value ) }
className = "w-full mb-2 p-2 border rounded"
/>
< input
type = "number"
placeholder = "Stock"
value = { variant . stock }
onChange = { ( e ) => handleVariantChange ( idx , "stock" , e . target . value ) }
className = "w-full mb-2 p-2 border rounded"
/>
{ /* Variant-specific images */ }
< input
type = "file"
multiple
onChange = { ( e ) => handleVariantImage ( idx , e . target . files ) }
accept = "image/*"
/>
</ div >
))}
Category Management
Category Selection
Products must be assigned to a category. The system loads categories using:
import { getCategories } from "../Redux/actions/Products/get_categories" ;
useEffect (() => {
dispatch ( getCategories ( token ));
}, [ dispatch , token ]);
Creating New Categories
Users can create categories on-the-fly during product creation:
{ categories . length > 0 && ! showNewCategoryInput ? (
< div className = "flex flex-col md:flex-row md:items-center md:justify-between" >
< select
name = "category"
value = { product . category }
onChange = { handleChange }
className = "text-white bg-gray-900 border border-purple-600 rounded-md px-4 py-2"
>
< option value = "" > Selecciona una categoría </ option >
{ categories . map (( cat ) => (
< option key = { cat . id } value = { cat . id } >
{ cat . name }
</ option >
)) }
</ select >
< span
onClick = { () => setShowNewCategoryInput ( true ) }
className = "text-purple-600 text-sm font-semibold cursor-pointer hover:underline"
>
Crear categoría
</ span >
</ div >
) : (
< input
type = "text"
placeholder = "Nombre de nueva categoría"
value = { newCategory }
onChange = { ( e ) => setNewCategory ( e . target . value ) }
className = "w-full text-white bg-gray-900 border border-purple-600 rounded-lg px-4 py-2"
/>
)}
Category Creation Flow
if ( newCategory ) {
const created = await dispatch ( createCategory ( newCategory , token ));
categoryId = created . id ;
dispatch ( getCategories ( token ));
setNewCategory ( "" );
setShowNewCategoryInput ( false );
}
Product Actions
Create Product Add new products with full details
Edit Product Modify existing product information
Delete Product Remove products from inventory
Get Products Retrieve all products with filters
Filtered Product Search
Search by Category or Name
The system supports filtered product retrieval:
import { getFilteredProducts } from "../Redux/actions/Products/get_filteredProducts" ;
// Filter by category
dispatch ( getFilteredProducts ({ category: selectedCategory }));
// Search by name
if ( search . length > 2 ) {
dispatch ( getFilteredProducts ({ search }));
}
Create Another Option
The form includes a checkbox to create multiple products in succession:
const [ createAnother , setCreateAnother ] = useState ( false );
if ( createAnother ) {
// Reset form
setProduct ({
name: "" ,
category: "" ,
description: "" ,
color: "" ,
images: [],
price: "" ,
stock: "" ,
buyPrice: "" ,
variants: [],
});
setImagePreviews ([]);
setVariantPreviews ([]);
} else {
setTimeout (() => navigate ( "/products" ), 1200 );
}
When “Create another product” is checked, the form resets after successful submission instead of navigating away.
Best Practices
Upload high-quality product images
Use multiple angles for better product representation
Include variant-specific images when applicable
Images are automatically optimized by Cloudinary
Use variants for color and size options
Set variant-specific pricing when costs differ
Track stock separately for each variant
Include clear variant descriptions
Always include both buy price and sale price
Use buy price to calculate profit margins
Consider variant pricing for premium options
Update prices regularly based on costs