7

Creating a Forum CMS with React.js and Ruby on Rails

 3 years ago
source link: https://hackernoon.com/creating-a-forum-cms-with-reactjs-and-ruby-on-rails-oz4t35ql
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Creating a Forum CMS with React.js and Ruby on Rails

@Aaron RoryAaron Newbold

Full-Stack Developer - JavaScript, React, Ruby, Rails. Portfolio: https://aaronrory.com

For this project, we will be using React.js, Ruby on Rails, React-Quill, Cloudinary, and Action Mailer to build a forum whose content is manageable by the site's users.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Features included in this Forum CMS:

Admin Panel

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Can disable a user's ability to post
  • Can disable a user's ability to comment on posts
  • Can promote a user to administrator (3 levels)

Forum Handling

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Create forums
  • Create Subforums (at least 1 level deep)
  • Rename forums and subforums
  • Remove forums and subforums (and all associated posts)
  • Create administrative only forums
    - Only administrators of a certain level can post on these forums
    - Allow the forum to be viewed only by administrators

Post Handling

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Create Format-Rich Topics using React-Quill
  • Edit Topics
  • Pin Topics
  • Lock Posts to prevent additional comments
  • Comment on Topics
  • Comment on Comments
  • Remove Topics

Profile Handling

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Profile image uploads using Cloudinary
  • Account Activation by email confirmation
  • Password changing
  • Password resetting by email confirmation

Entity Relationship Diagram (ERD) And Association Planning

Planning out the different tables, their fields, and their various associations beforehand helps a lot in guiding and keeping your database focused.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Chart built using website: LucidChart.com

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Users Table

Fields needed are,

username 
(string)
,
password_digest 
(string),
email 
(string)
,
is_activated 
(bool)
,
activation_key 
(string)
,
token 
(string)
,
admin_level 
(integer)
,
can_post_date 
(datetime)
,and
can_comment_date 
(datetime)
.The fields
id 
(integer)
and
created_at 
(timestamps)
are automatically created by Rails.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Our user will need a username as a means of identification. The email address will be used to contact them to send over an activation_key which will hold a URL link to set the is_activated flag on the user account (Account verification by email).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The token field will hold a random string which we will use for session persistence and checking if the user is still logged in.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The password_reset_token field will also hold a random string which we will use for resetting a user's password once requested.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Both the token_date and password_reset_date fields will be used to check whether a token is valid or has expired.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The admin_level field will span between 4 integers, 0 - 3.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Level 3 (Site Owner)

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Cannot be modified by any other moderator
  • Can remove or add Moderators
  • Can disable a user's ability to comment and create topics
  • Can create and edit Forums and subForums

Level 2 (Moderator)

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Can remove or add Moderators
  • Can create and edit Forums and subForums
  • Can disable a user's ability to comment and create topics

Level 1 (Forums Moderator)

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Can create and edit Forums and subForum
  • Can disable a user's ability to comment and create topics

Level 0 (Basic User)

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  • Can create and edit their own posts
  • Can comment on posts

Fields can_post_date and can_comment_date will hold DateTime values that determine the expiration date of a user's communications suspension.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

For example, if the date and time stored in any of the fields are greater than the current date and time then that user is unable to communicate using such a medium, and vice versa, if the date and time stored in these fields have already passed then the user's communications are no longer suspended.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Forums Table

Fields needed are,

name 
(string),
  admin_only 
(bool),
and 
admin_view_only 
(bool)
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The name field is simply an identifier for the main forum.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The admin_only field determines whether or not this forum only allows posts by administrators.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The admin_view_only field once check is coupled with the admin_only field, setting both to true and this prevents this forum from being seen or viewed by basic user accounts.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Subforums Table

Fields needed are

name 
(string), and foreign key 
forum_id
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The name field will hold the string identifier of the subforum, and the forum_id holds the id of the forum this subforum is a child of.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Posts Table

Fields needed are

title 
(string), 
body 
(text), 
is_pinned 
(bool), and 
is_locked 
(bool)
, along with foreign keys
forum_id
, 
subforum_id
,
and 
author_id
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The title field holds the title of the post and the body field holds the text content of the post.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The field is_pinned determines whether or not a post is sectioned as a pinned Post and takes top priority in the order of presentation when posts are listed.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The is_locked field determines whether or not a post can be commented on.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This table is linked to the Forum's Table through the foreign key forum_id and also to the Subforums' Table through the foreign key subforum_id. These fields hold a direct record object association in Ruby on Rails allowing for some pretty useful commands, which we will get into later when setting up the Back-end.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This table is linked to the User's Table through the foreign key author_id.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Comments Table

Fields needed in this table are

body 
(text) and foreign keys 
author_id
, 
comment_id
, and 
post_id
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The body field simply holds the text of the comment, the author_id holds the id of the user who wrote the comment, and post_id references the post the comment was written under.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The comment_id field allows comments to be written in response to another comment, essentially commenting on comments. The limit in our case is that we will allow commenting on comments but replying to comment only links the initial comment that was replied to.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

For example, Jane comments on a Post, and John replies to Jane's comment. Ron then replies to John's comment, which is in response to Jane's comment. Let's say Jane's comment has an id of 1 (*id not comment_id), then John's comment_id will be 1 referencing Jane's comment, and Ron's comment_id will also be 1, referencing Jane's comment.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Setting up the Front-end

We will build a stand-alone front-end with dummy data in the form of .js files to provide a demo of the features and allow client-based logic testing of these features.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

However, ultimately once the back-end is built, we will switch out the dummy data for the actual API queried database data.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Create React App

Since we will be creating a React application, we will use the quick start guide provided by Create React App.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ npx create-react-app forum-cms
$ cd forum-cms

"forum-cms" is just what I decided to call my app, you can name it whatever you want.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Install Needed Dependencies

PropTypes
Runtime type checking for React props and similar objects.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

You can use prop-types to document the intended types of properties passed to components. React will check props passed to your components against those definitions, and warn in development if they don’t match.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ npm i prop-types

React-Router-DOM
Allows the use of URL routes to load the different App components

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ npm i react-router-dom

AxiosUsed for API fetch requests

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ npm i axios

React-QuillA simple free-to-use rich-text editor, this is what handles runtime text editing. (Bolding, italicizes, underlings, heading, etc...)

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ npm i react-quill

Code Linter - ESLint
This isn't a necessary dependency, but I like to use code linters to catch various errors that may break the program, errors I may have overlooked. Code Linters also enforced code standardization.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Inside The package.json File

/package.json
found within the base directory of your project folder
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Your file should now look similar to this:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
{
  "name": "react-cmsblog",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "axios": "^0.20.0",
    "prop-types": "^15.7.2",
    "react": "^16.14.0",
    "react-dom": "^16.14.0",
    "react-quill": "^1.3.5",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.3"
  },

React and Dependency versions used in build noted above*

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Organizing Folder and File Structure

I tend to create sub-folders to organize my apps based on relativity. So here's what a basic layout of what my create react app folder structure looks like within the Visual Studio Code Integrated Development Environment (IDE).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Within the "src" folder, I created an "assets" folder with two sub-folders, "CSS" and "images".

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then I created a "components" folder with two sub-folders, "functional" and "presentational".

Lastly, I created a folder titled "tests" and afterward, I moved all files into their relative folders.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

After moving all the files around, you may have to change the import locations of the files within the index.js and App.js files

0 reactions
heart.png
light.png
money.png
thumbs-down.png

index.js

change from:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import './index.css'; // line 3

to:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import './assets/css/index.css'; // line 3

App.js

0 reactions
heart.png
light.png
money.png
thumbs-down.png

change from:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import logo from './logo.svg'; // line 2
import './App.css'; // line 3

to:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import logo from './assets/images/logo.svg'; // line 2
import './assets/css/App.css'; // line 3

Now the application should be able to run without errors.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Using History To Ensure The Page Scroll resets on each new page link

Okay, I've noticed after creating a few React pages and clicking through between the pages, upon entrance into a new page, the scroll position from the previous page is transferred over to the new page.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

In order to fix that, we will create a Scroll reset(ScrollToTop) component and place that component within the initial render of the ReactDOM function alongside the App component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/misc/pageScrollReset.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import { useEffect } from 'react';
import { withRouter } from 'react-router-dom';

function ScrollToTop({ history }) {
  useEffect(() => {
    const unlisten = history.listen(() => {
      window.scrollTo(0, 0);
    });
    return () => {
      unlisten();
    };
  });

  return (null);
}

export default withRouter(ScrollToTop);

The history prop passed to the ScrollToTop component is one of React-Router-DOM's dependencies that allows viewing React-Apps browsing history.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

So within this ScrollToTop component, we call the history.listen action, which is a callback method that listens to location changes, and we supply the

window.scrollTo(0, 0)
code, which occurs on each location change noted.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

We then wrap this code within a useEffectHook, which on render/mount, checks the browsing location history, and on unmount cancels out history listening by calling the unlisten function expression.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/index.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import './assets/css/index.css';
import App from './App';
import ScrollToTop from './components/misc/pageScrollReset'; // Scrolls page to top on route switch

ReactDOM.render(
  <Router>
    <ScrollToTop />
    <StrictMode>
      <App />
    </StrictMode>
  </Router>,
  document.getElementById('root'),
);

I created a stand-alone version of the Front-end before moving on and developing the Back-end API, but for smooth transitional purposes, I will come back to the Front-end after I explain the making of the Back-end API.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Setting up the Back-end API

Now for the easy part...( ͡° ͜ʖ ͡°)

0 reactions
heart.png
light.png
money.png
thumbs-down.png

We will start by creating a new rails application.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails new forum-cms --database=postgresql --api -T

Take note whenever you see the $ symbol, we are typing the presented text into the command line.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This line creates a new rails application named "forum-cms" using PostgreSQL as the database (including the "pg" gem), setting the is API flag and removing MiniTest asthe default testing framework.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Gems to Install

All the below gems should be added or already included in your Gemfile:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'
gem 'cloudinary'
gem 'rubocop', '~>0.81.0'

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'

group :development, :test do
  gem 'rspec-rails', '~> 3.5'
end

group :test do
  gem 'database_cleaner'
  gem 'factory_bot_rails', '~> 4.0'
  gem 'faker'
  gem 'shoulda-matchers', '~> 3.1'
end

After adding all the necessary gems into the Gemfile (Generally found at the root directory). Run the "bundle install" command into the terminal:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ bundle

"bundle" is the shorthand for "bundle install".

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Models

User Model

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Let's start by generating a model and its migrations using the rails generator.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails g model user username:string password_digest:string email:string is_activated:boolean activation_key:string token:string token_date:datetime password_reset_token:string password_reset_date:datetime admin_level:integer can_post_date:datetime can_comment_date:datetime

Now in the user.rb model

/app/models/user.rb
we will add the following code for validations and associations.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
class User < ApplicationRecord
  has_secure_password
  has_many :posts, inverse_of: 'author', dependent: :destroy
  has_many :comments, dependent: :destroy
  validates :username, length: { in: 4..32 }, presence: true,
                       uniqueness: { case_sensitive: false }
  validates :password, length: { minimum: 8 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :admin_level, numericality: { only_integer: true,
                                          less_than_or_equal_to: 3 }
  before_save { username.downcase! }
  before_save { email.downcase! }
end
has_secure_password
is a method supplied by the bcrypt gem used to set and authenticate a password attribute.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
has_many
allows us to set up model associations; in this case, one(1) user can have many posts and comments. The
dependent: :destroy
flag causes all associated posts and comments to be destroyed along with the user. And inverse_of will allow us to rename our association when coupled with the belongs_to code we will place in the
post.rb
model
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The regular expression set as a frozen constant, VALID_EMAIL_REGEX is explained here: REGEXR.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Also, take note of the before_save functions used. Before saving each user record, I take the values for both username and email and make sure they are entirely downcased. This preventing needing the use of ILIKE (POSTGRESQL search method for finding records without checking for case sensitivity) coupled with the where function later on when designing the login authentication system.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Check out the Rails Guide for an overview of all of the validations used.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Forum Model

Generate forum.rb model:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails g model forum name:string admin_only:boolean admin_only_view:boolean

Now in the forum.rb we will add validations and associations

app/models/forum.rb
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
class Forum < ApplicationRecord
  has_many :subforums, dependent: :destroy
  has_many :posts, dependent: :destroy
  validates :name, length: { in: 3..32 }, presence: true,
                   uniqueness: { case_sensitive: false }
  before_save { name.downcase! }

  # Grabs all posts without a subforum, while also limiting the amount posts retrieved
  def subforum_posts(per_page = 10, page = 1)
    offset = (page * per_page) - per_page
    retrieved_posts = posts.where(subforum_id: nil)
                           .offset(offset).limit(per_page)

    Forum.truncate_posts(retrieved_posts)
  end

  # Truncates posts title and body attribute returning a new array
  def self.truncate_posts(posts)
    returned_posts = []
    posts.each do |post|
      new_post = post.as_json(only: %i[id user_id is_pinned created_at])
      new_post['title'] = post.title.slice(0..30)
      new_post['body'] = post.body.slice(0..32)
      new_post['author'] = post.author.username
      new_post['subforum'] = post.subforum.name if post.subforum.present?
      new_post['forum'] = post.forum.name
      returned_posts.push(new_post)
    end

    returned_posts
  end

  def self.forum_all_json
    returned_json = []
    Forum.all.each do |forum|
      new_forum = forum.as_json
      new_forum['subforums'] = forum.subforums.as_json(only: %i[id name])

      returned_json.push(new_forum)
    end

    returned_json
  end
end

The

subforum_posts(subform)
method grabs all posts linked to the current forum without a subforum_id. These posts are then paginated and truncated.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The

self.truncate_posts(posts)
method simply returns a new Hash object with a shortened title and body of a post record along with the name of the author, subforum, and forum. The self. part of the method name ensures that it is a Class Method accessible by invoking the name of the class first to call it. EG.
Forum.truncate_posts(posts)
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The

self.forum_all_json
method simply grabs the resulting array of records from the
Forum.all
query and returns a new array with custom packed Hashes including the forum's subforum associations as keys.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Subforum Model

Generate forum.rb model:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails g model subforum name:string forum:belongs_to

Now in the subforum.rb we will add validations and associations

app/models/subforum.rb
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
class Subforum < ApplicationRecord
  belongs_to :forum
  has_many :posts, dependent: :destroy
  validates :name, length: { in: 3..32 }, presence: true,
                   uniqueness: { case_sensitive: false }
  before_save { name.downcase! }

  # Grabs all posts by subforum, while also limiting the amount of posts retrieved
  def subforum_posts(per_page = 10, page = 1)
    offset = (page * per_page) - per_page
    retrieved_posts = posts.offset(offset).limit(per_page)

    Forum.truncate_posts(retrieved_posts)
  end
end

The same logic used in the Forum model is used here:

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Post Model

Generate model and migrations:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails g model post title:string body:text forum:belongs_to subforum:belongs_to is_pinned:boolean is_locked:boolean user:belongs_to

After this migration is generated, we will need to change a line relating to the subforums belongs_to data type.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
class CreatePosts < ActiveRecord::Migration[6.0]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :body
      t.belongs_to :forum, null: false, foreign_key: true
      t.belongs_to :subforum, null: true
      t.boolean :is_pinned, default: false
      t.boolean :is_locked, default: false
      t.belongs_to :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end

We set

null: true
and remove the
foreign_key: true
argument allowing us to accept null values for the subforum_id foreign key and eliminating the Database Level Foreign Key CHECK constraint.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Code added in post.rb

/app/models/post.rb
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
class Post < ApplicationRecord
  belongs_to :forum
  belongs_to :subforum, optional: true
  belongs_to :author, class_name: 'User', foreign_key: 'user_id'
  has_many :comments, dependent: :destroy
  validates :title, length: { in: 3..48 }, presence: true
  validates :body, length: { in: 8..20_000 }, presence: true
  scope :pins, -> { where('is_pinned = true') }
  scope :not_pinned, -> { where('is_pinned = false') }

  def post_json
    new_post = attributes
    new_post['author'] = author.username
    new_post['subforum'] = subforum.name if subforum.present?
    new_post['forum'] = forum.name
    new_post['admin_only'] = forum.admin_only
    new_post['admin_only_view'] = forum.admin_only_view
    new_post
  end

  def self.author_posts_json(posts_array)
    returned_posts = []
    posts_array.each do |post|
      new_post = post.as_json(only: %i[id user_id is_pinned created_at])
      new_post['title'] = post.title.slice(0..30)
      new_post['body'] = post.body.slice(0..32)
      new_post['author'] = post.author.username
      new_post['subforum'] = post.subforum.name if post.subforum.present?
      new_post['forum'] = post.forum.name
      new_post['admin_only'] = post.forum.admin_only
      new_post['admin_only_view'] = post.forum.admin_only_view
      returned_posts.push(new_post)
    end

    returned_posts
  end

  def self.author_comments_json(comments_array)
    returned_comments = []
    comments_array.each do |comment|
      new_comment = comment.as_json
      new_comment['author'] = comment.author.username
      new_comment['admin_only'] = comment.post.forum.admin_only
      new_comment['admin_only_view'] = comment.post.forum.admin_only_view
      new_comment['server_date'] = DateTime.now
      returned_comments.push(new_comment)
    end

    returned_comments
  end

  def self.pins_json
    results = []
    all_pins = Post.pins
    all_pins.each do |p|
      new_post = p.post_json
      results.push(new_post)
    end

    results
  end
end

The

belongs_to
:author
line is the inverse of the
has_many
line in the user.rb model.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The

belongs_to :subforum, optional: true
method line allows a post to have a null id for its subforum_id without an error.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The defined scopes allow us to grab all like records from the controller that match the criteria presented in the where method clause.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

All of the methods in the Post model follow the same logic as the methods in the Forum model.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Comment Model

Generate comment.rb model:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails g model comment body:text user:references post:references comment:references

Now in the comment.rb file

app/models/comment.rb
0 reactions
heart.png
light.png
money.png
thumbs-down.png
class Comment < ApplicationRecord
  belongs_to :author, class_name: 'User', foreign_key: 'user_id'
  belongs_to :post
  belongs_to :comment, optional: true
  has_many :comments, dependent: :destroy
  validates :body, length: { in: 2..400 }, presence: true

  def comment_json
    new_comment = attributes
    new_comment['author'] = author.username
    new_comment
  end

  def self.author_comments_json(comments_array)
    returned_comments = []
    comments_array.each do |comment|
      new_comment = comment.as_json
      new_comment['post_author'] = comment.post.author.username
      new_comment['post_title'] = comment.post.title
      new_comment['forum'] = comment.post.forum.name
      new_comment['subforum'] = comment.post.subforum if comment.post.subforum.present?
      new_comment['author'] = comment.author.username
      returned_comments.push(new_comment)
    end

    returned_comments
  end
end

Nothing new here, so moving along.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Controllers

Registrations Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This controller will handle user creation when a user signs up for a new account. I won't go over every single method created in these controllers but just touch on why and what some of the more interesting ones were made to do (Check out the GitHub repository for the entire codebase).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Let's define some general helper methods first that we will use in a few of this controller's methods.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  private

  def register_params
    # whitelist params
    params.require(:user)
          .permit(:username, :email, :password, :password_confirmation)
  end

  def password_params
    # whitelist params
    params.require(:user)
          .permit(:password, :password_confirmation)
  end

These methods essentially create a hash based on the permitted symbol values while also grabbing these values from client-submitted parameters.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
EG. register_params
generates...
0 reactions
heart.png
light.png
money.png
thumbs-down.png
{ username: params[:user][:username],
  email: params[:user][:email],
  password: params[:user][:password],
  password_confirmation: [:user][:password_confirmation] }

And this method simply returns that hash.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Create method

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The create method in the registrations controller will handle new user registrations.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Register a new user account
  def create
    user = User.create!(register_params)
    new_activation_key = generate_token(user.id, 62)
    user.update_attribute(:admin_level, 3) if User.all.size <= 1
    if user.update_attribute(:activation_key, new_activation_key)
      ActivationMailer.with(user: user).welcome_email.deliver_now
    end
    json_response({ message: 'Account registered but activation required' },
                  :created)
  end

The basic idea of the create route and how it should function is, a user wants to create an account, so we receive their preferred account credentials through the register_params method and create a new User record.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Since we want email confirmation (scroll down below to section about setting up Action mailer for more info on that), upon creating this new User record, we also generate an email_confirmation_token/activation_key (check down below for setting up the Application controller) then we store this value on the User's record and also send the value along with a route for account activation to the User's related email address given.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The line where it updates the User's

:admin_level
to 3 is set to happen only if this is the first user created in the database to give this user administrative rights.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then ultimately, a JSON response is rendered, and the message: 'Account registered by activation required' is sent to the client.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Activate Account method

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This method works in tandem with the create method, a GET route leading to this method is sent in the email to confirm a User's account.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Link used in account activation email
  def activate_account
    # Set url variable to the front-end url
    url = 'https://arn-forum-cms.netlify.app/login'
    user = User.find(params[:id])

    if user.activation_key == params[:activation_key]
      user.update_attribute(:is_activated, true)
    end

    # json_response(message: 'Successfully activated account')
    redirect_to url
  end

It checks the

:activation_key
parameter supplied is the same as the one saved in the User's record, then updates the
:is_activated
attribute, and afterward redirects the user to the login page on the client end.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Forgot Password method

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now let's look at a similar chain of methods that also deals and uses tokens.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Generate password reset token and send to account's associated email
  def forgot_password
    user = User.find_by(email: params[:email])
    if user
      new_token = generate_token(user.id, 32, true)
      if user.update_attribute(:password_reset_token, new_token)
        user.update_attribute(:password_reset_date, DateTime.now)
        ActivationMailer.with(user: user).password_reset_email.deliver_now
      else
        json_response({ errors: user.errors.full_messages }, 401)
      end
    end
    json_response({ message: 'Password reset information sent to associated account.' })
  end

Follows the same logic as the

email_confirm_token
/
activation_key
. A new token is generated and stored on the User record; then, an email is sent out to the confirmed email address containing a link to the route that verifies the token.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Link used in account password reset email
  def password_reset_account
    # Set url variable to the front-end url
    reset_token = params[:password_reset_token]
    url = "https://arn-forum-cms.netlify.app/reset_password?token=#{reset_token}"

    redirect_to url
  end

This is the method used in the password reset email, which redirects to the

/reset_password
page on the client (front-end) while also supplying the reset_token as a parameter.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now for the final method used in the password_reset chain

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Change a user's password if they have a password reset token
  def change_password_with_token
    token = params[:password_reset_token]
    user = User.find_by(password_reset_token: token) if token.present?
    if user
      # Check if token is still valid
      return json_response({ message: 'Token expired' }, 400) if user.password_token_expired?

      if user.update(password_params)
        user.update_attribute(:password_reset_token, nil)
        json_response({ message: 'Password changed successfully' })
      else
        json_response({ errors: user.errors.full_messages }, 400)
      end
    else
      json_response({ errors: 'Invalid Token' }, 401)
    end
  end

Once the reset_token is received by the front-end client, and the user is redirected to the reset_password page through the email link. It is used here in this method to allow the user to reset their password using the password_params method to gather their new password values. The password_reset token is checked for date validity through the User model method added here:

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now in the user.rb file

app/models/user.rb
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def password_token_expired?
    offset = (Time.zone.now - password_reset_date).round
    offset / 1.hours >= 1 # Token expires after 1 hour
  end

After all the checks run through, the user's password is updated to the newly given password parameters.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Application Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Here is where we will store helper methods used frequently in multiple controllers.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
class ApplicationController < ActionController::API
  include Response
  include ExceptionHandler
  include TokenGenerator
  include CompareDates

  # Determine if user is authenticated
  def authorized_user?
    json_response({ errors: 'Account not Authorized' }, 401) unless current_user
  end

  # Determine if user is authenticated administrator
  def authorized_admin?
    authorized_user?
    json_response({ errors: 'Insufficient Administrative Rights' }, 401) unless @current_user.admin_level.positive?
  end

  private

  # Sets a global @current_user variable if possible
  def current_user
    return nil unless access_token.present?

    @current_user ||= User.find_by(token: access_token)
    return nil unless @current_user
    return nil if token_expire?(@current_user.token_date)

    @current_user
  end

  # Determines if token is expired based on the amount of time between the token_date and server date
  # Default expiration date is 1 day after creation
  def token_expire?(token_date, days = 1, hours = 24, minutes = 0, seconds = 0)
    date_diff = compare_dates(token_date)

    if date_diff[:days] >= days && date_diff[:hrs] >= hours &&
       date_diff[:mins] >= minutes && date_diff[:secns] >= seconds
      true
    end

    false
  end

  # Grabs the token placed in the HTTP Request Header, "Authorization"
  def access_token
    request.headers[:Authorization]
  end
end

Pretty straightforward here, but notice the included modules at the top of the Application Controller?

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Let's take a look at each one...

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Include Response

0 reactions
heart.png
light.png
money.png
thumbs-down.png
module Response
  def json_response(object, status = :ok)
    render json: object, status: status
  end
end

A simple module that takes two(2) arguments, an object, and a status,andthen returns the rails render method in JSON format using the arguments given as its options.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Include ExceptionHandler

0 reactions
heart.png
light.png
money.png
thumbs-down.png
module ExceptionHandler
  extend ActiveSupport::Concern

  included do
    rescue_from ActiveRecord::RecordNotFound do |e|
      json_response({ errors: e.message }, :not_found)
    end

    rescue_from ActiveRecord::RecordInvalid do |e|
      json_response({ errors: e.message }, :unprocessable_entity)
    end
  end
end

Some information here on ActiveSupport::Concern(Here's some additional info Here).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

In short, extending

ActiveSupport::Concern
allows us access to the included method which essentially evaluates the given code block in the context of the base class.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

So here's an inheritance chain of what happens in each controller since all controllers inherit from the Application Controller.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

EG.

RegistrationsController 
-> 
ApplicationController 
-> 
CompareDates 
-> 
TokenGenerator 
-> 
ExceptionHandler 
-> 
Response 
-> 
ActionController::API
  -> ....

You can see the entire inheritance link by typing in the rails console:
0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ RegistrationsController.ancestors

Include CompareDates

0 reactions
heart.png
light.png
money.png
thumbs-down.png
# Compares two dates and returns a hash with the difference in seconds, minutes, hours, and days
module CompareDates
  def compare_dates(date1, date2 = DateTime.now)
    diff_secns = date2.to_time - date1.to_time
    diff_mins = (diff_secns / 60).round
    diff_hrs = (diff_mins / 60).round
    diff_days = (diff_hrs / 24).round
    diff_text = "#{diff_days} day/s, #{diff_hrs % 24} hour/s, #{diff_mins % 60} minute/s, #{(diff_secns % 60).round} second/s"

    { diff_string: diff_text,
      days: diff_days, hrs: diff_hrs,
      mins: diff_mins, secns: diff_secns }
  end
end

This method shows how much time is left for a person that has had their communications suspended by an administrator before the suspension is retracted.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Include TokenGenerator

0 reactions
heart.png
light.png
money.png
thumbs-down.png
module TokenGenerator
  def generate_token(user_id, token_size = 32, url_safe = false)
    random_ascii = [
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '!', '@', '#', '$', '%', '^', '&',
      '*', '(', ')', '-', '_', '+', '|', '~', '=',
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
      'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
      'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
    ]
    url_safe_ascii = [
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, '!', '@', '#', '$',
      '*', '(', ')', '-', '_', '~',
      'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
      'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
      'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
    ]

    token = [user_id]

    (1..token_size - 1).each do
      token.push(random_ascii.sample) unless url_safe
      token.push(url_safe_ascii.sample) if url_safe
    end

    token.join('')
  end
end

The method we use and will use to generate the tokens for email confirmation, password reset confirmation, and user authentication after logging in.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

In order to ensure the tokens are always unique, instead of using a loop that checks if the randomly generated token already exists within the database, I prepend the user's id to each generated token.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Sessions Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This controller will handle allowing a user who has already signed up and confirmed their email address to log in.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
class SessionsController < ApplicationController
  before_action :authorized_user?, except: :create

  # When a user attempts to log in
  def create
    user = User.where(username: params[:user][:username].downcase)
               .or(User.where(email: params[:user][:email].downcase))
               .first

    return json_response({ errors: 'Incorrect login credentials' }, 401) unless user

    authenticate_user(user)
  end

  # When a user logs out
  def destroy
    @current_user.update(token: nil)
    json_response(user: { logged_in: false })
  end

  # Checks if a user is still logged in
  def logged_in
    json_response(user: user_status(@current_user))
  end

  private

  # Returns a Hash with additional keys for Front-End use
  def user_status(user)
    user_with_status = user.as_json(only: %i[id username is_activated
                                             token admin_level can_post_date
                                             can_comment_date])
    user_with_status['logged_in'] = true
    user_with_status['can_post'] = DateTime.now > user.can_post_date
    user_with_status['can_comment'] = DateTime.now > user.can_comment_date

    user_with_status
  end

  # Returns user Hash after successful authentication
  def authenticate_user(user)
    if user.try(:authenticate, params[:user][:password])
      return unless activated(user)

      new_token = generate_token(user.id)
      if user.update_attribute(:token, new_token)
        user.update_attribute(:token_date, DateTime.now)
        json_response(user: user_status(user))
      else
        json_response({ errors: user.errors.full_messages }, 401)
      end
    else
      json_response({ errors: 'Incorrect login credentials' }, 401)
    end
  end

  # Checks to make sure a user has confirmed their email address
  def activated(user)
    unless user.is_activated
      json_response({ errors: ['Account not activated'] }, 401)
      return false
    end

    true
  end
end

Fairly straightforward authentication procedure here...

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The Create method ensures that a user exists based on the given username or email.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Afterward, the

authenticate_user(user)
method ensures that the password given is linked to the user found, while also calling upon the
activated(user)
method to ensure that the user has also confirmed their email address.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then, if all checks out, the user is given a new login token which is saved to their record, and a JSON string is returned.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

I would also like to point out the

before_action :authorized_user?
line. This simply calls the
authorized_user?
method we created in the Application Controller before every method within the Sessions Controller.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

This method and its variant

(
authorized_admin?
)
will be used in all of the controllers to ensure that the person calling these routes/methods are users who are actually logged into the forum.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Forum Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Holds all of the forum handling methods for creation, updating, deletion, and displaying.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
class ForumsController < ApplicationController
  before_action :authorized_admin?, only: %i[create update destroy]
  before_action :set_forum, only: %i[update destroy]
  before_action :set_page_params, only: %i[index show_by_forum show_by_subforum]

  # Shows all Forum records and appends their posts and subforums by
  # adding new keys after converting to a hash
  def index
    all_forums = []
    Forum.all.each do |forum|
      new_forum = forum.attributes
      new_forum['posts'] = forum.subforum_posts(@per_page, @page)
      new_forum['subforums'] = return_subforums(forum, @per_page, @page)
      all_forums.push new_forum
    end

    json_response(results: { forums: all_forums, pinned_posts: Post.pins_json,
                             per_page: @per_page, page: @page })
  end

  # Shows all Forum records and their related subforums
  def index_all
    json_response(forums: Forum.forum_all_json)
  end

  # Similar process to the index method but only shows one Forum record
  # along with its Subforums and their posts
  def show_by_forum
    forum = Forum.find_by(name: params[:forum])
    selected_forum = forum.attributes
    selected_forum['posts'] = forum.subforum_posts(@per_page, @page)
    selected_forum['subforums'] = return_subforums(forum, @per_page, @page)

    json_response(results: { forum: selected_forum,
                             per_page: @per_page, page: @page })
  end

  # Shows not only the Forum record but the posts for a specific Subforum
  def show_by_subforum
    forum = Forum.find_by(name: params[:forum])
    selected_forum = forum.attributes

    subforum = Subforum.find_by(name: params[:subforum])
    selected_forum['posts'] = []
    new_subforum = { id: subforum.id,
                     subforum: subforum.name,
                     posts: subforum.subforum_posts(@per_page, @page) }
    selected_forum['subforums'] = [new_subforum]

    json_response(results: { forum: selected_forum,
                             per_page: @per_page, page: @page })
  end

  # Creates a Forum record and populates its Subforums through association
  # building. The params[:forum][:subforums] is an array of names for the
  # Subforums to be created.
  def create
    forum = Forum.create!(forum_params)
    all_subforums = params[:forum][:subforums]
    new_subforums = []
    all_subforums.each do |sub|
      new_hash = { name: sub }
      new_subforums.push(new_hash)
    end
    forum.subforums.create!(new_subforums)
    json_response(forums: Forum.forum_all_json)
  end

  def update
    if @forum.update(forum_params)
      json_response(forums: Forum.forum_all_json)
    else
      json_response({ errors: @forum.errors.full_messages }, 401)
    end
  end

  def destroy
    @forum.destroy
    json_response(forums: Forum.forum_all_json)
  end

  private

  def set_forum
    @forum = Forum.find(params[:id])
  end

  def set_page_params
    @per_page = params[:per_page].present? ? params[:per_page].to_i : 5
    @page = params[:page].present? ? params[:page].to_i : 1
  end

  def return_subforums(forum, per_page, page)
    all_subforums = []
    forum.subforums.each do |subforum|
      new_subforum = { id: subforum.id,
                       subforum: subforum.name,
                       posts: subforum.subforum_posts(per_page, page) }
      all_subforums.push(new_subforum)
    end

    all_subforums
  end

  def forum_params
    params.require(:forum)
          .permit(:name, :admin_only, :admin_only_view)
  end
end
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Nothing much new going on here, the variables with the @ symbol in the front, @forum, for example, are simply global variables accessible from within the entire class. These variables are then set when used in combination with private methods such as

set_forum
and
set_page_params
combined with the
before_action
method.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The

set_page_params
method plays a part in helping out with paginating the results of a search. This method works in tandem with the
subforum_posts
methods found in the Subforum and Forum models.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Subforums Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png
class SubforumsController < ApplicationController
  before_action :authorized_admin?, only: %i[create update destroy]
  before_action :set_forum, only: %i[create]
  before_action :set_subforum, only: %i[update destroy]

  def create
    @forum.subforums.create!(subforum_params)
    json_response(forums: Forum.forum_all_json)
  end

  def update
    if @subforum.update(subforum_params)
      json_response(forums: Forum.forum_all_json)
    else
      json_response({ errors: @forum.errors.full_messages }, 401)
    end
  end

  def destroy
    @subforum.destroy
    json_response(forums: Forum.forum_all_json)
  end

  private

  def set_forum
    @forum = Forum.find(params[:subforum][:forum_id])
  end

  def set_subforum
    @subforum = Subforum.find(params[:id])
  end

  def subforum_params
    params.require(:subforum).permit(:name)
  end
end

Nothing new here but I want to touch the method

authorized_admin?
which is run as a
before_action
for the methods
create
, 
update
, and 
destroy
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

As mentioned earlier when creating the methods used across multiple controllers, we added them into the Application Controller. And since all controllers inherit from the Application Controller we have access to those methods.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
authorized_admin?
essentially just checks that the user calling any of the routes mentioned, has administrative rights, meaning that their account's
admin_level
attribute is greater than 0.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Posts Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The posts controller holds all relevant post-related functions like

pin_post
, 
lock_post
, and 
suspended(date)
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def lock_post
    if @post.update(is_locked: [email protected]_locked)
      json_response(post: @post.post_json)
    else
      json_response({ errors: @post.errors.full_messages }, 401)
    end
  end

  def pin_post
    if @post.update(is_pinned: [email protected]_pinned)
      json_response(post: @post.post_json)
    else
      json_response({ errors: @post.errors.full_messages }, 401)
    end
  end

  def suspended(date)
    if date > DateTime.now
      json_response(errors: ['Your posting communications are still suspended'])
      return true
    end

    false
  end

Both methods

lock_post
and
pin_post
simply toggle the related field of the @post record grabbed.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

While the

suspended(date)
method runs a check to see if the @current_user attempting to create a post doesn't have their communications suspended by an administrator.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Comments Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Handles all commenting-related features, and allowing users to comment on comments is as simple as including that comment's id in the create request method.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def comment_params
    params.require(:comment).permit(:body, :comment_id, :user_id)
  end

Users Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

This controller handles everything related to the user other than registration and logging in. That entails returning user records, to populate their show pages, and housing methods used to suspend user communications, upload profile images, and set administrative rights.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def suspend_comms(user, comms, attr)
    comms_i = comms.map(&:to_i)
    d = DateTime.now
    ban_date = DateTime.new(comms_i[0], comms_i[1], comms_i[2], comms_i[3], comms_i[4], 0, d.offset);
    user.update_attribute(attr, ban_date)
  end

The

suspend_comms
method is used as a helper method within the main suspend_communication method. The comms attribute passed to this helper method is an array value that is broken down and passed from the front-end. This is done since the
DateTime()
method accepts multiple arguments, the year, month, day, hour, minute, second, and offset.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Setting up Active Storage

Go to cloudinary.com and sign up for a new free account or log in to your already made account.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Take note of your Cloud name (which you can change), API Key, and API Secret.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now back to your rails app...

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Create a "

cloudinary.yml
" file under the
/config
directory
0 reactions
heart.png
light.png
money.png
thumbs-down.png
production:
  cloud_name: your_cloud_name
  api_key: <%= ENV['CLOUDINARY_KEY'] %>
  api_secret: <%= ENV['CLOUDINARY_SECRET'] %>
  enhance_image_tag: true
  static_file_support: true

Your ENVIRONMENT variables will be handled by Heroku when you launch your back-end application to heroku.com.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Next, you will declare the Cloudinary service in your "

storage.yml
" in the
/config/storage.yml
directory.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

cloudinary:      
  service: Cloudinary

Here's what the top half of your "

storage.yml
" file may look like after adding the Cloudinary service.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Finally, in the

/config/environments/production.rb
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

Replace "

:local
" with "
:cloudinary
"
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :cloudinary

Later on, when we create the models and controllers, we will use these active storage helpers to manage and access the user's profile image.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
class User < ApplicationRecord
  has_secure_password
  has_many :posts, inverse_of: 'author', dependent: :destroy
  has_many :comments, dependent: :destroy
  validates :username, length: { in: 4..32 }, presence: true,
                       uniqueness: { case_sensitive: false }
  validates :password, length: { minimum: 8 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  validates :admin_level, numericality: { only_integer: true,
                                          less_than_or_equal_to: 3 }
  has_one_attached :profile_image
  before_save { username.downcase! }
  before_save { email.downcase! }
end

Added the line "

has_one_attached :profile_image
"
0 reactions
heart.png
light.png
money.png
thumbs-down.png
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def show
    json_response(user: user_with_image(@user))
  end

  private

  # Returns a hash object of a user with their profile_image included
  def user_with_image(user)
    user_with_attachment = user.attributes
    user_with_attachment['profile_image'] = nil

    unless user.profile_image_attachment.nil?
      user_with_attachment['profile_image'] = url_for(user.profile_image)
    end

    user_with_attachment
  end
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def show
    selected_user = User.find(params[:id])
    json_response(user: user_with_image(selected_user))
  end

  private

  def user_with_image(user)
    user_with_attachment = { id: user.id, username: user.username,
                             email: user.email, profile_image: nil,
                             can_post_date: user.can_post_date,
                             can_comment_date: user.can_comment_date,
                             created_at: user.created_at}
    unless user.profile_image_attachment.nil?
      user_with_attachment['profile_image'] = url_for(user.profile_image)
    end

    user_with_attachment
  end

Added the private method

user_with_image(user)
, which returns a user hash object with their profile_image attached.

Also called that method under the
show method within the
json_response 
method on the
selected_user
variable.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Don't forget to install Rails Active Storage with:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails active_storage:install

Using Action Mailer for account confirmation

Registrations Controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now, in the registrations controller, after the user's activation_key attribute is updated, send out the Activation Email:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
app/controllers/registrations_controller.rb
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def create
    user = User.create!(register_params)
    new_activation_key = generate_token(user.id, 52)
    user.update_attribute(:admin_level, 3) if User.all.size <= 1
    if user.update_attribute(:activation_key, new_activation_key)
      ActivationMailer.with(user: user).welcome_email.deliver_later
    end
    json_response({ user: user }, :created)
  end

The method called activate_account within the registrations controller handles account activation by updating the is_activated attribute on a record after comparing the given activation_key parameter with the saved value.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
app/controllers/registrations_controller.rb
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  def activate_account
    user = User.find(params[:id])

    if user.activation_key == params[:activation_key]
      user.update_attribute(:is_activated, true)
    end

    json_response(message: 'Successfully activated account')
  end

activate_account method accepts two parameters id and activation_key.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Here is how we will shape the route for this method

config/routes.rb
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  get '/activate_account',
      to: 'registrations#activate_account',
      as: 'activate_account'

Generate a new rails mailer, I called mine "Activation Mailer".

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ rails generate mailer ActivationMailer

In the file created,

app/mailers/activation_mailer.rb
add the following code.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
class ActivationMailer < ApplicationMailer
  default from: '[email protected]'

  def welcome_email
    @user = params[:user]
    mail(to: @user.email, subject: 'Welcome to the React.js Forum-CMS Demo')
  end
end

default from: '[email protected]' is simply the email address where the mail will have appeared to have been sent from.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The welcome_email method will allow us to link global variables like, @user, with a mailer template that we will be creating next.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now in the directory,

app/views/activation_mailer
we will create two .erb files.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The first one is welcome_email.html.erb,

app/views/activation_mailer/welcome_email.html.erb
, with the following provided template (You can change it up as you please, of course).
0 reactions
heart.png
light.png
money.png
thumbs-down.png
<!DOCTYPE html>
<html>
  <head>
    <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
  </head>
  <body>
    <h1><%= @user.username %>,</h1>
    <h1>Welcome to the React.js Forum-CMS Demo</h1>
    <p>
      You have successfully signed up but you need to activate your account.<br>
    </p>
    <p>
      To activate your account and login to the site, just follow this link: 
      <%= link_to "Confirmation link", activate_account_url(:id => @user.id, :activation_key => @user.activation_key) %>
    </p>
    <p>Thanks for joining and have a great day!</p>
  </body>
</html>

The URL helper activate_account_url is the route we assigned to the activate_account method that we created in the registrations controller

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The second one is welcome_email.text.erb,

app/views/activation_mailer/welcome_email.text.erb
, with the following provided template.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
Welcome to the React.js Forum-CMS Demo, <%= @user.username %>
===============================================
 
You have successfully signed up but you need to activate your account.
 
To activate your account and login to the site, just follow this link: 
<%= link_to "Confirmation link", activate_account_url(:id => @user.id, :activation_key => @user.activation_key) %>
 
Thanks for joining and have a great day!

The completed version of this mailer within the source code also contains code for the forgot_password mailer along with its routes and integration into the registrations controller (Explained above).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Deployment to Heroku

Merge recent changes to the git hub master branch.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Create a new Heroku application:

$ heroku create

Then push changes to Heroku master using:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ git push heroku master

Then generate database on Heroku application:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ heroku run rails db:migrate

Adding Sendgrid to your Heroku App to handle mailing

Documentation

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Using the Commandline

$ heroku addons:create sendgrid:starter

Using Browser

Open up your Heroku dashboard in your browser of choice and select your forum app.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Next, switch over from the Overview tab to the Resources tab.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Click the find more add-ons button...

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then scroll down until you see the SendGrid app or select the Email/SMS category from the side menu and select it.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Lastly, you will select which of your Heroku apps you wish to provision the SendGrid app for, then click Submit the Order Form.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Setting SMTP settings for ActionMailer

Create a new file called smtp.rb and place it within the

config/initializers/smtp.rb
folder.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
ActionMailer::Base.smtp_settings = {
  address: 'smtp.sendgrid.net',
  port: 587,
  domain: 'YourAppName.herokuapp.com',
  user_name: ENV['SENDGRID_USERNAME'],
  password: ENV['SENDGRID_PASSWORD'],
  authentication: :login,
  enable_starttls_auto: true
}

Replace "YourAppName" with the name of your Heroku app. The SENDGRID_USERNAME and SENDGRID_PASSWORD are environment variables that are automatically created when you install the SendGrid add-on to your Heroku app.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Also, in the

config/environments/production.rb
file, add these two lines of code
0 reactions
heart.png
light.png
money.png
thumbs-down.png
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: "https://YourHerokuApp.herokuapp.com" }

Once again, replace "YourHerokuApp" with the name of your actual Heroku app.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Afterward, your Heroku app should be able to send out emails using SendGrid.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

However, for me, I was getting an authentication error stating that my account was disabled, so I decided to go a step further and generate a SendGrid API key.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating a SendGrid API Key

Firstly, I went to Sendgrid.comand signed up for an account. Requiring verification through two-factor authentification (2fa).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

After creating the account, I authorized the use of a Single Senderwhich verified me as the owner of an email account that I will use to send the emails from. I decided to create a new email for this project.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Once the email you provide is verified you can then integrate using the WEB API or SMTP Relay configurations. We will be using SMTP Relay configurations with Action Mailer.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Here is where we will create our API Key and access the necessary SMTP configuration variables.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now we will configure our Heroku app's environment variables and add a new environment variable called "SENDGRID_API_KEY"

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ heroku config:set SENDGRID_API_KEY=YOURAPIKEY

"YOURAPIKEY" will of course be the same API Key you generated through SendGrid a few moments ago.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now go back to the file called smtp.rb that we added earlier located in

config/initializers/smtp.rb
and configure it so.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
ActionMailer::Base.smtp_settings = {
  address: 'smtp.sendgrid.net',
  port: 587,
  domain: 'YourHerokuApp.herokuapp.com',
  user_name: 'apikey',
  password: ENV['SENDGRID_API_KEY'],
  authentication: :plain
}

"YourHerokuApp" will be the name of your Heroku app.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now you should be set to send out emails through your Heroku App.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Set Heroku config variables

Documentation

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Using terminal Set Cloudinary Key

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ heroku config:set CLOUDINARY_KEY=YOURCLOUDINARYKEY

Set Cloudiny Secret

0 reactions
heart.png
light.png
money.png
thumbs-down.png
$ heroku config:set CLOUDINARY_SECRET=YOURCLOUDINARYSECRET

Now, time to reconfigure the Front-End to use the Back-End API.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the User related components

src/components/functional/users/*
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The User components will consist of Registration, Logging In, User Profile Page, Forgot Password/Reset Password, New Users Notifications (As seen near the bottom of the page), and Administrator Panel.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Registration Component

The idea behind the registration component is simple, receive input from the user such as username, email, and password, then send this information to the Back-end API's user registration route.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/functional/users/register.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useEffect, useState } from 'react';
import propTypes from 'prop-types';
import PinnedPostDisplay from '../presentational/blogPage/pinnedPostDisplay';
import PostDisplay from '../presentational/blogPage/postDisplay';

const BlogPage = ({ allPosts }) => {
  const [pinnedPosts, setPinnedPosts] = useState([]);

  const populatePins = () => pinnedPosts.map(post => (
    <PinnedPostDisplay key={post.id} post={post} />
  ));

  const populatePosts = () => allPosts.map(post => (
    <PostDisplay key={post.id} post={post} />
  ));

  // Grab all pinned Post on Component Load
  useEffect(() => {
    const postPins = allPosts.filter(post => post.is_pinned);
    setPinnedPosts(postPins);
  });

  return (
    <div className="bg-main pt-1">
      <div className="container">
        <div>
          <h2>Pinned Posts</h2>
          <div>{populatePins()}</div>
        </div>
        <div>
          <h2>All Posts</h2>
          <div>{populatePosts()}</div>
        </div>
      </div>
    </div>
  );
};

BlogPage.propTypes = {
  allPosts: propTypes.instanceOf(Array).isRequired,
};

export default BlogPage;
import React, { useState } from 'react';
import propTypes from 'prop-types';
import { userRegister } from '../../misc/apiRequests';
import ConfirmPage from '../confirmPage';

const Register = ({ handleModal, handleLoader }) => {
  const [username, setUsername] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');
  const [message, setMessage] = useState('');
  const [userCreds, setUserCreds] = useState({});
  const [emailConfirm, setEmailConfirm] = useState(false);

  const handleSubmit = e => {
    e.preventDefault();
    if (password !== passwordConfirm) {
      return handleModal(["Password doesn't Match Confirmation!"]);
    }

    const user = {
      username: username.trim(),
      email: email.trim(),
      password,
      password_confirmation: passwordConfirm,
    };

    setUserCreds({ username: username.trim(), email: email.trim() });

    handleLoader(true);
    userRegister(user)
      .then(response => {
        if (response.success) { setMessage(response.message); setEmailConfirm(true); }
        if (!response.success) handleModal(response.errors);
        handleLoader(false);
      });

    return null;
  };

  return emailConfirm
    ? <ConfirmPage user={userCreds} />
    : (
      <div id="LoginPage" className="bg-main pt-1">
        <div className="container-md">
          <h2 className="text-center mb-1">Register New User</h2>
          <form className="login-form" onSubmit={handleSubmit}>
            <h4>Username</h4>
            <input
              type="text"
              value={username}
              onChange={e => setUsername(e.target.value)}
              minLength="3"
              required
            />
            <h4>Email</h4>
            <input
              type="text"
              value={email}
              onChange={e => setEmail(e.target.value)}
              minLength="3"
              required
            />
            <h4>Password</h4>
            <input
              type="password"
              value={password}
              onChange={e => setPassword(e.target.value)}
              required
            />
            <h4>Password Confirmation</h4>
            <input
              type="password"
              value={passwordConfirm}
              onChange={e => setPasswordConfirm(e.target.value)}
              required
            />
            <button type="submit">Register</button>
          </form>

          <h4 className="text-center p-1">{message}</h4>
        </div>
      </div>
    );
};

Register.propTypes = {
  handleModal: propTypes.func.isRequired,
  handleLoader: propTypes.func.isRequired,
};

export default Register;

Take note of the incoming props,

handleModal
, and
handleLoader
. These two functions will be passed to all components that make API requests. The loader function simply determines when the visual cue for a pending request to the API is in progress. The modal function deals with displaying the returned responses from the API (success messages, errors...etc.)
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Both of these props are passed from the main component, App.js

src/App.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useState, useEffect, useCallback } from 'react';
import {
  Switch,
  Route,
} from 'react-router-dom';
import './assets/css/App.css';

import Register from './components/functional/users/register';
import Modal from './components/functional/modal';
import Loader from './components/presentational/loader';

const App = () => {
  const [errors, setErrors] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [showModal, setShowModal] = useState(false);

  // Toggle modal and clear status
  const handleModal = useCallback((errors = []) => {
    setErrors(errors);
  }, [setErrors]);

  const handleLoader = useCallback((loading = true) => {
    setIsLoading(loading);
  }, [setIsLoading]);

  // open Modal to show errors
  useEffect(() => {
    setShowModal(errors.length > 0);
  }, [errors]);

  return (
    <div className="App">
      {/* ...Some code not shown */}
      <main className="bg-navbar pt-1">
        {/* ...Some code not shown */}
        <Switch>
          <Route
            exact
            path="/sign_up"
            render={() => <Register handleModal={handleModal} handleLoader={handleLoader} />}
          />
        </Switch>
      </main>
      <div className="blend-main-footer" />
      <footer className="footer">
        {/* ...Some code not shown */}
      </footer>
      {showModal && <Modal errors={errors} handleModal={handleModal} />}
      {isLoading && <Loader />}
    </div>
  );
};

