I have an Add Product form. In this form, there is an Upload Image field to upload jpg and png from your computer. Anyway it doesn’t work and products get uploaded without image. My aim is to convert it to a URL field which I can paste product image URLs.
You can find the code snippets from before I started working on below.
Client side AddProduct.js component:
const AddProduct = props => {
const {
user,
productFormData,
formErrors,
productChange,
addProduct,
brands,
image
} = props;
const handleSubmit = event => {
event.preventDefault();
addProduct();
};
return (
<div className='add-product'>
<form onSubmit={handleSubmit} noValidate>
<Row>
<Col xs='12' lg='6'>
<Input
type={'text'}
error={formErrors['sku']}
label={'Sku'}
name={'sku'}
placeholder={'Product Sku'}
value={productFormData.sku}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
<Col xs='12' lg='6'>
<Input
type={'text'}
error={formErrors['name']}
label={'Name'}
name={'name'}
placeholder={'Product Name'}
value={productFormData.name}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
<Col xs='12' md='12'>
<Input
type={'textarea'}
error={formErrors['description']}
label={'Description'}
name={'description'}
placeholder={'Product Description'}
value={productFormData.description}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
<Col xs='12' lg='6'>
<Input
type={'number'}
error={formErrors['quantity']}
label={'Quantity'}
name={'quantity'}
decimals={false}
placeholder={'Product Quantity'}
value={productFormData.quantity}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
<Col xs='12' lg='6'>
<Input
type={'number'}
error={formErrors['price']}
label={'Price'}
name={'price'}
min={1}
placeholder={'Product Price'}
value={productFormData.price}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
<Col xs='12' md='12'>
<SelectOption
error={formErrors['taxable']}
label={'Taxable'}
name={'taxable'}
options={taxableSelect}
value={productFormData.taxable}
handleSelectChange={value => {
productChange('taxable', value);
}}
/>
</Col>
<Col xs='12' md='12'>
<SelectOption
disabled={user.role === ROLES.Merchant}
error={formErrors['brand']}
name={'brand'}
label={'Select Brand'}
value={
user.role === ROLES.Merchant ? brands[1] : productFormData.brand
}
options={brands}
handleSelectChange={value => {
productChange('brand', value);
}}
/>
</Col>
<Col xs='12' md='12'>
<Input
type={'file'}
error={formErrors['file']}
name={'image'}
label={'file'}
placeholder={'Please Upload Image'}
value={image}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
<Col xs='12' md='12' className='my-2'>
<Switch
id={'active-product'}
name={'isActive'}
label={'Active?'}
checked={productFormData.isActive}
toggleCheckboxChange={value => productChange('isActive', value)}
/>
</Col>
</Row>
Client side Input.js component:
const _onChange = e => {
if (e.target.name == 'image') {
onInputChange(e.target.name, e.target.files[0]);
} else {
onInputChange(e.target.name, e.target.value);
}
};
Client side Product reducer:
const initialState = {
products: [],
storeProducts: [],
product: {
_id: ''
},
storeProduct: {},
productsSelect: [],
productFormData: {
sku: '',
name: '',
description: '',
quantity: 1,
price: 1,
image: {},
isActive: true,
taxable: { value: 0, label: 'No' },
brand: {
value: 0,
label: 'No Options Selected'
}
},
isLoading: false,
productShopData: {
quantity: 1
},
formErrors: {},
editFormErrors: {},
shopFormErrors: {},
};
const productReducer = (state = initialState, action) => {
switch (action.type) {
// ... Other cases
case ADD_PRODUCT:
return {
...state,
products: [...state.products, action.payload]
};
case RESET_PRODUCT:
return {
...state,
productFormData: {
sku: '',
name: '',
description: '',
quantity: 1,
price: 1,
image: {},
isActive: true,
taxable: { value: 0, label: 'No' },
brand: {
value: 0,
label: 'No Options Selected'
}
},
product: {
_id: ''
},
formErrors: {}
};
default:
return state;
}
};
export default productReducer;
Client side Product actions:
export const addProduct = () => {
return async (dispatch, getState) => {
try {
const rules = {
sku: 'required|alpha_dash',
name: 'required',
description: 'required|max:200',
quantity: 'required|numeric',
price: 'required|numeric',
taxable: 'required',
image: 'required',
brand: 'required'
};
const product = getState().product.productFormData;
const user = getState().account.user;
const brands = getState().brand.brandsSelect;
const brand = unformatSelectOptions([product.brand]);
const newProduct = {
sku: product.sku,
name: product.name,
description: product.description,
price: product.price,
quantity: product.quantity,
image: product.image,
isActive: product.isActive,
taxable: product.taxable.value,
brand:
user.role !== ROLES.Merchant
? brand !== 0
? brand
: null
: brands[1].value
};
const { isValid, errors } = allFieldsValidation(newProduct, rules, {
'required.sku': 'Sku is required.',
'alpha_dash.sku':
'Sku may have alpha-numeric characters, as well as dashes and underscores only.',
'required.name': 'Name is required.',
'required.description': 'Description is required.',
'max.description':
'Description may not be greater than 200 characters.',
'required.quantity': 'Quantity is required.',
'required.price': 'Price is required.',
'required.taxable': 'Taxable is required.',
'required.image': 'Please upload files with jpg, jpeg, png format.',
'required.brand': 'Brand is required.'
});
if (!isValid) {
return dispatch({ type: SET_PRODUCT_FORM_ERRORS, payload: errors });
}
const formData = new FormData();
if (newProduct.image) {
for (const key in newProduct) {
if (newProduct.hasOwnProperty(key)) {
if (key === 'brand' && newProduct[key] === null) {
continue;
} else {
formData.set(key, newProduct[key]);
}
}
}
}
const response = await axios.post(`${API_URL}/product/add`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const successfulOptions = {
title: `${response.data.message}`,
position: 'tr',
autoDismiss: 1
};
if (response.data.success === true) {
dispatch(success(successfulOptions));
dispatch({
type: ADD_PRODUCT,
payload: response.data.product
});
dispatch(resetProduct());
dispatch(goBack());
}
} catch (error) {
handleError(error, dispatch);
}
};
};
Server side Product API route:
const { s3Upload } = require('../../utils/image');
const storage = multer.memoryStorage();
const upload = multer({ storage });
router.post('/add', auth, role.check(ROLES.Admin, ROLES.Merchant), upload.single('image'), async (req, res) => {
try {
const sku = req.body.sku;
const name = req.body.name;
const description = req.body.description;
const quantity = req.body.quantity;
const price = req.body.price;
const taxable = req.body.taxable;
const isActive = req.body.isActive;
const brand = req.body.brand;
const image = req.file;
if (!sku) {
return res.status(400).json({ error: 'You must enter product SKU.' });
}
if (!description || !name) {
return res.status(400).json({ error: 'You must enter product name and description.' });
}
if (!quantity) {
return res.status(400).json({ error: 'You must enter a quantity.' });
}
if (!price) {
return res.status(400).json({ error: 'You must enter a price.' });
}
const foundProduct = await Product.findOne({ sku });
if (foundProduct) {
return res.status(400).json({ error: 'This SKU is already in use.' });
}
const { imageUrl, imageKey } = await s3Upload(image);
const product = new Product({
sku,
name,
description,
quantity,
price,
taxable,
isActive,
brand,
imageUrl,
imageKey
});
const savedProduct = await product.save();
res.status(200).json({
success: true,
message: `Product has been successfully added!`,
product: savedProduct
});
} catch (error) {
return res.status(400).json({
error: 'Your request could not be processed. Please try again.'
});
}
}
);
Server side image.js utils file:
exports.s3Upload = async image => {
try {
let imageUrl = '';
let imageKey = '';
if (image) {
const params = {
Key: image.originalname,
Body: image.buffer,
ContentType: image.mimetype
};
const s3Upload = await s3bucket.upload(params).promise();
imageUrl = s3Upload.Location;
imageKey = s3Upload.key;
}
return { imageUrl, imageKey };
} catch (error) {
return { imageUrl: '', imageKey: '' };
}
};
Server side Product database schema:
const Mongoose = require('mongoose');
const slug = require('mongoose-slug-generator');
const { Schema } = Mongoose;
const options = {
separator: '-',
lang: 'en',
truncate: 120
};
Mongoose.plugin(slug, options);
const ProductSchema = new Schema({
sku: { type: String },
name: { type: String, trim: true },
slug: { type: String, slug: 'name', unique: true },
imageUrl: { type: String },
imageKey: { type: String },
description: { type: String, trim: true },
quantity: { type: Number },
price: { type: Number },
taxable: { type: Boolean, default: false },
isActive: { type: Boolean, default: true },
brand: { type: Schema.Types.ObjectId, ref: 'Brand', default: null },
updated: Date,
created: { type: Date, default: Date.now }
});
module.exports = Mongoose.model('Product', ProductSchema);
I made the following modifications in the code.
Client side AddProduct.js component: (Set the input type to text and changed file and image values to imageUrl for matching the database)
<Col xs='12' md='12'>
<Input
type={'text'}
error={formErrors['imageUrl']}
name={'imageUrl'}
label={'Image'}
placeholder={'Please Enter Product Image URL'}
value={productFormData.imageUrl}
onInputChange={(name, value) => {
productChange(name, value);
}}
/>
</Col>
Client side Input.js component: (Removed if else statement related to image and file)
const _onChange = e => {
onInputChange(e.target.name, e.target.value);
};
Client side Product reducer: Changed image: {} to imageUrl: '' in productFormData since it’s accepting string now)
const initialState = {
productFormData: {
sku: '',
name: '',
description: '',
quantity: 1,
price: 1,
imageUrl: '',
isActive: true,
taxable: { value: 0, label: 'No' },
brand: {
value: 0,
label: 'No Options Selected'
}
}
}
const productReducer = (state = initialState, action) => {
switch (action.type) {
// ... Other cases
case RESET_PRODUCT:
return {
...state,
productFormData: {
sku: '',
name: '',
description: '',
quantity: 1,
price: 1,
imageUrl: '',
isActive: true,
taxable: { value: 0, label: 'No' },
brand: {
value: 0,
label: 'No Options Selected'
}
},
product: {
_id: ''
},
formErrors: {}
};
Client side Product actions: (Removed image.required rules and changed image to imageUrl)
export const addProduct = () => {
return async (dispatch, getState) => {
try {
const rules = {
sku: 'required|alpha_dash',
name: 'required',
description: 'required|max:200',
quantity: 'required|numeric',
price: 'required|numeric',
taxable: 'required',
brand: 'required'
};
const product = getState().product.productFormData;
const user = getState().account.user;
const brands = getState().brand.brandsSelect;
const brand = unformatSelectOptions([product.brand]);
const newProduct = {
sku: product.sku,
name: product.name,
description: product.description,
price: product.price,
quantity: product.quantity,
imageUrl: product.imageUrl,
isActive: product.isActive,
taxable: product.taxable.value,
brand:
user.role !== ROLES.Merchant
? brand !== 0
? brand
: null
: brands[1].value
};
const { isValid, errors } = allFieldsValidation(newProduct, rules, {
'required.sku': 'Sku is required.',
'alpha_dash.sku':
'Sku may have alpha-numeric characters, as well as dashes and underscores only.',
'required.name': 'Name is required.',
'required.description': 'Description is required.',
'max.description':
'Description may not be greater than 200 characters.',
'required.quantity': 'Quantity is required.',
'required.price': 'Price is required.',
'required.taxable': 'Taxable is required.',
'required.brand': 'Brand is required.'
});
if (!isValid) {
return dispatch({ type: SET_PRODUCT_FORM_ERRORS, payload: errors });
}
const formData = new FormData();
if (newProduct.imageUrl) {
for (const key in newProduct) {
if (newProduct.hasOwnProperty(key)) {
if (key === 'brand' && newProduct[key] === null) {
continue;
} else {
formData.set(key, newProduct[key]);
}
}
}
}
const response = await axios.post(`${API_URL}/product/add`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const successfulOptions = {
title: `${response.data.message}`,
position: 'tr',
autoDismiss: 1
};
if (response.data.success === true) {
dispatch(success(successfulOptions));
dispatch({
type: ADD_PRODUCT,
payload: response.data.product
});
dispatch(resetProduct());
dispatch(goBack());
}
} catch (error) {
handleError(error, dispatch);
}
};
};
Server side Product API route: (Removed upload.single('image') and const { imageUrl, imageKey } = await s3Upload(image);, changed const image = req.file; to const imageUrl = req.body.imageUrl;)
// Add product
router.post('/add', auth, role.check(ROLES.Admin, ROLES.Merchant), async (req, res) => {
try {
const sku = req.body.sku;
const name = req.body.name;
const description = req.body.description;
const quantity = req.body.quantity;
const price = req.body.price;
const taxable = req.body.taxable;
const isActive = req.body.isActive;
const brand = req.body.brand;
const imageUrl = req.body.imageUrl;
if (!sku) {
return res.status(400).json({ error: 'You must enter product SKU.' });
}
if (!description || !name) {
return res.status(400).json({ error: 'You must enter product name and description.' });
}
if (!quantity) {
return res.status(400).json({ error: 'You must enter a quantity.' });
}
if (!price) {
return res.status(400).json({ error: 'You must enter a price.' });
}
const foundProduct = await Product.findOne({ sku });
if (foundProduct) {
return res.status(400).json({ error: 'This SKU is already in use.' });
}
// ... ...
Completely removed image.js utils file
Didn’t modify anything in Product database schema
The above reveals my current code but now it doesn’t get my input values (product name, description, etc – not only imageUrl) and saves an empty form to the database. It’s weird because I didn’t change anything about other form fields. I’m probably missing something. Any help will be appreciated!