Save images in the MongoDB database after uploading from the React.js.
I want to save the file's path(only) along with other data like template's name, package_ids etc...
Following is the templateCreate action:
export const createTemplate = (templateData) => async (dispatch, getState) => {
try {
dispatch({ type: TEMPLATE_CREATE_REQUEST });
const {
adminLogin: { adminInfo },
} = getState();
const config = {
url: "http://localhost:8000/v1/template/create",
method: "POST",
data: templateData,
headers: {
"Content-Type": "application/json",
"x-access-token": adminInfo.data.JWToken,
},
};
const { data } = await axios(config).catch(console.error);
dispatch({ type: TEMPLATE_CREATE_SUCCESS, payload: data });
} catch (error) {
dispatch({
type: TEMPLATE_CREATE_FAILURE,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
The createTempleteReducer function:
export const templateCreateReducer = (state = {}, action) => {
switch (action.type) {
case TEMPLATE_CREATE_REQUEST:
return { loading: true };
case TEMPLATE_CREATE_SUCCESS:
return { loading: false, success: true, template: action.payload };
case TEMPLATE_CREATE_FAILURE:
return { loading: false, error: action.payload };
default:
return state;
}
};
The templateScreen:
return (
<>
<FormContainer>
<h2 className="template-h2">Create New Template</h2>
<Form onSubmit={submitHandler}>
<Form.Group controlId="preview_image">
<Form.Label>Select Preview Image</Form.Label>
<Form.Control
type="file"
accept=".png, .jpg, .jpeg .pdf"
name="preview_image"
/>
</Form.Group>
...other fields
</Form>
</FormContainer>
</>
);
The submitHandler function:
const submitHandler = (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("name", name);
formData.append("package_ids", JSON.stringify(package_ids));
formData.append("preview_image", preview_image);
formData.append("html_template", html_template);
formData.append("required_variables", required_variables);
console.log(preview_image);
const config = {
headers: {
"Content-Type": "application/json",
"x-access-token": adminInfo.data.JWToken,
},
};
dispatch(createTemplate(formData));
};
It saves the data to the database but preview_image is saved as 'C:\\fakepath\\2021_Facebook_icon.jpg' I'm not sure from where the fakepath came. and it is also not uploading the image in the uploads folder
The useEffect hook which currently just lists the package data from the database:
useEffect(() => {
dispatch(listPackages());
}, [dispatch]);
Now what to do next to save all the data to the database using useEffect and createTemplate action function. I am using multer in the backend(Node.js)
Related
I am building post like / unlike feature using React context, but I have no idea what to do in reducer to update UI. Currently when I click like / unlike button, ui doesn't update instantly, have to refresh page to see the update.
backend logic
exports.likePost = async (req, res) => {
try {
const result = await Post.findByIdAndUpdate(
req.body.postId,
{
$push: { likes: req.body.userId },
},
{ new: true }
);
return res.json(result);
} catch (err) {
console.log(err.message);
}
};
exports.unlikePost = async (req, res) => {
try {
const result = await Post.findByIdAndUpdate(
req.body.postId,
{
$pull: { likes: req.body.userId },
},
{ new: true }
);
return res.json(result);
} catch (err) {
console.log(err.message);
}
};
component
{post.likes.includes(loggedInUser._id) ? (
<IconButton
color="secondary"
component="span"
onClick={() => unlikePost(loggedInUser._id, post._id)}
>
<Like />
</IconButton>
) : (
<IconButton
color="secondary"
component="span"
onClick={() => likePost(loggedInUser._id, post._id)}
>
<Unlike />
</IconButton>
)}
context
const initialState = {
posts: [],
};
// Like post
const likePost = async (userId, postId) => {
try {
const res = await axios.put(
`/api/posts/like`,
{ userId, postId },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
dispatch({ type: "LIKE_POST", payload: res.data });
} catch (err) {
console.log(err);
}
};
// Unlike post
const unlikePost = async (userId, postId) => {
try {
const res = await axios.put(
`/api/posts/unlike`,
{ userId, postId },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
dispatch({ type: "UNLIKE_POST", payload: res.data });
} catch (err) {
console.log(err);
}
};
reducer
case "LIKE_POST":
return {
...state,
posts: // ???
),
};
case "UNLIKE_POST":
return {
...state,
posts: // ???,
};
What should be the logic for reducer?
Something like this:
case "LIKE_POST":
return {
...state,
like: action.likeValue,
};
case "UNLIKE_POST":
return {
...state,
unlike: action.unlikeValue,
};
When you want to change the value:
dispatch({ type: "LIKE_POST", likeValue: res.data });
dispatch({ type: "UNLIKE_POST", unlikeValue: res.data });
In your initial state:
const initialState = {
posts: [],
like: [],
unlike: [],
};
Here is a good explanation, which helped me: link
I'm working on a project of mine using React and Redux.
I'm trying to retrieve the token field from my auth state from my store (which contains an authentication key that I intend to send over to my server side) using useSelectorbut the thing is, that it doesn't load in time. How do I fix this?
I'll add below parts of my code that are associated with this issue:
LikeButton.js: (here is the part where I'm trying to retrieve token)
...
const LikeButton = ({ postId }) => {
const classes = useStyles();
const [isLiked, setIsLiked] = useState(false);
const isMount = useIsMount();
const dispatch = useDispatch();
const { token } = useSelector((state) => state.auth);
const { likedPostsIds } = useSelector((state) => state.likes);
useEffect(() => {
if (token) dispatch(fetchLikedPosts(token));
}, [token]);
...
likeActions.js:
...
export const fetchLikedPosts = ({ token }) => (dispatch) => {
fetch("http://localhost:8080/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: fetchLikedPostsQuery(),
})
.then((res) => res.json())
...
authActions.js
...
export const signIn = ({ password, email }) => (dispatch) => {
dispatch({
type: IS_AUTH_LOADING,
});
fetch("http://localhost:8080/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: signInQuery(password, email),
})
.then((res) => res.json())
.then((resData) => {
if (resData.errors) {
dispatch(showNotification("User authentication has failed!", ERROR));
dispatch({
type: AUTH_ERROR,
});
} else {
setLocalStorageAuth(resData.data.signIn.token, expiryDate);
dispatch({
type: SIGN_IN,
payload: resData.data.signIn,
});
}
})
.catch((err) => {
dispatch(
showNotification(
"User authentication has failed. Please try again later.",
ERROR
)
);
dispatch({
type: AUTH_ERROR,
});
emptyLocalStorage();
});
};
...
auth.js:
...
const initialState = {
token: localStorage.getItem("token"),
isAuth: localStorage.getItem("isAuth"),
expiryDate: localStorage.getItem("expiryDate"),
isLoading: false,
};
export default function authReducer(state = initialState, action) {
switch (action.type) {
case REGISTER_LOADING:
case IS_AUTH_LOADING:
return {
...state,
isLoading: true,
};
case SIGN_IN:
return {
token: action.payload.token,
isAuth: true,
isLoading: false,
};
case AUTH_ERROR:
case LOGIN_FAIL:
case REGISTER_FAIL:
case LOGOUT_SUCCESS:
return {
token: null,
isAuth: false,
isLoading: false,
};
case CREATE_USER:
return {
...state,
isLoading: false,
};
default:
return state;
}
}
...
The fetchLikedPosts function expects an object with a token key as an argument. the effect currently passes just the token as an argument. Try
useEffect(() => {
// note the change from token to {token}
if (token) dispatch(fetchLikedPosts({token}));
}, [token]);
i tried looking for similar answers to help solve my problem but i couldn't find anything using react redux hooks. This code was from a tutorial and originally written using the Context api. I wanted to trying using it with react-redux-hooks, but i got stuck. Basically i'm trying to register a user with a name, email and password, then pass these three as an object to the express server which will validated it and give me back a jwt token. Then come back to the client side and send the token to the reducer, which adds the token to localstorage and sets the state to isAuthenticated. The error i get is on the dispatch.
Dispatch
const onSubmit = e => {
e.preventDefault();
if (name === "" || email === "" || password === "") {
dispatch(setAlert("Please enter all fields", "danger"));
} else if (password !== password2) {
dispatch(setAlert("Passwords do not match", "danger"));
} else {
dispatch(register({ name, email, password })); // Error is here
}
setTimeout(() => {
dispatch(removeAlert());
}, 5000);
};
Action
export const register = async formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
try {
const res = await axios.post("/api/users", formData, config);
return {
type: "REGISTER_SUCCESS",
payload: res.data
};
} catch (err) {
return {
type: "REGISTER_FAIL",
payload: err.response.data.msg
};
}
};
Reducer
const authReducer = (
state = {
token: localStorage.getItem("token"),
isAuthenticated: null,
loading: true,
user: null,
error: null
},
action
) => {
switch (action.type) {
case "REGISTER_SUCCESS":
console.log("register success");
localStorage.setItem("token", action.payload.token);
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false
};
case "REGISTER_FAIL":
console.log("register failed");
localStorage.removeItem("token");
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
error: action.payload
};
default:
return state;
}
};
Store
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|| compose;
const store = createStore(
allReducers,
composeEnhancers(applyMiddleware(thunk))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Express server
router.post(
"/",
[
check("name", "Please a name")
.not()
.isEmpty(),
check("email", "Please include a valid email").isEmail(),
check(
"password",
"Please enter a password with 6 or more characters"
).isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const { name, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
msg: "User already exists"
});
}
user = new User({
name,
email,
password
});
// hash passsword
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
config.get("jwtSecret"),
{
expiresIn: 360000
},
(err, token) => {
if (err) throw err;
res.json({
token
});
}
);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error");
}
}
);
I believe this question has answers to the issue your experiencing here: how to async/await redux-thunk actions?
Using this example, it may look something like this (wasn't able to test it):
export const register = formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
const request = axios.post("/api/users", formData, config);
return dispatch => {
const onSuccess = success => {
dispatch({
type: "REGISTER_SUCCESS",
payload: success.data
});
return success;
};
const onError = error => {
dispatch({
type: "REGISTER_FAIL",
payload: error.response.data.msg
});
return error;
};
request.then(onSuccess, onError);
};
};
export const register = formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
return async dispatch => {
const onSuccess = success => {
dispatch({
type: "REGISTER_SUCCESS",
payload: success.data
});
return success;
};
const onError = error => {
dispatch({
type: "REGISTER_FAIL",
payload: error.response.data.msg
});
return error;
};
try {
const success = await axios.post("/api/users", formData, config);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
};
In this project I have a Redux Form with an input type='file' to upload an image to Cloudinary along with the other data upload to MongoLab. The form is reused with an item (Book) creation component (booklist\client\src\components\AddBook.js) and the one for editing a Book (booklist\client\src\components\Book.js).
The AddBookForm form. The book prop when present comes from the parent Book component. The other Fields were omitted.
class AddBookForm extends Component {
componentDidMount() {
this.handleInitialize();
}
handleInitialize() {
let names = '';
if (this.props.book) {
const authors = this.props.book.authors && this.props.book.authors.map(ath => {
let str = `${ath.firstname} ${ath.lastname}, `;
names = names + str;
});
names = names.slice(0, -2);
}
const initData = {
'title': this.props.book && this.props.book.title || '',
'pages': this.props.book && this.props.book.pages || 0,
'publisher': this.props.book && this.props.book.publisher || '',
'publishedAt': this.props.book && moment(this.props.book.publishedAt).format('MM.DD.YYYY') || '',
'releasedAt': this.props.book && moment(this.props.book.releasedAt).format('MM.DD.YYYY') || '',
'isbn13': this.props.book && this.props.book.isbn13 || '',
'cover': this.props.book && this.props.book.cover || '',
'authors': names,
book_id: this.props.book && this.props.book._id,
cloudinarySecureUrl: this.props.book && this.props.book.cloudinarySecureUrl
};
this.props.initialize(initData);
}
render() {
const { onSubmit, handleSubmit, pristine, reset, submitting } = this.props;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Field
name='cover'
type='file'
component={fileField}
label='Cover'
comment='Please provide a cover. Optional'
/>
<button
disabled={submitting}
className='add-form-action'
>
Add Book
</button>
<button type='button' disabled={pristine || submitting} onClick={reset}>
Clear Values
</button>
</form>
)
}
export default AddBookForm = reduxForm({
form: 'AddBookForm'
})(AddBookForm);
The Book component method for handling the form submission
onSubmit(formData) {
const authors = formData.authors;
const authorsToArray = [];
const authorsArray = authors.split(',');
for (let ath of authorsArray) {
const firstname = ath.trim().split(' ')[0];
const lastname = ath.trim().split(' ')[1] || '';
authorsToArray.push({
firstname,
lastname
});
}
formData.authors = authorsToArray;
this.props.addBook(formData, this.props.history);
}
This is the addBook() action to handle the Book form data upload.
export const addBook = (bookData, history) => (dispatch) => {
const cloudinaryUrl = 'https://api.cloudinary.com/v1_1/*******/upload';
const cloudinaryUploadPreset = '*******';
const formData = new FormData();
formData.append('file', bookData.cover[0]);
formData.append('upload_preset', cloudinaryUploadPreset);
axios({
url: cloudinaryUrl,
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
})
.then(res => {
bookData.cloudinarySecureUrl = res.data.secure_url;
axios.post('/api/books', bookData)
.then(res => {
history.push('/')
})
.catch(err => dispatch({
type: GET_ERRORS,
payload: err.response.data
}));
})
.catch(error => console.log('Cloudinary image upload error:', error.message));
};
What do I default bookData.cover[0] to so I can submit the form with no image for the Field name='cover'? Is there/should I take another way? The complete repo is on https://github.com/ElAnonimo/booklist
UPDATE
With this addition to the catch clause of the addBook() action on no image upload when editing a Book I'm getting the correct list of books even the image persists from when the image was uploaded.
.catch(error => {
history.push('/');
});
This is what I did in the book creation action.
// add book
export const addBook = (bookData, history) => (dispatch) => {
const cloudinaryUrl = 'https://api.cloudinary.com/v1_1/dlzbcvsbf/upload';
const cloudinaryUploadPreset = 'hvqidzpj';
const formData = new FormData();
// bookData.cover is the `FileList` array created when user submits a cover picture, bookData.cover[0] is the actual cover picture file
bookData.cover && bookData.cover[0] && formData.append('file', bookData.cover[0]);
formData.append('upload_preset', cloudinaryUploadPreset);
if (!bookData.cover[0]) {
axios.post('/api/books', bookData)
.then(res => {
history.push('/')
})
.catch(err => dispatch({
type: GET_ERRORS,
payload: err.response.data
}));
} else {
axios({
url: cloudinaryUrl,
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: formData
})
.then(res => {
// book cover isn't stored in the DB no need to send it to backend
delete bookData.cover;
bookData.cloudinarySecureUrl = res.data.secure_url;
axios.post('/api/books', bookData)
.then(res => {
history.push('/')
})
.catch(err => dispatch({
type: GET_ERRORS,
payload: err.response.data
}));
})
.catch(error => {
console.log('Cloudinary image upload error:', error.message);
history.push('/');
});
}
};
I'm trying to add toast notifications to my app, one plugin I've been trying to use is react-toastify.
The issue I'm having is probably more a general react/redux issue more than with a plugin such as react-toastify.
I'm using a reducer to set the redux state for errors and success messages, from what I understand with the current code, each error or success message is persistent in the store until another action is called to clear them.
The issue I can't figure out is how do I trigger a toast only once. Eg. I enter the wrong credentials, it creates an error toast, but whenever the state changes and reloads (typing anything into the email or password fields) it creates another toast.
How do I get it to only show once?
userActions.js
function handleErrors(res) {
if (res.ok) {
return res.json();
} else {
return res.json().then(err => {throw err;});
}
}
export const login = (user) => dispatch => {
fetch(`${url}/login`,
{
credentials: 'include',
method: 'post',
body: user,
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
})
.then(handleErrors)
.then(res =>
dispatch({
type: LOGIN,
payload: res
})
)
.catch(error =>
dispatch({
type: ERROR,
payload: error
})
)
}
userReducer.js
const initialState = {
errors: '',
success: ''
};
export default function(state = initialState, action) {
switch (action.type) {
case LOGIN:
return {
...state,
errors: '',
success: action.payload.message
};
case ERROR:
return {
...state,
success: '',
errors: action.payload.message
}
default:
return state;
}
}
app.js
app.post('/login', function(req, res) {
... return res.status(500).send({ message: 'Wrong credentials' });
... return res.status(200).send({ message: 'good!' });
});
login.js
class Login extends React.Component {
constructor() {
super();
this.state = {
email: "",
password: ""
}
}
handleChange = event => {
this.setState({
[event.target.id]: event.target.value
});
}
render() {
const { errors, login, success } = this.props;
if (success !== '') toast.success(success, {
position: toast.POSITION.TOP_CENTER
});
if (errors !== '') toast.error(errors, {
position: toast.POSITION.TOP_CENTER
});
return (
<div>
<input type="text" id="email" placeholder="Email Address" onChange={this.handleChange} />
<input type="password" id="password" placeholder="Password" onChange={this.handleChange} />
<button onClick={() => login(JSON.stringify({email: this.state.email, password: this.state.password}))}>Log In</button>
<ToastContainer />
</div>
)
}
}
const mapStateToProps = state => ({
errors: state.store.errors,
success: state.store.success
});
export default connect(mapStateToProps, {login})(Login);
You're calling toast.success or toast.error inside render which makes a new toast pop up every time you re-render the component.
The solution is simple. Move your toast calls outside render, where they will only be called once.
One way to achieve this is to return a value from your userAction.
export const login = (user) => dispatch => {
return new Promise((resolve, reject) => {
fetch(`${url}/login`,
{
credentials: 'include',
method: 'post',
body: user,
headers: new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
})
.then(handleErrors)
.then(res => {
dispatch({
type: LOGIN,
payload: res
})
resolve(res)
}
)
.catch(error => {
dispatch({
type: ERROR,
payload: error
})
reject(error)
}
)
}
}
Then use that value to toast in login.js.
class Login ... {
...
loginUser = () => {
this.props.login(JSON.stringify({email: this.state.email, password: this.state.password}))
.then(res => {
toast.success(res.message, { position: toast.POSITION.TOP_CENTER })
}
).catch(error => {
toast.error(error.message, { position: toast.POSITION.TOP_CENTER })
}
)
}
...
render() {
return (
...
<button onClick={this.loginUser}>Log In</button>
...
)
}
}
There are other ways to achieve the same functionality and depending on the structure of your project, you may want to toast in a more generalized way.