export default App;

There are three parts to the

handleModal
process, the Modal component, the two states (errors and showModal), and the
useEffect
which determines the state value of showModal.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The

handleModal 
and
handleLoader
functions are wrapped with the
useCallback
function which is used to prevent an infinite rendering loop. This loop occurs because of the child component calling the function which updates the main component which will end up re-rendering the child component.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The showModal and isLoading states are what determine whether or not the respective components are shown, and the

handleLoader
and
handleModal 
functions are used to set values to these states.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/functional/users/register.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  const handleSubmit = e => {
    e.preventDefault();
    if (password !== passwordConfirm) {
      return handleModal(["Password doesn't Match Confirmation!"]);
    }

    const user = {
      username: username.trim(),
      email: email.trim(),
      password,
      password_confirmation: passwordConfirm,
    };

    setUserCreds({ username: username.trim(), email: email.trim() });

    handleLoader(true);
    userRegister(user)
      .then(response => {
        if (response.success) { setMessage(response.message); setEmailConfirm(true); }
        if (!response.success) handleModal(response.errors);
        handleLoader(false);
      });

    return null;
  };

The

handleSubmit
function, which is set to execute on submission of the user registration form, also holds the asynchronous function
userRegister
, which sends the request to the API to register a new user. The request is treated as an
Async
Promise
, and the return value from the API server determines whether an error modal opens or the presentational confirmation page displays.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Login/Sign-in component

src/components/functional/users/login.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

After registration comes logging in, this component takes the same input values from the User as in the registration component (username, email, password) and uses it as verification.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The Login component structure is similar to the Registration component layout, having a form,

handleSubmit
function,
asynchronous
request
(
userLogin
)
, and
handleModal 
and
handleLoader 
functions.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The login page is accessed by clicking on the LoginBtn component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/presentational/users/loginBtn.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

And just as with the previous components, the Registration,Login, and LoginBtn components are all called within the App component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

There are three API request functions that work together in maintaining a user's login token validity,

UserLogin
, 
UserLoggedIn
, and 
UserLogout
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The three of these functions utilize the built-in

window.sessionStorage
function to store the user's token.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
userLogin
creates and sets a session item containing the user token and other relevant information returned from the API.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
userLoggedIn
uses the data stored in the session to see if the token stored is still valid.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

And Lastly,

userLogout
removes the stored session information out of storage while also sending a request to the API to render the current user token invalid.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Forgot/Reset Password components

src/components/functional/users/forgotPassword.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Once again, this is a form component following a similar format to the above form components. This time, the only input required from the User is their email.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

And as previously mentioned, while we were building the backend API request for this component, this component initiates a request which generates a reset password token and attaches it to the user account related to the supplied email address. Then afterward, an email is sent to the email address with a clickable link containing the generated token as a URL parameter.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then that's where the resetPassword component comes in.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/functional/users/resetPassword.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useEffect, useState } from 'react';
import { Redirect, useLocation } from 'react-router-dom';
import propTypes from 'prop-types';
import { changePasswordWithToken } from '../../misc/apiRequests';

const ResetPassword = ({ handleModal, handleLoader }) => {
  const [redirect, setRedirect] = useState(false);
  const [passwordReset, setPasswordReset] = useState(false);
  const [message, setMessage] = useState('');
  const [password, setPassword] = useState('');
  const [passwordConfirm, setPasswordConfirm] = useState('');

  const loginRedirect = (<Redirect to="/login" />);

  function useQuery() {
    return new URLSearchParams(useLocation().search);
  }
  const query = useQuery();

  const handleSubmit = e => {
    e.preventDefault();
    if (password !== passwordConfirm) {
      return handleModal(["Password doesn't Match Confirmation!"]);
    }
    const token = query.get('token');
    const user = { password, passwordConfirm };

    handleLoader(true);
    changePasswordWithToken(token, user)
      .then(response => {
        if (response.success) { setMessage(response.message); setPasswordReset(true); }
        if (!response.success) handleModal(response.errors);
        handleLoader(false);
      });
  };

  useEffect(() => {
    let timer;
    if (passwordReset) {
      timer = setTimeout(() => {
        setRedirect(true);
      }, 5000);
    }
    return () => clearTimeout(timer);
  }, [passwordReset]);

  const renderMain = passwordReset
    ? (
      <div className="bg-main pt-1">
        <div className="text-center container-md">
          <h2>{message}</h2>
          <h4>You will be redirected to the login page in a few seconds...</h4>
        </div>
      </div>
    )
    : (
      <div id="LoginPage" className="bg-main pt-1">
        <div className="container-md">
          <h2 className="text-center mb-1">Set a new Password</h2>
          <form className="login-form" onSubmit={handleSubmit}>
            <h4>New Password</h4>
            <input
              type="password"
              value={password}
              onChange={e => setPassword(e.target.value)}
              required
            />
            <h4>Password Confirmation</h4>
            <input
              type="password"
              value={passwordConfirm}
              onChange={e => setPasswordConfirm(e.target.value)}
              required
            />
            <button type="submit">Change Password</button>
          </form>
        </div>
      </div>
    );

  return redirect ? loginRedirect : renderMain;
};

ResetPassword.propTypes = {
  handleModal: propTypes.func.isRequired,
  handleLoader: propTypes.func.isRequired,
};

export default ResetPassword;

The

useQuery
function utilizes the built-in JS function
URLSearchParams
the values supplied to this function are the query parameters of the URL in string format. The
react-router-dom
dependent function
useLocation()
returns the URL of the react-app, and its searchproperty returns the query parameters within that URL.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then a variable called query is used to access the returned URLSearchParams function to

get()
the value of the parameter named 'token' and on form submit this token value is sent to the API through the use of the API request method
changePasswordWithToken
along with the new password.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then on a successful response from the server, the setPasswordReset state is set to true, which activates the condition for the useEffect to redirect the user to the login page (The loginRedirect JSX variable is returned instead of the renderMain variable).

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  useEffect(() => {
    let timer;
    if (passwordReset) {
      timer = setTimeout(() => {
        setRedirect(true);
      }, 5000);
    }
    return () => clearTimeout(timer);
  }, [passwordReset]);

loginRedirect JSX return variable...

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  const loginRedirect = (<Redirect to="/login" />);

Return statement...

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  return redirect ? loginRedirect : renderMain;

Creating the New Users/All Users components

src/components/functional/users/newUsers.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The newUsers component makes an API request which returns all of the latest users signed up. This component then displays the names of the 8 latest new users along with a count of the total number of users signed up.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The display of the count of the total number of users signed up is a Link to the allUsers component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/functional/users/allUsers.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

This component displays a list of all users ordered from newest to oldest, their name, account type(member/administrator), communications status(posting/commenting bans), and profile image.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Profile Page component

src/components/functional/users/profilePage.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The profile component displays information about the selected user like their name, account status, communications status, latest posts, and latest comments.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

If the user is looking at their own profile page they are allowed to upload a profile image and sign out from here.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Also, the Profile Page component houses the Administrative Panel views. The allowed administrative actions are relevant to whether or not the administrator is looking at their own profile page or the profile page of another user.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Administrative Panel components

src/components/functional/users/admin/adminPanel.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The administrative panel consists of multiple modal components...

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useEffect, useState } from 'react';
import propTypes from 'prop-types';
import RenameForumModal from './modals/renameForumModal';
import NewSubforumModal from './modals/newSubforumModal';
import NewforumModal from './modals/newForum';
import RenameSubforumModal from './modals/renameSubforum';
import SuspendUser from './modals/suspendUser';
import PromoteUser from './modals/promoteUser';
import { fetchAllForums, forumRemove } from '../../../misc/apiRequests';

The prop passed to this component, normally known as

handleModal 
has been renamed to
handleMainModal 
(Passed from profilePage component)to allow this component its own function that handles its various modals.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
const AdminPanel = ({
  user, selectedUser, handleSelectedUser, handleLoader, handleMainModal,
}) => {
  const [allForums, setForums] = useState([]);
  const [selectedForum, setSelectedForum] = useState({});
  const [selectedSubforum, setSelectedSubforum] = useState({});
  const [showModal, setShowModal] = useState(false);
  const [modalType, setModalType] = useState('renameForum');

  const handleModal = (forum, formType = 'renameForum', subforum = {}) => {
    setSelectedSubforum(subforum);
    setSelectedForum(forum);
    setModalType(formType);
    setShowModal(true);
  };
.......................

The newly defined

handleModal
function is used to determine which modal component should be displayed. It accepts arguments for forum, formType, and subforum. These parameters, once supplied, are used to update the different component states, like selectedForum, modalType, and selectedSubforum, some of which are then supplied to the various modal components (as seen below).
0 reactions
heart.png
light.png
money.png
thumbs-down.png
        {showModal && (
        <div className="modal">
          <button type="button" className="modal-bg" onClick={handleFormReset}>x</button>
          <div className="modal-content">
            <div className="container-md">
              {modalType === 'renameForum' && (
                <RenameForumModal
                  forum={selectedForum}
                  handleForums={handleForums}
                  handleFormReset={handleFormReset}
                  handleLoader={handleLoader}
                  handleModal={handleMainModal}
                />
              )}
              {modalType === 'renameSubforum' && (
                <RenameSubforumModal
                  forum={selectedForum}
                  subforum={selectedSubforum}
                  handleForums={handleForums}
                  handleFormReset={handleFormReset}
                  handleLoader={handleLoader}
                  handleModal={handleMainModal}
                />
              )}
              {modalType === 'newSubforum' && (
                <NewSubforumModal
                  forum={selectedForum}
                  handleForums={handleForums}
                  handleFormReset={handleFormReset}
                  handleLoader={handleLoader}
                  handleModal={handleMainModal}
                />
              )}
              {modalType === 'newForum' && (
                <NewforumModal
                  handleFormReset={handleFormReset}
                  handleForums={handleForums}
                  handleLoader={handleLoader}
                  handleModal={handleMainModal}
                />
              )}
            </div>
          </div>
        </div>
        )}

The newForum, newSubforum, renameForum, renameSubforum components are all form submission components with similar component structures (the main difference being the API request they each send out to the back-end server).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

As mentioned above, from a UI perspective, these administrative actions are only accessible if an administrator clicks onto their own profile page.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Administrator Panel (On your own profile page)

0 reactions
heart.png
light.png
money.png
thumbs-down.png

However, the administrative actions for suspending communications and promoting/demoting a user's account status are only viewable on that user's profile page in question.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Administrator Panel (On another user's profile page)

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Suspend User component

src/components/functional/users/admin/modal/suspendUser.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useState } from 'react';
import propTypes from 'prop-types';
import { userSuspendComms } from '../../../../misc/apiRequests';
import { convertRailsDate, convertToRubyDate } from '../../../../misc/convertDate';

const SuspendUser = ({
  user, selectedUser, handleSelectedUser, handleFormReset, handleLoader, handleMainModal,
}) => {
  // eslint-disable-next-line camelcase
  const { can_post_date, can_comment_date } = selectedUser;
  const [suspendPostsExpiryDate, setSuspendPostExpiry] = useState(convertRailsDate(can_post_date));
  const [
    suspendCommentsExpiryDate,
    setSuspendCommentsExpiry,
  ] = useState(convertRailsDate(can_comment_date));
// ... Some code not shown

Take note of the two imported helper functions,

convertRailsDate,
and
convertToRubyDate
found within
convertDate.js
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Both states,

suspendPostsExpiryDate
, and
suspendCommentsExpiryDate 
are values populated on component mount/load. The DateTime format stored by the Rails PostgreSQL database is different than the formatted value used and accepted by the datetime-local input type. This is why the
convertRailsDate
function is used on the can_comment_date, and the can_post_date, which are DateTime values passed from the Rails Database.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
      <h4>Suspend Posting abilities until</h4>
      <input
        type="datetime-local"
        value={suspendPostsExpiryDate}
        onChange={e => setSuspendPostExpiry(e.target.value)}
        required
      />
      <h4>Suspend Commenting abilities until</h4>
      <input
        type="datetime-local"
        value={suspendCommentsExpiryDate}
        onChange={e => setSuspendCommentsExpiry(e.target.value)}
        required
      />

Then in the handleSubmit function called on form submission...

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  // Handle modification of User's suspended activities
  const handleSubmit = e => {
    e.preventDefault();
    const suspendUser = {
      id: selectedUser.id,
      can_post_date: convertToRubyDate(suspendPostsExpiryDate),
      can_comment_date: convertToRubyDate(suspendCommentsExpiryDate),
      admin_id: user.id,
    };
// ...Some code not shown

the

convertToRubyDate
function is called on the returned value from the datetime-local type input fields.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/misc/convertDate.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
// ...Some code above not shown

const convertRailsDate = date => date.substring(0, date.length - 1);

const convertToRubyDate = date => {
  const dateArray = date.split('-');
  let timeArray = dateArray[2].substring(dateArray[2].indexOf('T'));
  timeArray = timeArray.split(':');
  timeArray[0] = timeArray[0].substring(1);
  dateArray[2] = dateArray[2].substring(0, dateArray[2].indexOf('T'));

  return dateArray.concat(timeArray.slice(0, 2));
};

export { convertDate, convertRailsDate, convertToRubyDate };

The

convertRailsDate
function simply removes the "Z" from the end of the DateTime string value returned from the Rails Database.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

And then, the

convertToRubyDate 
function breaks down the returned value from the HTML input of type date-time local into an array and supplies this array to the backend as arguments for Ruby's
DateTime.new(year, month, date, hour, minute)
function.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The promoteUser component is a fairly straightforward form submission type component with a select box input type, so no need for an in-depth explanation...

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Forum related components

src/components/functional/blogPage/*
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The Forum components will consist of the landing page BlogPage, New Post, Edit Post Page, Show Post Page, Topic Forum Page, Page Pagination, and Comments.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Creating the Blog Page Component

The Blog Page component is the default page (landing page) for the forum, meaning it's the first page a user sees upon entering the website. This page shows the Pinned Posts, All the Forums, and their Subforums, along with any related posts (paginated).

0 reactions
heart.png
light.png
money.png
thumbs-down.png
src/components/functional/blogPage.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png
const BlogPage = ({
  user, handlePostSelect, handleLoader, handleModal,
}) => {
  const [pinnedPosts, setPinnedPosts] = useState([]);
  const [forumTopics, setForumTopics] = useState([]);
  // ...Some code not shown

  // Grab all pinned Posts, and sort all other posts by forum on Component Load
  useEffect(() => {
    handleLoader(true);
    const forum = { per_page: 5, page: 1 };
    fetchAllForumPosts(forum.per_page, forum.page)
      .then(response => {
        if (response.success) {
          setPinnedPosts(response.pinned_posts.filter(post => !post.admin_only_view));
          setForumTopics(response.forums);
        }
        if (!response.success) handleModal(response.errors);
        handleLoader(false);
      });
  }, [handleLoader, handleModal]);
// ...Some code not shown

The BlogPage component calls the fetchAllForumPosts API request immediately on component load and stores the returned response values into the two states pinnedPosts and forumTopics.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useEffect, useState } from 'react';
import propTypes from 'prop-types';
import PinnedPostDisplay from '../presentational/blogPage/pinnedPostDisplay';
import ForumDisplay from '../presentational/blogPage/forumDisplay';
import '../../assets/css/blogPage.css';
import { fetchAllForumPosts } from '../misc/apiRequests';

const BlogPage = ({
  user, handlePostSelect, handleLoader, handleModal,
}) => {
  const [pinnedPosts, setPinnedPosts] = useState([]);
  const [forumTopics, setForumTopics] = useState([]);

  // Populate Pinned Posts
  const populatePins = () => pinnedPosts.map(post => (
    <button type="button" key={post.id} className="bare-btn" onClick={() => handlePostSelect(post)}>
      <PinnedPostDisplay post={post} />
    </button>
  ));

  // Populate all subforums and related posts paginated by 5 posts per page
  const populateAllForums = () => forumTopics.map(forumData => (
    <ForumDisplay
      key={forumData.name}
      user={user}
      forum={forumData}
      handlePostSelect={handlePostSelect}
      postsPages={5}
    />
  ));

The values stored in the two states are then used in tandem with the two methods populatePins and populateAllForums.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

PopulatePins

The PopulatePins method grabs the data from the array value stored within the pinnedPosts state and for each index of the array, which would be an object containing various properties, the imported PinnedPostDisplay component template is used to create selectable post buttons.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Take note that the prop handlePostSelect, which is passed to the BlogPage component, is then called onClick for the button encasing the PinnedPostDisplay component template.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  const App = () => {
  const [selectedPost, setSelectedPost] = useState(null);
  const [redirect, setRedirect] = useState(null);

  // Handles selection of post when post is clicked
  const handlePostSelect = post => {
    setSelectedPost(post);
  };

  // Follow up redirect after a post is selected
  useEffect(() => {
    if (selectedPost) {
      const { forum, subforum, id } = selectedPost;
      setRedirect(<Redirect to={`/${forum}${subforum ? `/${subforum}` : ''}/posts/${id}/show`} />);
    }
  }, [selectedPost]);

  useEffect(() => { setRedirect(null); setSelectedPost(null); }, [redirect]);

  return redirect || (
    <div className="App">
      <header className="bg-navbar">
        <nav className="container">
          <div className="flex-row">
// ...Some code not shown
src/App.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Now in the App component, the main component, the

handlePostSelect
function, is used to give the child components the ability to set the selectedPost state of the parent component, App.js.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The first useEffect hook causes a re-render whenever the selectedPost state is updated, and upon component mount/load, the redirect state is fed a link made up of the object properties saved in the selectedPost state. This Redirect simply sends the user to the relevant forum or subforum which had been clicked previously within the confines of one of the child components of the main App.js component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The second useEffect hook clears the saved state values for redirect and selectedPost.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Also, note that the return statement for the App.js component, which renders the JSX, first checks if there is a value in the redirect state. If there so happens to be a redirect link stored in the redirect state, then the page is redirected to the given link.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

So, that being said, the

handlePostSelect
function essentially provides the means to redirect a user to the selected post or topic from within the various child components.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

PopulateAllForums method

The

PopulateAllForums
method grabs the data from the array value stored within the forumTopics state and for each index of the array, which would be an object containing various properties, the imported ForumDisplay component template is used to create selectable forums, subforums, and their posts.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useEffect, useState } from 'react';
import propTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Paginate from '../../functional/blogPage/paginatePosts';
import populatePosts from './populatePosts';
import SubForumDisplay from './subForumDisplay';

const ForumDisplay = ({
  user, forum, postsPages, handlePostSelect, isSubforum,
}) => {
  // ...Some code not shown

  const populateSubForums = () => subForums.map(subforumData => (
    <SubForumDisplay
      key={subforumData.subforum}
      forum={{ id: forum.id, name: forum.name, isSubforum }}
      subforum={subforumData}
      handleIcon={handleIcon}
      handlePostSelect={handlePostSelect}
      checkForumContraints={checkForumContraints}
    />
  ));
src/components/presentational/blogPage/forumDisplay.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The ForumDisplay component imports multiple components for templated use. Including the PaginatePosts, and SubForumDisplay components coupled with the populatePosts function.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Since the populatePosts functionis a supplemental function used by the paginatePosts component and a few other components, let's run through its structure first.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

PopulatePosts method

import React from 'react';
import PostDisplay from './postDisplay';

const PopulatePosts = (postsArray, handlePostSelect, isPinned = false) => postsArray.map(post => (
  <button type="button" key={post.id} className="bare-btn row" onClick={() => handlePostSelect(post)}>
    <PostDisplay post={post} isPinned={isPinned} />
  </button>
));

export default PopulatePosts;
src/components/presentational/blogPage/populatePosts.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

This function takes 3 arguments,

postsArray
(an array),
 handlePostSelect
(a function), 
isPinned
(a boolean)
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The postsArray array is used to generate HTML buttons, which onClick accesses the

handlePostSelect
function stored in the App.js main component. The
isPinned
boolean is a visual factor that determines if the red star is shown before the title of the post.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The PostDisplaycomponent stored within the button wrapper simply displays truncated information about the posts related to a particular forum/subforum.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

PaginatePosts method

import React, { useEffect, useState } from 'react';
import propTypes from 'prop-types';

const Paginate = ({
  posts, populatePosts, postsPages, handlePostSelect,
}) => {
  const [pinnedPosts, setPinnedPosts] = useState([]);
  const [selectedPosts, setPosts] = useState([]);
  const [postsPerPage] = useState(postsPages);
  const [page, setPage] = useState(1);
  const [maxPages, setMaxPages] = useState(1);

  const handlePrev = () => {
    if (page > 1) {
      setPage(page - 1);
    }
  };

  const handleNext = () => {
    if (page < maxPages) {
      setPage(page + 1);
    }
  };

  // Calculates the max amount of pages using the length of the posts and postsPages props
  useEffect(() => {
    const pageMax = Math.ceil(posts.length / postsPerPage);
    setMaxPages(pageMax || 1);
  }, [posts, postsPerPage]);

  // Filters and stores the posts prop array into pinned and unpinned posts.
  // Unpinned posts are then paginated and stored into the selectedPosts state
  useEffect(() => {
    const postsPinned = posts.filter(post => post.is_pinned);
    const unPinnedPosts = posts.filter(post => !post.is_pinned);
    const startingIndex = (page * postsPerPage) - postsPerPage;
    const endingIndex = (page * postsPerPage) - 1;
    const paginatedPosts = unPinnedPosts.filter((post, index) => {
      if (index >= startingIndex && index <= endingIndex) {
        return post;
      }
      return null;
    });
    setPinnedPosts(postsPinned);
    setPosts(paginatedPosts);
  }, [page, posts, postsPerPage]);

  return (
    <div>
      {populatePosts(pinnedPosts, handlePostSelect, true)}
      {populatePosts(selectedPosts, handlePostSelect)}
      <div className="paginate">
        <button type="button" onClick={handlePrev}>Prev</button>
        <span>
          {page}
          /
          {maxPages}
        </span>
        <button type="button" onClick={handleNext}>Next</button>
      </div>
    </div>
  );
};
// Some code not shown...
src/components/functional/blogPage/paginatePosts.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

This component accepts 4 props,

posts
(array of posts), 
populatePosts
(function previously defined), 
postsPages
(determines amount of posts shown per page), and 
handlePostSelect
(function previously defined)
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

SubForumDisplay method

import Paginate from '../../functional/blogPage/paginatePosts';
import populatePosts from './populatePosts';

const SubForumDisplay = ({
  forum, subforum, handleIcon, handlePostSelect, checkForumContraints, postsPerPage,
}) => {
  const [forumTitle, setForumTitle] = useState('');
  const [posts, setPosts] = useState([]);
  const [showForum, setShowForum] = useState(false);

  // Some code not shown...
  return (
    <div className="forum-section ml-1">
      <div className="header-title">
        <Link to={`/${forum.name}/${forumTitle}`}>
          <h4 className="text-camel">{forumTitle}</h4>
        </Link>
        <button type="button" onClick={() => handleShowForum(showForum)}>
          {handleIcon(showForum)}
        </button>
      </div>
      {showForum && (
        <div>
          {checkForumContraints() && (
            <Link
              to={`/${forum.name}/${forumTitle}/posts/new?forum_id=${forum.id}&&subforum_id=${subforum.id}`}
              className="new-post-btn"
            >
              New Topic
            </Link>
          )}
          <div className="post-section">
            <Paginate
              posts={posts}
              handlePostSelect={handlePostSelect}
              populatePosts={populatePosts}
              postsPages={postsPerPage}
            />
          </div>
        </div>
      )}
    </div>
  );
};
src/components/presentational/blogPage/subForumDisplay.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Both forumDisplay and subForumDisplay components are presentational components used to manage how forums and subforums are structured and shown to the user, thus they both employ the same child components.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

"Announcements" as shown in the above photo, would be the result of the forumDisplay component, and "Rules" would be the result of the subForumDisplay component. "Rules of Engagement By Aaron Rory" would be the result of the paginatePosts component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

NewBlogPost Component

import React, { useState } from 'react';
import propTypes from 'prop-types';
import { Link, Redirect, useLocation } from 'react-router-dom';
import ReactQuill from 'react-quill';
import { postNew } from '../../misc/apiRequests';
import { modules, formats } from '../../misc/presets/quillModules';
import 'react-quill/dist/quill.snow.css';

const NewBlogPost = ({
  match, user, handlePostSelect, handleLoader, handleModal,
}) => {
  const [newPostTitle, setPostTitle] = useState('');
  const [newPostBody, setPostBody] = useState('');
  const { forum, subforum } = match.params;

  function useQuery() {
    return new URLSearchParams(useLocation().search);
  }
  const query = useQuery();

  const handleChangeTitle = e => {
    const elem = e.target;
    setPostTitle(elem.value);
  };

  const handleSubmitPost = e => {
    e.preventDefault();
    if (!user.can_post) return;

    const formData = new FormData();
    formData.append('post[title]', newPostTitle.trim());
    formData.append('post[body]', newPostBody);
    formData.append('post[forum_id]', query.get('forum_id'));
    formData.append('post[subforum_id]', query.get('subforum_id'));
    formData.append('post[user_id]', user.id);

    handleLoader(true);
    postNew(formData)
      .then(response => {
        if (response.success) handlePostSelect(response.post);
        if (!response.success) handleModal(response.errors);
        handleLoader(false);
      });
  };

  const renderMain = (
    <div id="BlogPage" className="bg-main">
      <div className="container-md">
        <form className="newPost" onSubmit={handleSubmitPost} encType="multipart/form-data">
          <Link to={`/${forum}${subforum ? `/${subforum}` : ''}`}>
            <i className="fas fa-chevron-circle-left pr-01" />
            Back
          </Link>
          <h4 className="text-grey">Forum</h4>
          <h3 className="text-camel">{`New ${forum}/${subforum} Topic`}</h3>
          <input
            name="postTitle"
            type="text"
            value={newPostTitle}
            onChange={handleChangeTitle}
            placeholder="Post Title"
            minLength="6"
            maxLength="32"
            required
          />
          <ReactQuill
            theme="snow"
            modules={modules}
            formats={formats}
            value={newPostBody}
            onChange={setPostBody}
          />
          <button type="submit" className="submit-btn">Submit</button>
        </form>
      </div>
    </div>
  );

  return user.logged_in ? renderMain : <Redirect to="/login" />;
};
src/components/functional/blogPage/newPost.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The match prop is passed along from the App.js main component, and match.params is used to grab the subforum and forum variable parameter values.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
https://arn-forum-cms.netlify.app/announcements/rules/posts/new?forum_id=1&&subforum_id=1
Example URL address
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The forum parameter would be "announcements", and the subforum parameter would be "rules".

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The built-in function,

URLSearchParams
, was first used while creating the Forgot/Reset Password component, is used here to grab the parameter values for both forum_id and subforum_id from the current URL web address.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

This component is essentially just another form type component, except this time the form is submitted as a

multipart/form-data
encoded FormDataobject. The reason being, initially, the idea was to allow image uploads through this form.
0 reactions
heart.png
light.png
money.png
thumbs-down.png
// Create New Post
const postNew = async post => {
  let login;
  if (sessionStorage.getItem('user')) login = JSON.parse(sessionStorage.getItem('user'));
  return axios.post(`${URL}posts`, post,
    { headers: { 'Content-Type': 'multipart/form-data', Authorization: login.token } })
    .then(response => {
      const { post } = response.data;

      return { post, success: true };
    })
    .catch(error => errorCatch(error));
};
src/components/misc/apiRequests.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

When sending over FormData through Axios the request changes up slightly too. As seen in the postNew apiRequest an additional header, Content-Type is required and the object data format sent is slightly different (No need to encase the object within an object).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

The ReactQuill component is used just as any other component would be, and the modules and formats imported were created using the documentation provided by the ReactQuill npm package.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
// Some code not shown...
  <Route
    exact
    path="/:forum/posts/new"
    render={props => (
      <NewPost
        match={props.match}
        user={user}
        handlePostSelect={handlePostSelect}
        handleLoader={handleLoader}
        handleModal={handleModal}
      />
    )}
  />
  <Route
    exact
    path="/:forum/:subforum/posts/new"
    render={props => (
      <NewPost
        match={props.match}
        user={user}
        handlePostSelect={handlePostSelect}
        handleLoader={handleLoader}
        handleModal={handleModal}
      />
    )}
  />
// Some code not shown...
src/App.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

As with all components, don't forget to add a new Route into the App.js main component to allow proper access to it.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

EditBlogPost Component

src/components/functional/blogPage/editPost.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The editPost.js component is another form type component, and just like the newBlogPost.js component, the match prop is passed along from App.js.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

EG.

https://arn-forum-cms.netlify.app/announcements/rules/posts/3/edit
0 reactions
heart.png
light.png
money.png
thumbs-down.png
  useEffect(() => {
    let isMounted = true;

    if (match.params.id) {
      const postID = parseInt(match.params.id, 10);
      handleLoader(true);
      fetchPost(postID)
        .then(response => {
          if (response.success) {
            if (isMounted) setSelectedPost(response.post);
          }
          if (!response.success) handleModal(response.errors);
          handleLoader(false);
        });
    }
    return () => { isMounted = false; };
  }, [match.params.id, handleLoader, handleModal]);

The

useEffect
react Hook, which runs as soon as the component is loaded, is used to fetch the post data from the API using the ID(would be 3 using the above example link) retrieved from the address URL through
match.params
.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Here's what the routed link looks like in App.js:

0 reactions
heart.png
light.png
money.png
thumbs-down.png
          <Route
            exact
            path="/:forum/posts/:id/edit"
            render={props => (
              <EditPost
                match={props.match}
                user={user}
                handlePostSelect={handlePostSelect}
                handleLoader={handleLoader}
                handleModal={handleModal}
              />
            )}
          />
          <Route
            exact
            path="/:forum/:subforum/posts/:id/edit"
            render={props => (
              <EditPost
                match={props.match}
                user={user}
                handlePostSelect={handlePostSelect}
                handleLoader={handleLoader}
                handleModal={handleModal}
              />
            )}
          />

The words with the colon (:) in front are all considered parameters. These parameters, :forum, :id, and :subforum would be the values retrieved using match.params.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

ShowPostPage Component

src/components/functional/blogPage/postPage.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

A functional component more akin to a presentational component, but its functionality lies in allowing the creator of the post to remove and allowing administrators to either pinning or locking a post (Disallowing commenting on the post).

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Comments Component

src/components/functional/comments/commentSection.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The commentSection.js component follows a similar coding structure to the blogPage.js component. It is ultimately a form submission type of component that relies on a pagination component and a display component.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
import React, { useEffect, useRef, useState } from 'react';
import propTypes from 'prop-types';
import { Link } from 'react-router-dom';
import CommentDisplay from './commentDisplay';
import PaginateComments from './paginateComments';
import { commentNew, commentEdit, commentRemove } from '../../misc/apiRequests';

The pagination component used is called paginateComments.js, and the display component used is called commentDisplay.js.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
  const populateComments = commentsArray => commentsArray.map(comment => (
    <CommentDisplay
      key={comment.id}
      user={user}
      allComments={postComments}
      comment={comment}
      handleSelectComment={handleSelectComment}
      handleEditComment={handleEditComment}
      handleRemoveComment={handleRemoveComment}
    />
  ));

Similar to previously discussed functions like

populatePosts
, 
populateSubForums
, and 
populateSubForums
, the
 populateComments 
function uses the 
commentDisplay
component to structure the fetched comments grabbed on the mount.
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Overall, the comments component functions very similarly to all the previously mentioned form submission type components.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

PaginateComments Component

src/components/functional/comments/paginateComments.js
0 reactions
heart.png
light.png
money.png
thumbs-down.png

It follows the same structural pattern of the paginatePosts.js component mentioned up above.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Deployment to Netlify

Now that we have finished the setup for our front-end application let's get this boy over onto Netlifyso that we can share a live interactive version of this project with others.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
0 reactions
heart.png
light.png
money.png
thumbs-down.png

After creating/logging into your Netlify account, we will attempt to create a new Netlify app from our GitHub repository.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Netlify has a feature called Continous Development, which updates and rebuilds the Netlify app whenever the GitHub repository is updated.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

After selecting GitHub as the Git provider, you will be prompted to log into your GitHub account and allow access to the repository for Netlify App.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then from there, you will be shown a list of all your GitHub repositories, and here is where you select the relevant repository to build the Netlify app from.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

After you select the repository, you will need to input the build command and the publish directory for your project.

0 reactions
heart.png
light.png
money.png
thumbs-down.png
{
  "name": "react-cmsblog",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.5.0",
    "@testing-library/user-event": "^7.2.1",
    "axios": "^0.20.0",
    "prop-types": "^15.7.2",
    "react": "^16.14.0",
    "react-dom": "^16.14.0",
    "react-quill": "^1.3.5",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.3"
  },
  "scripts": {
    "server": "react-scripts start",
    "start": "react-scripts build",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
//... Some code not shown
}
/package.json
0 reactions
heart.png
light.png
money.png
thumbs-down.png

The

build
command would be defined in the
package.json
under the "scripts" tag, and the folder generated by this build command would be under the directory of
/public/
0 reactions
heart.png
light.png
money.png
thumbs-down.png

Then from there, you should just be able to hit the deploy site button.

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Also, note that you can rename your Netlify app after creation by clicking on the App (Found on the team overview tab after logging in, then hitting Site settings -> Change site name)

0 reactions
heart.png
light.png
money.png
thumbs-down.png

Live Netlify Site Of Project

0 reactions
heart.png
light.png
money.png
thumbs-down.png
0 reactions
heart.png
light.png
money.png
thumbs-down.png
5
heart.pngheart.pngheart.pngheart.png
light.pnglight.pnglight.pnglight.png
boat.pngboat.pngboat.pngboat.png
money.pngmoney.pngmoney.pngmoney.png
by Aaron Newbold @Aaron Rory. Full-Stack Developer - JavaScript, React, Ruby, Rails. Portfolio: https://aaronrory.com Check out my Portfolio
Join Hacker Noon

Create your free account to unlock your custom reading experience.


Recommend

  • 57
    • www.tuicool.com 6 years ago
    • Cache

    5 Best Ruby on Rails CMS platforms

    Having a development team working on your website is great. They can manage things, write impressive code, and make your site unique. Yet sometimes, it makes a project complicated. This happens especially when your websit...

  • 36
    • www.tuicool.com 6 years ago
    • Cache

    Ruby on Rails - send Emails with style

    Have you ever had to send emails from your Rails application? Probably your answer will be yes. Most of us dealt already at some point with the pain of sending HTML formatted emails using Ruby on Rails

  • 57
    • www.tuicool.com 6 years ago
    • Cache

    Node.js and Ruby on Rails: a comparison

    When a new project starts, there's always the need to decide which approach is better for its development. There are numerous possibilities out there and there's always those which are more popular and attractive upfront...

  • 35
    • www.tuicool.com 5 years ago
    • Cache

    Ruby on Rails 6.0 Beta 1 Deprecations

    About this time last year, I covered Ruby on Rails 5.2 Deprecations and got lots of appreciative feedback. With the upcoming next major upd...

  • 58

    In a rush?Skip to Rails e-commerce tutorial. When it comes to certain things in life, I'm a purist. A snob, even. Now if you don't have time—or simply don't care—to learn why I'm a purist, feel free to skip to...

  • 31
    • www.tuicool.com 5 years ago
    • Cache

    42 performance tips for Ruby on Rails

    This was originally posted on my blog Since Ruby on Rails is not the fastest web framework out there you sometimes need to improv...

  • 36
    • www.tuicool.com 5 years ago
    • Cache

    Inheritance on-the-fly with Ruby on Rails

    Inheritance on-the-fly wit...

  • 10

    What, Exactly, Is the Metaverse Standards Forum Creating?No one will agree on what the metaverse is. But that's not stopping a coalition of big names in tech from designing the tools needed to build it....

  • 2

    Connect a Ruby on Rails App with React in a MonolithPaweł Dąbrowski on Aug 3, 2022

  • 1

    TL;DR In this article, you'll learn how to build a forum system that allows users to create, react, and reply to post threads. In the end, we will also send a notification on each reply on a thread with Novu, you can skip the last...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK