Backend DevelopmentFrontend Development

Building a Blog With Reactjs And Laravel Part12: Website Categories and Posts

Building a Blog With Reactjs And Laravel website categories and posts

In this part of this series we will display categories and posts in the website pages. I will start with categories display in navbar, footer and sidebar then i will move to posts.

 

 

 

Displaying Categories

Let’s update the Category model to include the number of posts attribute so we can display it in the sidebar of the website.

app/Category.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $appends = ['num_posts'];

    public function getNumPostsAttribute()
    {
        return $this->posts()->count();
    }

    public function posts()
    {
        return $this->hasMany(Post::class, 'category_id');
    }
}

Now the categories response include num_posts attribute. The next step is to create api handler to display all categories.

Create directory apis/ in resources/js/website/ and inside it add file Category.js and with the below code:

resources/js/website/apis/Category.js

import axios from "axios";

const Category = {
    getAll: () => {
        return axios.get('/categories?all=1');
    },
    getById: (id) => {
        return axios.get('/categories/' + id);
    }
};

export default Category;

In the above code i added functions getAll() and getById(), those are the functions we need.

There is one thing we need to think about displaying categories is that we need to display categories in multiple places like header, sidebar, and footer so we need a way to share data globally. To overcome this problem React provides a concept called “React Context“, you can read more about it in this article.

In short words Context enable us to share data to the entire application without using props so that all components can have access to these data in a concise way.

Let’s create resources/js/website/GlobalContext.js

import React from 'react';

const GlobalContext = React.createContext({categories: []});

export default GlobalContext;

In the code above i create a context for our global data using React.createContext(defaultvalue) function. The function takes one parameter which is the default value to use in this case i passed an object as the default value which have only ‘categories‘.

 

Applying the Context Using Provider

Next to use this context update resources/js/website/App.js like this:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router} from "react-router-dom";
import Routes from './Routes';
import Header from './components/partials/Header';
import Footer from './components/partials/Footer';
import GlobalContext from './GlobalContext';
import Category from './apis/Category';

export default class App extends Component {

    constructor(props)
    {
        super(props);

        this.state = {
            categories: []
        }
    }

    componentDidMount()
    {
        Category.getAll().then(response => this.setState({categories: response.data.data}));
    }

    render() {
        return (
            <GlobalContext.Provider value={{categories: this.state.categories}}>
                <Router>
                    <Header />
                    <Routes />
                    <Footer />
                </Router>
            </GlobalContext.Provider>
        );
    }
}

if (document.getElementById('app')) {
    ReactDOM.render(<App />, document.getElementById('app'));
}

As shown above to use the context you need to wrap the component that will access the global data between this <Context.Provider /> component, for the case of our application here in the render() function i enclosed it with <GlobalContext.Provider />. The component takes a value prop which is the real value to use in this case i updated the categories from this.state.categories.

 

Accessing the Context in Other Components

To access the context or in other words consume it there are multiple ways, i will stick with one way here let’s see it here in action:

Open resources/js/website/components/partials/Header.js and update it with this code:

import React from 'react';
import {Link} from "react-router-dom";
import { withRouter } from "react-router";
import GlobalContext from '../../GlobalContext';


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

        this.state = {
            category_id: ""
        };
    }

    componentDidMount()
    {
        if(this.props.location.pathname.indexOf("category") !== -1) {
            let path_parts = this.props.location.pathname.split("/");

            // category_id is at index 2
            this.setState({
                category_id: path_parts[2]
            });
        } else {
            this.setState({
                category_id: ""
            });
        }
    }

    componentDidUpdate(prevProps)
    {
        if (prevProps !== this.props) {
            if (this.props.location.pathname.indexOf("category") !== -1) {
                let path_parts = this.props.location.pathname.split("/");

                // category_id is at index 2
                this.setState({
                    category_id: path_parts[2]
                });
            } else {
                this.setState({
                    category_id: ""
                });
            }
        }
    }

    render()
    {
        return (
            <header id="top">

                <div className="row">

                    <div className="header-content twelve columns">

                        <h1 id="logo-text"><Link to="/">React Laravel Blog</Link></h1>
                        <p id="intro">Interactive website built with react and laravel</p>

                    </div>

                </div>

                <nav id="nav-wrap"><a id="toggle-btn" title="Menu" href="#">Menu</a>


                    <div className="row">

                        <ul id="nav" className="nav">
                            <li className={this.props.location.pathname=='/'?'current':''}><Link to="/">Home</Link></li>
                            {
                                this.context.categories.map(category => <li className={this.state.category_id==category.id?'current':''} key={category.id}><Link to={'/category/' + category.id + '/' + category.slug}>{ category.title }</Link></li>)
                            }
                        </ul>

                    </div>

                </nav>

            </header>
        )
    }
}

