Skip to main content

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.

Product Creation Form

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;
}

FormData Construction

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 }));
  }
};
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);
    });
  }
});

Variant Form Fields

{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

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

Build docs developers (and LLMs) love