I am doing an app with express, mongodb and reactjs. In my API i have a method to create a Game. A Game has the following structure:
"name": "Hit the Jackpot",
"description": "Test your luck and win fabulous prizes in this carnival game sensation.",
"employees": ["5d713995b721c3bb38c1f5d9"],
"hours": [
{
"opening": "2023-06-07T10:00:00Z",
"closing": "2023-06-07T20:00:00Z"
},
],
"createdAt": "2023-06-07T14:00:00Z"
},
I am using multer to upload files and store them.
This is my games.js (controller)
const Game = require('../models/Game');
const Employee = require('../models/Employee');
const asyncHandler = require('../middleware/async');
const ErrorResponse = require('../utils/errorResponse');
const path = require('path');
const multer = require('multer');
// @desc Create a game
// @route POST /api/games/register
// @access Private
exports.registerGame = asyncHandler(async (req, res, next) => {
const { name, description, employees, hours } = req.body;
// Verify that the hours are valid and dont overlap
const overlappingHours = hours.some((currentHour, currentIndex) => {
return hours.slice(currentIndex + 1).some((nextHour) => {
return (
(currentHour.opening <= nextHour.opening &&
nextHour.opening < currentHour.closing) ||
(currentHour.opening < nextHour.closing &&
nextHour.closing <= currentHour.closing) ||
(nextHour.opening <= currentHour.opening &&
currentHour.closing <= nextHour.closing)
);
});
});
if (overlappingHours) {
return res.status(400).json({
success: false,
error: 'Range of hours are overlapping',
});
}
upload(req, res, async (err) => {
console.log('req.file', req.file);
console.log('err', err);
if (err) {
return next(new ErrorResponse('Problem with file upload', 500));
}
if (!req.file) {
return next(new ErrorResponse('Please upload a file', 400));
}
//Custome file name
file.name = `photo_${game._id}${path.parse(file.name).ext}`;
file.mv(`${process.env.FILE_UPLOAD_PATH}/${file.name}`, async (err) => {
if (err) {
console.error(err);
return next(new ErrorResponse(`Problem with file upload`, 500));
}
});
const game = await Game.create({
name,
description,
employees,
hours,
photo: file.name,
});
res.status(201).json({
success: true,
data: game,
});
});
});
exports.getGames = asyncHandler(async (req, res) => {
const games = await Game.find({});
res.status(200).json({
success: true,
count: games.length,
data: games,
});
});
//MULTER file upload
// Set storage engine
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, process.env.FILE_UPLOAD_PATH); // Set the destination folder for uploaded files
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(
null,
`photo_${game._id}_${uniqueSuffix}${path.extname(file.originalname)}`
); // Generate a unique filename for the uploaded file
},
});
// Create multer instance
const upload = multer({
storage: storage,
limits: {
fileSize: process.env.MAX_FILE_UPLOAD,
},
fileFilter: function (req, file, cb) {
if (file.mimetype.startsWith('image')) {
cb(null, true); // Accept the file if it is an image
} else {
cb(new Error('Please upload an image file')); // Reject the file if it is not an image
}
},
}).single('file');
And this is my form component GameForm.js
import React, { useState, useEffect } from 'react'
import FormGroup from '../UI/FormGroup'
import '../../styles/components/Auth/RegisterForm.css'
import { useDispatch, useSelector } from 'react-redux';
import { createGame } from '../../slices/gameSlice';
import { fetchEmployees } from '../../slices/employeeSlice';
function GameForm() {
const { employees: employeesList } = useSelector((state) => state.employees);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchEmployees());
}, [dispatch]);
const [formData, setFormData] = useState({
name: "",
description: "",
employees: [],
hours: [
{
opening: "",
closing: ""
}
],
photo:null
});
const { name, description, hours, employees, photo } = formData;
const onChange = e => {
if (e.target.name === 'opening' || e.target.name === 'closing') {
setFormData({
...formData,
hours: [
{
...hours[0],
[e.target.name]: e.target.value
}
]
});
} else if (e.target.name === 'employees') {
const selectedOptions = Array.from(e.target.options)
.filter((option) => option.selected)
.map((option) => option.value);
setFormData({ ...formData, [e.target.name]: selectedOptions });
} else if (e.target.name === 'photo') {
const file = e.target.files[0];
setFormData({ ...formData, [e.target.name]: file });
} else {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
};
const onSubmit = e => {
e.preventDefault();
dispatch(createGame(formData));
}
const formFields = [
{
name: "name",
type: "text",
placeholder: "Name",
label: "Name",
value: name,
onChange: onChange
},
{
name: "description",
type: "text",
placeholder: "Description",
label: "Description",
value: description,
onChange: onChange
},
{
name: "photo",
type: "file",
placeholder: "Game Photo",
label: "Game Photo",
onChange: onChange
},
{
name: "opening",
type: "datetime-local",
placeholder: "Opening Hour",
label: "Opening Hour",
value: hours[0].opening,
onChange: onChange
},
{
name: "closing",
type: "datetime-local",
placeholder: "Closing Hour",
label: "Closing Hour",
value: hours[0].closing,
onChange: onChange
},
{
name: "employees",
type: "select",
placeholder: "Employees",
label: "Employees",
value: employees,
options: employeesList.filter(employee => employee.type === 'manager').map((employee) => ({
value: employee._id,
label: `${employee.name} ${employee.lastName}`,
})),
multiple: true,
onChange: onChange
},
]
console.log(formData)
return (
<div style={{display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', width: '100%'}}>
<form className='form_container' onSubmit={onSubmit} encType='multipart/form-data' >
{formFields.map((field) => (
<FormGroup key={field.name} {...field} multiple={field.multiple} />
))}
<button type="submit">Register</button>
</form>
</div>
)
}
export default GameForm
Where i send all the data as formData and receive it in my slice.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const initialState = {
games: [],
isLoading: false,
};
export const createGame = createAsyncThunk(
'games/createGame',
async (game, { rejectWithValue }) => {
try {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const body = JSON.stringify(game);
const response = await axios.post(
'http://localhost:5000/api/games/register',
body,
config
);
return response.data.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
export const getGames = createAsyncThunk(
'games/getGames',
async (_, { rejectWithValue }) => {
try {
const response = await axios.get('http://localhost:5000/api/games');
console.log(response.data);
return response.data.data;
} catch (error) {
return rejectWithValue(error.response.data);
}
}
);
export const gameSlice = createSlice({
name: 'games',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(getGames.pending, (state) => {
state.isLoading = true;
})
.addCase(getGames.fulfilled, (state, action) => {
state.isLoading = false;
state.games = action.payload;
})
.addCase(getGames.rejected, (state) => {
state.isLoading = false;
state.games = [];
})
.addCase(createGame.pending, (state) => {
state.isLoading = true;
})
.addCase(createGame.fulfilled, (state, action) => {
state.isLoading = false;
state.games.push(action.payload);
})
.addCase(createGame.rejected, (state) => {
state.isLoading = false;
state.games = [];
});
},
});
export const {} = gameSlice.actions;
export default gameSlice.reducer;
The issue is that i am getting a 404 bad request with an error message of Please upload a file
I console log my formData before i submit the form and this i what i am sending
{name: 'sadsads', description: 'asdsadsa', employees: Array(1), hours: Array(1), photo: File}
description
:
"asdsadsa"
employees
:
['5d713995b721c3bb38c1f5d6']
hours
:
[{…}]
name
:
"sadsads"
photo
:
File {name: 'pexels-adrian-gabriel-1113927.jpg', lastModified: 1686791592156, lastModifiedDate: Wed Jun 14 2023 22:13:12 GMT-0300 (hora estándar de Argentina), webkitRelativePath: '', size: 2633645, …}
[[Prototype]]
:
Object
So i am sending a file with the name.
But my redux action createGame comes rejected
type(pin):"games/createGame/rejected"
success(pin):false
error(pin):"Please upload a file"
name(pin):"sadsads"
description(pin):"asdsadsa"
0(pin):"5d713995b721c3bb38c1f5d6"
photo(pin):
requestId(pin):"ders8C0k4SG1FZ9jzn0CR"
rejectedWithValue(pin):true
requestStatus(pin):"rejected"
aborted(pin):false
condition(pin):false
And this is my formdata that i am sending
{
"name": "sadsads",
"description": "asdsadsa",
"employees": [
"5d713995b721c3bb38c1f5d6"
],
"hours": [
{
"opening": "2023-06-01T23:12",
"closing": "2023-06-09T23:12"
}
],
"photo": {}
}
So my photo is coming as an empty object. What am i doing wrong? It is probably an issue in my slice because the formData contains a file name, but then in my redux devtools my photo is empty so it seems i am not sending anything to my backend.