Header.contextType = GlobalContext;

export default withRouter(Header);

As you see i used contextType property to assign the context to the Header component, then i accessed the categories in the render() function using this.context.categories, Great! now let’s update Sidebar and Footer components the same way.

Do the same thing for sidebar update resources/js/website/components/partials/Sidebar.js like this:

import React from 'react';
import {Link} from "react-router-dom";
import GlobalContext from '../../GlobalContext';

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

    componentDidMount()
    {

    }

    render()
    {
        return (
            <div id="sidebar" className="four columns">
                <div className="widget widget_categories group">
                    <h3>Categories.</h3>
                    <ul>
                        {
                            this.context.categories.map(category => {
                               return category.num_posts >0 ?(
                                   <li key={category.id}><Link to={'/category/' + category.id + '/' + category.slug}>{ category.title }</Link> ({category.num_posts})</li>
                               ) : null
                            })
                        }
                    </ul>
                </div>

                <div className="widget widget_tags">
                    <h3>Post Tags.</h3>

                    <div className="tagcloud group">
                        <a href="#">Corporate</a>
                        <a href="#">Onepage</a>
                        <a href="#">Agency</a>
                        <a href="#">Multipurpose</a>
                        <a href="#">Blog</a>
                        <a href="#">Landing Page</a>
                        <a href="#">Resume</a>
                    </div>

                </div>

            </div>
        );
    }
}

Sidebar.contextType = GlobalContext;

export default Sidebar;

Also update resources/js/website/components/partials/Footer.js

import React from 'react';
import {Link} from "react-router-dom";
import GlobalContext from '../../GlobalContext';

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

    render()
    {
        return (
            <footer>

                <div className="row">

                    <div className="six columns info">

                        <h3>About US</h3>

                        <p>Interactive full featured website built with reactjs and laravel. Include admin panel and authentication system and different modules like posts, categories, tags and comments.
                        </p>
                    </div>

                    <div className="six columns">
                        <h3 className="social">Navigate</h3>

                        <ul className="navigate group">
                            <li><Link to="/">Home</Link></li>
                            {
                                this.context.categories.map(category => <li key={category.id}><Link to={'/category/' + category.id + '/' + category.slug}>{ category.title }</Link></li>)
                            }
                        </ul>
                    </div>

                </div>

            </footer>
        );
    }
}

Footer.contextType = GlobalContext;

export default Footer;

Now if you run the website it will display the categories using in the three places using only one request.

In the Header component i added another update to make the menu link active for the current category as you see by splitting this.props.location.pathname and get the id of the category from the pathname and added it to the state:

if(this.props.location.pathname.indexOf("category") !== -1) {
            let path_parts = this.props.location.pathname.split("/");

            // category_id is at index 2
            this.setState({
                category_id: path_parts[2]
            });
        } else {
            this.setState({
                category_id: ""
            });
        }

 

Displaying Tags

We need to display list of tags in the sidebar so let’s create apis first

Create Tag.js in resources/js/website/apis/ directory

import axios from "axios";

const Tag = {
    getAll: () => {
        return axios.get('/tags?all=1');
    },
    getById: (id) => {
        return axios.get('/tags/' + id);
    }
};

export default Tag;

Next go to Sidebar component in resources/js/website/components/partials/Sidebar.js and update as shown below:

import React from 'react';
import {Link} from "react-router-dom";
import GlobalContext from '../../GlobalContext';
import TagApi from '../../apis/Tag';

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

        this.state = {
            tags: []
        };
    }

    componentDidMount()
    {
        TagApi.getAll().then(response => {
           this.setState({
               tags: response.data.data
           });
        });
    }

    render()
    {
        return (
            <div id="sidebar" className="four columns">
                <div className="widget widget_categories group">
                    <h3>Categories.</h3>
                    <ul>
                        {
                            this.context.categories.map(category => {
                               return category.num_posts >0 ?(
                                   <li key={category.id}><Link to={'/category/' + category.id + '/' + category.slug}>{ category.title }</Link> ({category.num_posts})</li>
                               ) : null
                            })
                        }
                    </ul>
                </div>

                {
                    this.state.tags.length > 0? (
                        <div className="widget widget_tags">
                            <h3>Post Tags.</h3>

                            <div className="tagcloud group">
                                {
                                    this.state.tags.map(tag => {
                                        return (
                                            <Link key={tag.id} to={'/tag/' + tag.id + '/' + tag.title}>{tag.title}</Link>
                                        )
                                    })
                                }
                            </div>

                        </div>
                    ) : null
                }

            </div>
        );
    }
}

Sidebar.contextType = GlobalContext;

export default Sidebar;

As you see i called the Tag service to fetch all tags and displayed them as links to tag details route.

Displaying Posts

The next step is to display posts and this is big section in this article but nothing to worry about i will simplify things as possible. So we need to display posts in home, category page, tag page, and post details page. For the homepage i will display the recent posts, for the category and tag i will display posts for category and tag respectively.

In resources/js/website/apis/ add Post.js with the following contents:

import axios from 'axios';

const Post = {
  getRecent: () => {
      return axios.get('/posts?recent=1');
  },
  getByCategory: (id, page = 1) => {
      return axios.get('/posts?category=' + id + '&page=' + page);
  },
    getByTag: (tag, page = 1) => {
        return axios.get('/posts?tag=' + tag + '&page=' + page);
    },
    getById: (id) => {
      return axios.get('/posts/' + id);
    }
};

export default Post;

 

Home Posts

Open resources/js/website/components/pages/Home.js and modify it as follows:

import React from 'react';
import Sidebar from '../partials/Sidebar';
import Article from '../partials/Article';
import Post from '../../apis/Post';

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

        this.state = {
          posts: [],
          spinner: false
        };
    }

    componentDidMount()
    {
        this.setState({
            spinner: true
        });

        Post.getRecent().then(response => {
            this.setState({
                posts: response.data.data,
                spinner: false
            });
        });
    }

    render()
    {
        return (
            <div id="content-wrap">
                <div className="row">

                    <div id="main" className="eight columns">

                        <img src={process.env.MIX_APP_URL + 'assets/website/images/ajax-loader.gif'} style={{display: this.state.spinner==true?'block':'none'}} />
                        {
                            this.state.posts.map(post => <Article key={post.id} post={post} />)
                        }

                    </div>

                    <Sidebar/>

                </div>
            </div>
        )
    }
}

export default Home;

For the gif loader image download any loader from this site and put it in the specified location as shown above.

resources/js/website/components/partials/Article.js

import React from "react";
import {Link} from "react-router-dom";

const Article = (props) => {

    return (
        <article className="entry">

            <header className="entry-header">

                <h2 className="entry-title">
                    <Link to={`/p/${props.post.id}/${props.post.slug}` }>{props.post.title}</Link>
                </h2>

                <div className="entry-meta">
                    <ul>
                        <li>{props.post.date_formatted}</li>
                        <span className="meta-sep">•</span>
                        <li><Link to={`/category/${props.post.category.id}/${props.post.category.slug}`} title="" rel="category tag">{props.post.category.title}</Link></li>
                        <span className="meta-sep">•</span>
                        <li>{props.post.user.name}</li>
                    </ul>
                </div>

            </header>

            <div className="entry-content-media">
                <div className="post-thumb">
                    <img src={process.env.MIX_APP_URL + `uploads/${props.post.image}`} width="614" />
                </div>
            </div>

            <div className="entry-content">
                <p>{props.post.excerpt}</p>
            </div>

        </article>
    );
};

export default Article;

Displaying posts in category page

Open resources/js/website/components/pages/Category.js and update as shown below:

import React from 'react';
import Sidebar from '../partials/Sidebar';
import Article from '../partials/Article';
import { withRouter } from "react-router";
import PostApi from '../../apis/Post';
import CategoryApi from '../../apis/Category';
import Pagination from "../partials/Pagination";

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

        this.state = {
            posts: {},
            spinner: false,
            category_title: "",
            isLoaded: false
        };
    }

    componentDidMount()
    {
        this.setState({
            spinner: true,
            isLoaded: true
        });

        this.fetch();
    }

    componentDidUpdate(prevProps)
    {
        if (prevProps !== this.props && this.state.isLoaded == false) {
            this.setState({
                spinner: true,
                isLoaded: true
            });

            this.fetch();
        }
    }

    fetch() {
        CategoryApi.getById(this.props.match.params.id).then(response => this.setState({category_title: response.data.data.title}));

        PostApi.getByCategory(this.props.match.params.id).then(response => {
            this.setState({
                posts: response.data.data,
                spinner: false,
                isLoaded: false
            });
        });
    }

    onPaginate(page) {
        this.setState({
            spinner: true
        });

        PostApi.getByCategory(this.props.match.params.id, page).then(response => {
            this.setState({
                posts: response.data.data,
                spinner: false
            });
        });
    }

    render()
    {
        return (
            <div id="content-wrap">
                <div className="row">
                    <div id="main" className="eight columns">

                        <img src={process.env.MIX_APP_URL + 'assets/website/images/ajax-loader.gif'} style={{display: this.state.spinner==true?'block':'none'}} />

                        <h1>Category: { this.state.category_title }</h1>

                        {
                            this.state.posts.data && this.state.posts.data.length > 0?(
                                this.state.posts.data.map(post => <Article key={post.id} post={post} />)
                            ) : (
                                <p>No posts found</p>
                            )
                        }

                        <Pagination data={this.state.posts} onclick={this.onPaginate.bind(this)} />

                    </div>

                    <Sidebar/>

                </div>
            </div>
        )
    }
}

export default withRouter(Category);

As shown in the stuff above i loaded the posts in componentDidMount() and componentDidUpdate() life cycle hooks because when you visit specific category the componentDidMount() fired only once so if you go to another category it’s not fired and this leads to the posts not updated so to handle such situation i load the post also in componentDidUpdate() which fired every time you navigate to the component and added a simple check to compare the current props with previous props and isLoaded==false so we don’t fetch the posts twice.

resources/js/website/components/partials/Pagination.js

import React from 'react';
import PaginationItem from './PaginationItem';

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

    render()
    {
        return this.props.data && this.props.data.total > this.props.data.per_page?(
            <div className="pagenav">
                <p>
                    <PaginationItem title="Prev" page={this.props.data.current_page-1} enabled={this.props.data.current_page > 1} onclick={this.props.onclick} rel="prev" />
                    <PaginationItem title="Next" page={this.props.data.current_page+1} enabled={this.props.data.current_page < this.props.data.last_page} onclick={this.props.onclick} rel="next" />
                </p>
            </div>
        ) : null
    }
}

export default Pagination;

resources/js/website/components/partials/PaginationItem.js

import React from 'react';

const disabledStyle = {
  color: '#ccc'
};

class PaginationItem extends React.Component {

    constructor(props)
    {
        super(props);
    }

    paginate(e) {
        e.preventDefault();

        this.props.onclick(this.props.page);
    }

    render() {
        return (
            <a href="#" rel={this.props.rel} onClick={this.paginate.bind(this) } style={{color: !this.props.enabled?'#ccc':''}}>{this.props.title}</a>
        );
    }
}

export default PaginationItem;

 

Displaying posts in tag page

Update resources/js/website/components/pages/Tag.js like this:

import React from 'react';
import Sidebar from '../partials/Sidebar';
import Article from '../partials/Article';
import { withRouter } from "react-router";
import PostApi from '../../apis/Post';
import TagApi from '../../apis/Tag';
import Pagination from "../partials/Pagination";


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

        this.state = {
            posts: {},
            spinner: false,
            tag_title: "",
            isLoaded: false
        };
    }

    componentDidMount()
    {
        this.setState({
            spinner: true,
            isLoaded: true
        });

        this.fetch();
    }

    componentDidUpdate(prevProps)
    {
        if (prevProps !== this.props &&this.state.isLoaded == false) {
            this.setState({
                spinner: true,
                isLoaded: true
            });

            this.fetch();
        }
    }

    fetch() {
        TagApi.getById(this.props.match.params.id).then(response => this.setState({tag_title: response.data.data.title}));

        PostApi.getByTag(this.props.match.params.id).then(response => {
            this.setState({
                posts: response.data.data,
                spinner: false,
                isLoaded: false
            });
        });
    }

    onPaginate(page) {
        this.setState({
            spinner: true
        });

        PostApi.getByTag(this.props.match.params.id, page).then(response => {
            this.setState({
                posts: response.data.data,
                spinner: false
            });
        });
    }

    render()
    {
        console.info(this.props);
        return (
            <div id="content-wrap">
                <div className="row">
                    <div id="main" className="eight columns">

                        <img src={process.env.MIX_APP_URL + 'assets/website/images/ajax-loader.gif'} style={{display: this.state.spinner==true?'block':'none'}} />

                        <h1>Tag: {this.state.tag_title}</h1>

                        {
                            this.state.posts.data && this.state.posts.data.length > 0?(
                                this.state.posts.data.map(post => <Article key={post.id} post={post} />)
                            ) : (
                                <p>No posts found</p>
                            )
                        }

                        <Pagination data={this.state.posts} onclick={this.onPaginate.bind(this)} />

                    </div>

                    <Sidebar/>

                </div>
            </div>
        )
    }
}

export default withRouter(Tag);

Displaying post detail page

First install this package:

npm install react-html-parser

This package used to display html text in react jsx templates, we will use it to display post content.

Open and update resources/js/website/components/pages/Post.js

import React from 'react';
import {Link} from "react-router-dom";
import Sidebar from '../partials/Sidebar';
import Comments from '../partials/Comments';
import CommentForm from '../partials/CommentForm';
import PostApi from '../../apis/Post';
import ReactHtmlParser from 'react-html-parser';

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

        this.state = {
            post: {},
            spinner: false,
            isLoaded: false
        };
    }

    componentDidMount()
    {
        this.setState({
            spinner: true,
            isLoaded: true
        });

        PostApi.getById(this.props.match.params.id).then(response => this.setState({post: response.data.data, spinner: false, isLoaded: false}));
    }

    componentDidUpdate(prevProps)
    {
        if(this.props != prevProps && this.state.isLoaded == false) {
            this.setState({
                spinner: true,
                isLoaded: true
            });

            PostApi.getById(this.props.match.params.id).then(response => this.setState({post: response.data.data, spinner: false, isLoaded: false}));
        }
    }

    render()
    {
        return (
            <div id="content-wrap">
                <div className="row">
                    <div id="main" className="eight columns">

                        <img src={process.env.MIX_APP_URL + 'assets/website/images/ajax-loader.gif'} style={{display: this.state.spinner==true?'block':'none'}} />

                        {
                            this.state.post.hasOwnProperty('title')?(
                                <article className="entry">

                                    <header className="entry-header">

                                        <h2 className="entry-title">
                                            {this.state.post.title}
                                        </h2>

                                        <div className="entry-meta">
                                            <ul>
                                                <li>{this.state.post.date_formatted}</li>
                                                <span className="meta-sep">•</span>
                                                <li><Link to={`/category/${this.state.post.category.id}/${this.state.post.category.slug}`} title="" rel="category tag">{this.state.post.category.title}</Link></li>
                                                <span className="meta-sep">•</span>
                                                <li>{this.state.post.user.name}</li>
                                            </ul>
                                        </div>

                                    </header>

                                    <div className="entry-content-media">
                                        <div className="post-thumb">
                                            <img src={process.env.MIX_APP_URL + `uploads/${this.state.post.image}`} />
                                        </div>
                                    </div>

                                    <div className="entry-content">
                                        <p className="lead">{ ReactHtmlParser(this.state.post.content) }</p>
                                    </div>

                                    {
                                        this.state.post.hasOwnProperty('tags') && this.state.post.tags.length > 0? (
                                            <p className="tags">
                                                <span>Tagged in </span>:
                                                {
                                                    this.state.post.tags.map((tag, index) => <span><Link key={tag.id} to={'/tag/' + tag.id + '/' + tag.title}>{tag.title}</Link>{index!=this.state.post.tags.length-1?',':null} </span>)
                                                }
                                            </p>
                                        ) : null
                                    }

                                    <ul className="post-nav group">
                                        {
                                            this.state.post.prev_post != null?(
                                                <li className="prev"><Link rel="prev" to={'/p/' + this.state.post.prev_post.id + '/' + this.state.post.prev_post.slug}><strong>Previous Article</strong> {this.state.post.prev_post.title}</Link></li>
                                            ) : null
                                        }

                                        {
                                            this.state.post.next_post != null?(
                                                <li className="next"><Link rel="next" to={'/p/' + this.state.post.next_post.id + '/' + this.state.post.next_post.slug}><strong>Next Article</strong> {this.state.post.next_post.title}</Link></li>
                                            ) : null
                                        }
                                    </ul>

                                </article>
                            ) : null
                        }


                            <div id="comments">

                                {
                                    this.state.post.hasOwnProperty('approved_comments') && this.state.post.approved_comments.length > 0?(
                                        <div>
                                            <h3>{this.state.post.approved_comments.length} Comments</h3>

                                            <Comments comments={this.state.post.approved_comments} />
                                        </div>
                                        ): null
                                }

                                {
                                    localStorage.getItem('user.api_token') != null?<CommentForm/>:(
                                        <Link to='/login'>Login to comment</Link>
                                    )
                                }


                            </div>



                    </div>

                    <Sidebar/>

                </div>
            </div>
        )
    }
}

export default Post;

resources/js/website/components/partials/Comments.js

import React from 'react';

const Comments = (props) => {

    return (
            <ol className="commentlist">

                {
                    props.comments.map(comment =>
                        <li className="depth-1" key={comment.id}>
                            <div className="avatar">
                                <img className="avatar" src={process.env.MIX_APP_URL + 'assets/website/images/user-01.png'} alt="" width="50" height="50" />
                            </div>
                            <div className="comment-content">
                                <div className="comment-info">
                                    <cite>{comment.user.name}</cite>

                                    <div className="comment-meta">
                                        <time className="comment-time" dateTime={comment.created_at}>{comment.created_at}</time>
                                    </div>
                                </div>
                                <div className="comment-text">
                                    <p>{comment.comment}</p>
                                </div>
                            </div>
                        </li>
                    )
                }

            </ol>
    )
};

export default Comments;

 

Continue to part13: Post comments and wrapping up

 

3 1 vote
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
0

You may also like

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments