Building a Blog With Reactjs And Laravel Admin Comments

Building a Blog With Reactjs And Laravel Part8: Admin Comments

In this article of this series i will add comments CRUD to the React admin panel so i will begin by creating the store then integrate this store into the components.

 

 

 

Let’s start by creating the apis to interact with the server so create the api file resources/js/admin/apis/Comment.js and add this code:

import axios from 'axios';

const Comment = {
    list: (page = 1) => {
        return axios.get('/comments?page=' + page);
    },
    showOne: (id) => {
        return axios.get('/comments/' + id);
    },
    edit: (payload, id) => {
        return axios.put('/comments/' + id, payload, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token")}});
    },
    remove: (id) => {
        return axios.delete('/comments/' + id, {headers: {Authorization: 'Bearer ' + localStorage.getItem("user.api_token")}});
    }
};

export default Comment;

 

Preparing Store

Let’s create the redux store using for the comments so we will add the action types first.

Update resources/js/admin/store/actionTypes/CommentTypes.js

export const LIST_COMMENTS = 'LIST_COMMENTS';
export const LIST_COMMENTS_SUCCESS = 'LIST_COMMENTS_SUCCESS';
export const LIST_COMMENTS_FAILURE = 'LIST_COMMENTS_FAILURE';
export const EDIT_COMMENTS = 'EDIT_COMMENTS';
export const EDIT_COMMENTS_SUCCESS = 'EDIT_COMMENTS_SUCCESS';
export const EDIT_COMMENTS_FAILURE = 'EDIT_COMMENTS_FAILURE';
export const DELETE_COMMENTS = 'DELETE_COMMENTS';
export const DELETE_COMMENTS_SUCCESS = 'DELETE_COMMENTS_SUCCESS';
export const DELETE_COMMENTS_FAILURE = 'DELETE_COMMENTS_FAILURE';

Here we have little actions as we will display, edit and remove comments only.

Next we need to add the real actions in resources/js/admin/store/actions/CommentActions.js

import * as CommentTypes from '../actionTypes/CommentTypes';

import Comment from '../../apis/Comment';


/**
 * list comments action
 */
function listComments(page) {

    return function (dispatch, getState) {

        dispatch({
            type: CommentTypes.LIST_COMMENTS
        });


        // async call
        Comment.list(page)
            .then(response => dispatch({type: CommentTypes.LIST_COMMENTS_SUCCESS, data: response.data.data}))
            .catch(error => dispatch({type: CommentTypes.LIST_COMMENTS_FAILURE, error: error.response.data}));
    }
}

/**
 * edit comment action
 */
function editComment(payload, id, cb = null) {

    return function (dispatch, getState) {

        dispatch({
            type: CommentTypes.EDIT_COMMENTS
        });

        Comment.edit(payload, id)
            .then(response => {
                dispatch({type: CommentTypes.EDIT_COMMENTS_SUCCESS, data: response.data, id});

                if(cb != null) {
                    cb();
                }
            })
            .catch(error => dispatch({type: CommentTypes.EDIT_COMMENTS_FAILURE, error: error.response.data, id}));


    }
}

/**
 * delete comment action
 */
function deleteComment(id)
{
    return function (dispatch, getState) {

        dispatch({
            type: CommentTypes.DELETE_COMMENTS
        });


        Comment.remove(id)
            .then(response => dispatch({type: CommentTypes.DELETE_COMMENTS_SUCCESS, message: response.data.message, id: id}))
            .catch(error => dispatch({type: CommentTypes.DELETE_COMMENTS_FAILURE, error: error.response.data}));
    }
}

export {
    listComments,
    editComment,
    deleteComment
}

As you see in the above code we have three actions listComments, editComment, deleteComment. The missing part is to create the reducer to update the state, this is can be done in resources/js/admin/store/reducers/CommentReducer.js

import * as CommentTypes from '../actionTypes/CommentTypes';

const initialState = {
    comments: {},
    comment: {},
    success_message: "",
    error_message: "",
    validation_errors: null,
    list_spinner: false
};

const commentReducer = function (state = initialState, action) {
    let comments = {};

  switch (action.type) {
      case CommentTypes.LIST_COMMENTS:
          return {
              ...state,
              list_spinner: true
          };
      case CommentTypes.LIST_COMMENTS_SUCCESS:
          return {
              ...state,
              list_spinner: false,
              comments: action.data
          };
      case CommentTypes.LIST_COMMENTS_FAILURE:
          return {
              ...state,
              list_spinner: false,
              error_message: action.error
          };
      case CommentTypes.DELETE_COMMENTS:
          return {
              ...state,
              list_spinner: true
          };
      case CommentTypes.DELETE_COMMENTS_SUCCESS:
          comments = state.comments;
          comments.data = state.comments.data.filter(item => item.id != action.id);

          return {
              ...state,
              list_spinner: false,
              comments: comments,
              success_message: action.message,
              error_message: ''
          };
      case CommentTypes.DELETE_COMMENTS_FAILURE:
          return {
              ...state,
              list_spinner: false,
              error_message: action.error.message,
              success_message: ''
          };
      case CommentTypes.EDIT_COMMENTS:
          return {
              ...state,
              list_spinner: true
          };
      case CommentTypes.EDIT_COMMENTS_SUCCESS:
          let all_comments = state.comments;

          let comments_data = state.comments.data;

          comments_data = comments_data.map(item => {
              if(item.id == action.id) {
                  return action.data.data;
              } else {
                  return item;
              }
          });

          all_comments.data = comments_data;

          return {
              ...state,
              list_spinner: false,
              comments: all_comments,
              success_message: action.data.message,
              error_message: "",
              validation_errors: null
          };
      case CommentTypes.EDIT_COMMENTS_FAILURE:
          return {
              ...state,
              list_spinner: false,
              error_message: action.error.message,
              validation_errors: action.error.errors,
              success_message: ""
          };
      default:
          return state;
  }
};

export default commentReducer;

Now update the root reducer to add the comments. Open resources/js/admin/store/reducers/RootReducer.js and add this code:

import { combineReducers } from 'redux';

import categoryReducer  from './CategoryReducer';
import tagReducer  from './TagReducer';
import postReducer from './PostReducer';
import commentReducer from './CommentReducer';

const rootReducer = combineReducers({
   category: categoryReducer,
   tag: tagReducer,
   post: postReducer,
   comment: commentReducer
});

export default rootReducer;

We added one more item to the root reducer which is the commentReducer with an alias of ‘comment‘ you already have an idea of how to access the comments now in components.



Updating Components

Update resources/js/admin/components/pages/comments/Index.js:

import React from 'react';
import {connect} from 'react-redux';

// actions
import { listComments } from '../../../store/actions/CommentActions';

// partials
import Spinner from '../../partials/Spinner';
import Breadcrumb from '../../partials/Breadcrumb';
import Pagination from '../../partials/Pagination';
import SuccessAlert from '../../partials/SuccessAlert';
import ErrorAlert from '../../partials/ErrorAlert';
import Row from './Row';

// style
import '../../../css/comment.css';

class Index extends React.Component
{
    constructor(props)
    {
        super(props);
    }

    componentDidMount()
    {
        this.props.listComments(1);
    }

    render()
    {
        return (
            <div className="content-wrapper">
                <section className="content-header">
                    <h1>
                        Comments
                    </h1>

                    <Breadcrumb />

                </section>

                <section className="content">
                    <div className="row">
                        <div className="col-md-12">
                            <div className="box">
                                <div className="box-header">
                                    <h3 className="box-title">All comments</h3>
                                </div>

                                <div className="box-body">
                                    <Spinner show={this.props.comments.list_spinner}/>

                                    <SuccessAlert msg={this.props.comments.success_message}/>
                                    <ErrorAlert msg={this.props.comments.error_message}/>

                                    <table className="table">
                                        <thead>
                                        <tr>
                                            <th>#</th>
                                            <th>Author</th>
                                            <th width="53%">Comment</th>
                                            <th>In Response To</th>
                                            <th>Submitted on</th>
                                        </tr>
                                        </thead>
                                        <tbody>
                                        {
                                            this.props.comments.comments.data?(
                                                this.props.comments.comments.data.map(item => <Row key={item.id} comment={item} validation_errors={this.props.comments.validation_errors} />)
                                            ):null
                                        }
                                        </tbody>
                                    </table>
                                </div>

                                <Pagination data={this.props.comments.comments} onclick={this.props.listComments.bind(this)} />

                            </div>
                        </div>
                    </div>
                </section>
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => {

    return {
        comments: state.comment
    };
};

const mapDispatchToProps = (dispatch) => {

    return {
      listComments: (page) => dispatch(listComments(page))
    };
};


export default connect(mapStateToProps, mapDispatchToProps)(Index);

This component display all comments using a partial component <Row />. The <Row/> component being called inside of map() function takes a single comment and display it.

 

Create this component resources/js/admin/components/pages/comments/Row.js and add this code:

import React from 'react';
import { connect } from 'react-redux';

// actions
import { deleteComment, editComment } from '../../../store/actions/CommentActions';

// partials
import ControlButtons from './ControlButtons';
import EditForm from './EditForm';

class Row extends React.Component {

    constructor(props)
    {
        super(props);

        this.state = {
          edit_mode: false,
          comment_text: ""
        };

        this.handleDelete = this.handleDelete.bind(this);

        this.onChangeEdit = this.onChangeEdit.bind(this);

        this.handleEdit = this.handleEdit.bind(this);

        this.handleApprove = this.handleApprove.bind(this);

        this.handleUnapprove = this.handleUnapprove.bind(this);
    }

    enableEditMode() {
        this.setState({
           edit_mode: true,
            comment_text: this.props.comment.comment
        });
    }

    disableEditMode() {
        this.setState({
            edit_mode: false,
            comment_text: ""
        });
    }


    handleDelete(e) {
        e.preventDefault();

        if(confirm("Are you sure?")) {
            this.props.deleteComment(this.props.comment.id);
        }
    }

    onChangeEdit(e) {
        this.setState({
           comment_text: e.target.value
        });
    }

    handleEdit(e) {
        e.preventDefault();

        let self = this;

        this.props.editComment({comment: this.state.comment_text}, this.props.comment.id, function () {
            self.setState({
                edit_mode: false,
                comment_text: ""
            });
        });
    }

    handleApprove(e) {
        e.preventDefault();

        let self = this;

        this.props.editComment({approved: 1}, this.props.comment.id);
    }

    handleUnapprove(e) {
        e.preventDefault();

        let self = this;

        this.props.editComment({approved: 2}, this.props.comment.id);
    }

    render()
    {
        return (
            <tr className="comment-row">
                <td>
                    {this.props.comment.id}
                    {
                        this.props.comment.approved==0?(
                            <span className="badge bg-red">new</span>
                    ):null}
                </td>
                <td>
                    {this.props.comment.user.name}<br/>
                    {this.props.comment.user.email}
                </td>
                <td className="comment-content">
                    {this.props.comment.comment}
                    <ControlButtons comment={this.props.comment} onDelete={this.handleDelete} edit_mode={this.state.edit_mode} onEnableEdit={this.enableEditMode.bind(this)} onDisableEdit={this.disableEditMode.bind(this)} onApprove={this.handleApprove} onUnapprove={this.handleUnapprove} />
                    <EditForm edit_mode={this.state.edit_mode} comment_text={this.state.comment_text} onDisableEdit={this.disableEditMode.bind(this)} onChangeEdit={this.onChangeEdit} onSubmit={this.handleEdit} validation_errors={this.props.validation_errors} />
                </td>
                <td>{this.props.comment.post.title}</td>
                <td>{this.props.comment.date_formatted}</td>
            </tr>
        )
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        deleteComment: (id) => dispatch(deleteComment(id)),
        editComment: (payload, id, cb) => dispatch(editComment(payload, id, cb))
    }
};

export default connect(null, mapDispatchToProps)(Row);



The <Row/> component above displays single row in addition to that it makes use of two other partial components <ControlButtons /> and <EditForm />. The <ControlButtons />  componentdisplays buttons like approve, disapprove, edit and trash. We pass props to <ControlButtons /> such as the comment, onDelete event, edit_mode, onEnableEdit and onDisable events.

The <EditForm /> prop displays edit form so that we can edit the comment inline instead of creating a separate edit page. The component also takes props such as edit_mode, comment_text, onDisableEdit and other props.

So you can think of it as follows when we display the grid the EditForm of all rows is hidden then when the user click the edit button of any row it toggles the display of the EditForm of that row.

 

Let’s add the code of control buttons, create resources/js/admin/components/pages/comments/ControlButtons.js with this code:

import React from 'react';

class ControlButtons extends React.Component
{
    constructor(props)
    {
        super(props);

        this.handleEdit = this.handleEdit.bind(this);
    }

    handleEdit(e) {
        e.preventDefault();

        if(this.props.edit_mode == false) {
            this.props.onEnableEdit();
        } else {
            this.props.onDisableEdit();
        }
    }

    render()
    {
        return (
          <div className="comments-controls-bar">
              {
                  this.props.comment.approved == 0 || this.props.comment.approved == 2?(
                          <a href="#" className="approve-comment" onClick={this.props.onApprove}>Approve | </a>
                  ) : null
              }

              {
                  this.props.comment.approved == 0 || this.props.comment.approved == 1?(
                          <a href="#" className="unapprove-comment" onClick={this.props.onUnapprove}>Unapprove | </a>
                  ) : null
              }
              <a href="#" className="edit-comment" onClick={this.handleEdit}>Edit | </a> <a href="#" className="trash-comment" onClick={this.props.onDelete}>Trash</a>
          </div>
        );
    }
}

export default ControlButtons;

This component displays list of buttons, we add a class for each button so that we can give different style for each button. In the case of approve button i check if the the comment is pending or disapproved then i display it. Also for the disapprove button i check if the comment is pending or approved then i display it.

 

Create resources/js/admin/components/pages/comments/EditForm.js with the below code:

import React from 'react';

class EditForm extends React.Component
{
    constructor(props)
    {
        super(props);
    }

    render()
    {
        return this.props.edit_mode?(
            <form method="post" role="form" className="edit-form" onSubmit={this.props.onSubmit}>
                <textarea className={`form-control ${this.props.validation_errors!=null?'has-error':''}`} value={this.props.comment_text} onChange={this.props.onChangeEdit}></textarea>
                {
                    this.props.validation_errors!=null?(<div className="help-block has-error">{this.props.validation_errors.comment[0]}</div>):null
                }
                <button type="submit" className="btn btn-info pull-right">Update</button>
                <button type="button" className="btn btn-default" onClick={this.props.onDisableEdit}>Cancel</button>
            </form>
        ):null;
    }
}

export default EditForm;

The EditForm component has a simple form with a textarea and two buttons for saving the form or cancel. For saving the form we use the event passed as a prop from the <Row /> component which is this.props.onSubmit. Also we display the validation errors using this.props.validation_errors prop.



resources/js/admin/css/comment.css

.comments-controls-bar {
    display: none;
    margin-top: 5px;
}

.comment-row .comment-content:hover .comments-controls-bar {
    display: block;
}

.comments-controls-bar a {
    font-size: 13px;
    font-weight: bold;
}

.comments-controls-bar a.approve-comment {
    color: #00a65a;
}

.comments-controls-bar a.unapprove-comment {
    color: #d98500;
}

.comments-controls-bar a.trash-comment {
    color: red;
}

.edit-form {
    margin-top: 5px;
    margin-bottom: 5px;
}

.edit-form textarea {
    margin-bottom: 8px;
}

.edit-form textarea.has-error {
    border-color: red;
}

.edit-form div.has-error {
    color: red;
}

Now to try this in the terminal run

npm run dev

Great the comments is working now.

 

 

Continue to part9: Admin Users

 

 

0 0 vote
Article Rating
Share this: