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. Features included in this Forum CMS: Admin Panel 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 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 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 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 , their , and their various beforehand helps a lot in guiding and keeping your database focused. tables fields associations Chart built using website: LucidChart.com Users Table Fields needed are, , , , , , , , and . The fields and are automatically created by Rails. username (string) password_digest (string), email (string) is_activated (bool) activation_key (string) token (string) admin_level (integer) can_post_date (datetime) can_comment_date (datetime) id (integer) created_at (timestamps) Our user will need a as a means of identification. The address will be used to contact them to send over an which will hold a link to set the flag on the user account (Account verification by email). username email activation_key URL is_activated The field will hold a random string which we will use for session persistence and checking if the user is still logged in. token The field will also hold a random string which we will use for resetting a user's password once requested. password_reset_token Both the and fields will be used to check whether a token is valid or has expired. token_date password_reset_date The field will span between 4 integers, 0 - 3. admin_level Level 3 (Site Owner) 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) 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) Can create and edit Forums and subForum Can disable a user's ability to comment and create topics Level 0 (Basic User) Can create and edit their own posts Can comment on posts Fields and will hold values that determine the expiration date of a user's communications suspension. can_post_date can_comment_date DateTime For example, if the and stored in any of the fields are than the and then that user is to using such a medium, and vice versa, if the and stored in these fields have then the user's are . date time greater current date time unable communicate date time already passed communications no longer suspended Forums Table Fields needed are, . name (string), admin_only (bool), and admin_view_only (bool) The field is simply an identifier for the main forum. name The field determines whether or not this forum only allows posts by administrators. admin_only The field once check is coupled with the field, setting both to true and this prevents this forum from being seen or viewed by basic user accounts. admin_view_only admin_only Subforums Table Fields needed are . name (string), and foreign key forum_id The field will hold the string identifier of the subforum, and the holds the id of the forum this subforum is a child of. name forum_id Posts Table Fields needed are , along with foreign keys . title (string), body (text), is_pinned (bool), and is_locked (bool) forum_id , subforum_id , and author_id The field holds the title of the post and the field holds the text content of the post. title body The field 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. is_pinned The field determines whether or not a post can be commented on. is_locked This table is linked to the through the foreign key and also to the through the foreign key . These fields hold a direct record object association in allowing for some pretty useful commands, which we will get into later when setting up the . Forum's Table forum_id Subforums' Table subforum_id Ruby on Rails Back-end This table is linked to the through the foreign key . User's Table author_id Comments Table Fields needed in this table are . body (text) and foreign keys author_id , comment_id , and post_id The field simply holds the text of the comment, the holds the of the user who wrote the comment, and references the post the comment was written under. body author_id id post_id The 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. comment_id 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 has an of 1 (* ), then John's will be 1 referencing Jane's comment, and Ron's will also be 1, referencing Jane's comment. comment id id not comment_id comment_id comment_id Setting up the Front-end We will build a front-end with in the form of files to provide a of the features and allow logic testing of these features. stand-alone dummy data .js demo client-based However, ultimately once the is built, we will switch out the dummy data for the actual API queried database data. back-end Create React App Since we will be creating a React application, we will use the quick start guide provided by . Create React App $ npx create-react-app forum-cms $ forum-cms cd "forum-cms" is just what I decided to call my app, you can name it whatever you want. Install Needed Dependencies PropTypes Runtime type checking for React props and similar objects. 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. $ npm i prop-types React-Router-DOM Allows the use of URL routes to load the different App components $ npm i react-router-dom Axios Used for API fetch requests $ npm i axios React-Quill A simple free-to-use rich-text editor, this is what handles runtime text editing. (Bolding, italicizes, underlings, heading, etc...) $ npm i react-quill - ESLint Code Linter 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. Inside The package.json File found within the base directory of your project folder /package.json Your file should now look similar to this: { : , : , : , : { : , : , : , : , : , : , : , : , : , : }, "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* 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 Integrated Development Environment (IDE). Visual Studio Code Within the folder, I created an folder with two sub-folders, and . "src" "assets" "CSS" "images" Then I created a folder with two sub-folders, and . Lastly, I created a folder titled and afterward, I moved all files into their relative folders. "components" "functional" "presentational" "tests" After moving all the files around, you may have to change the import locations of the files within the and files index.js App.js index.js change from: ; import './index.css' // line 3 to: ; import './assets/css/index.css' // line 3 App.js change from: logo ; ; import from './logo.svg' // line 2 import './App.css' // line 3 to: logo ; ; import from './assets/images/logo.svg' // line 2 import './assets/css/App.css' // line 3 Now the application should be able to run without errors. 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 from the is over to the . scroll position previous page transferred new page In order to fix that, we will create a Scroll reset( ) component and place that component within the initial render of the function alongside the component. ScrollToTop ReactDOM App src/components/misc/pageScrollReset.js { useEffect } ; { withRouter } ; { useEffect( { unlisten = history.listen( { .scrollTo( , ); }); { unlisten(); }; }); ( ); } withRouter(ScrollToTop); import from 'react' import from 'react-router-dom' ( ) function ScrollToTop { history } => () const => () window 0 0 return => () return null export default The prop passed to the component is one of React-Router-DOM's dependencies that allows viewing . history ScrollToTop React-Apps browsing history So within this component, we call the action, which is a method that listens to , and we supply the code, which occurs on each location change noted. ScrollToTop history.listen callback location changes window.scrollTo(0, 0) We then wrap this code within a Hook, which on , checks the browsing location history, and on cancels out history listening by calling the function expression. useEffect render/mount unmount unlisten src/index.js React, { StrictMode } ; ReactDOM ; { BrowserRouter Router } ; ; App ; ScrollToTop ; ReactDOM.render( <ScrollToTop /> <StrictMode> <App /> </StrictMode> , .getElementById( ), ); import from 'react' import from 'react-dom' import as from 'react-router-dom' import './assets/css/index.css' import from './App' import from './components/misc/pageScrollReset' // Scrolls page to top on route switch < > Router </ > Router document 'root' I created a 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. stand-alone version Setting up the Back-end API ( ͡° ͜ʖ ͡°) Now for the easy part... We will start by creating a new rails application. $ rails forum-cms --database=postgresql --api -T new Take note whenever you see the $ symbol, we are typing the presented text into the command line. This line creates a new rails application named using as the database (including the gem), setting the is flag and removing as the default testing framework. "forum-cms" PostgreSQL "pg" API MiniTest Gems to Install All the below gems should be added or already included in your Gemfile: gem , gem gem , gem group , gem , group gem gem , gem gem , # Use Active Model has_secure_password 'bcrypt' '~> 3.1.7' 'cloudinary' 'rubocop' '~>0.81.0' # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 'rack-cors' :development :test do 'rspec-rails' '~> 3.5' end :test do 'database_cleaner' 'factory_bot_rails' '~> 4.0' 'faker' 'shoulda-matchers' '~> 3.1' end After adding all the necessary gems into the (Generally found at the root directory). Run the " " command into the terminal: Gemfile bundle install $ bundle " bundle " is the shorthand for " bundle install ". Creating the Models User Model Let's start by generating a model and its migrations using the rails generator. $ 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 model we will add the following code for validations and associations. user.rb /app/models/user.rb has_secure_password has_many , , has_many , validates , { .. }, , { } validates , { } VALID_EMAIL_REGEX = .freeze validates , , { }, { VALID_EMAIL_REGEX }, { } validates , { , } before_save { username.downcase! } before_save { email.downcase! } < ApplicationRecord class User :posts inverse_of: 'author' dependent: :destroy :comments dependent: :destroy :username length: in: 4 32 presence: true uniqueness: case_sensitive: false :password length: minimum: 8 /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i :email presence: true length: maximum: 255 format: with: uniqueness: case_sensitive: false :admin_level numericality: only_integer: true less_than_or_equal_to: 3 end is a method supplied by the bcrypt gem used to set and authenticate a password attribute. has_secure_password allows us to set up model associations; in this case, one( ) can have many and . The flag causes all associated posts and comments to be destroyed along with the user. And will allow us to rename our association when coupled with the code we will place in the model has_many 1 user posts comments dependent: :destroy inverse_of belongs_to post.rb The regular expression set as a constant, is explained here: . frozen VALID_EMAIL_REGEX REGEXR Also, take note of the functions used. Before saving each user record, I take the values for both and and make sure they are entirely downcased. This preventing needing the use of ( ) coupled with the function later on when designing the login authentication system. before_save username email ILIKE POSTGRESQL search method for finding records without checking for case sensitivity where Check out the Rails Guide for an overview of all of the validations used. Forum Model Generate model: forum.rb $ rails g model forum name:string admin_only:boolean admin_only_view:boolean Now in the we will add validations and associations . forum.rb app/models/forum.rb has_many , has_many , validates , { .. }, , { } before_save { name.downcase! } offset = (page * per_page) - per_page retrieved_posts = posts.where( ) .offset(offset).limit(per_page) Forum.truncate_posts(retrieved_posts) returned_posts = [] posts.each new_post = post.as_json( %i[id user_id is_pinned created_at]) new_post[ ] = post.title.slice( .. ) new_post[ ] = post.body.slice( .. ) new_post[ ] = post.author.username new_post[ ] = post.subforum.name post.subforum.present? new_post[ ] = post.forum.name returned_posts.push(new_post) returned_posts returned_json = [] Forum.all.each new_forum = forum.as_json new_forum[ ] = forum.subforums.as_json( %i[id name]) returned_json.push(new_forum) returned_json < ApplicationRecord class Forum :subforums dependent: :destroy :posts dependent: :destroy :name length: in: 3 32 presence: true uniqueness: case_sensitive: false # Grabs all posts without a subforum, while also limiting the amount posts retrieved def subforum_posts (per_page = , page = ) 10 1 subforum_id: nil end # Truncates posts title and body attribute returning a new array . def self truncate_posts (posts) do |post| only: 'title' 0 30 'body' 0 32 'author' 'subforum' if 'forum' end end . def self forum_all_json do |forum| 'subforums' only: end end end The method grabs all posts linked to the without a These posts are then paginated and truncated. subforum_posts(subform) current forum subforum_id. The method simply returns a new Hash object with a shortened title and body of a post record along with the name of the , , and . The part of the method name ensures that it is a Class Method accessible by invoking the name of the class first to call it. self.truncate_posts(posts) author subforum forum self. EG . Forum.truncate_posts(posts) The method simply grabs the resulting array of records from the query and returns a new array with custom packed Hashes including the forum's subforum associations as keys. self.forum_all_json Forum.all Subforum Model Generate model: forum.rb $ rails g model subforum name:string forum:belongs_to Now in the we will add validations and associations . subforum.rb app/models/subforum.rb belongs_to has_many , validates , { .. }, , { } before_save { name.downcase! } offset = (page * per_page) - per_page retrieved_posts = posts.offset(offset).limit(per_page) Forum.truncate_posts(retrieved_posts) < ApplicationRecord class Subforum :forum :posts dependent: :destroy :name length: in: 3 32 presence: true uniqueness: case_sensitive: false # Grabs all posts by subforum, while also limiting the amount of posts retrieved def subforum_posts (per_page = , page = ) 10 1 end end The same logic used in the Forum model is used here: Post Model Generate model and migrations: $ rails g model post :string body:text forum:belongs_to subforum:belongs_to is_pinned:boolean is_locked:boolean user:belongs_to title After this migration is generated, we will need to change a line relating to the subforums data type. belongs_to create_table t.string t.text t.belongs_to , , t.belongs_to , t.boolean , t.boolean , t.belongs_to , , t.timestamps < ActiveRecord::Migration[6.0] class CreatePosts def change :posts do |t| :title :body :forum null: false foreign_key: true :subforum null: true :is_pinned default: false :is_locked default: false :user null: false foreign_key: true end end end We set and remove the argument allowing us to accept null values for the foreign key and eliminating the . null: true foreign_key: true subforum_id CHECK constraint Database Level Foreign Key Code added in . post.rb /app/models/post.rb belongs_to belongs_to , belongs_to , , has_many , validates , { .. }, validates , { .. }, scope , -> { where( ) } scope , -> { where( ) } new_post = attributes new_post[ ] = author.username new_post[ ] = subforum.name subforum.present? new_post[ ] = forum.name new_post[ ] = forum.admin_only new_post[ ] = forum.admin_only_view new_post returned_posts = [] posts_array.each new_post = post.as_json( %i[id user_id is_pinned created_at]) new_post[ ] = post.title.slice( .. ) new_post[ ] = post.body.slice( .. ) new_post[ ] = post.author.username new_post[ ] = post.subforum.name post.subforum.present? new_post[ ] = post.forum.name new_post[ ] = post.forum.admin_only new_post[ ] = post.forum.admin_only_view returned_posts.push(new_post) returned_posts returned_comments = [] comments_array.each new_comment = comment.as_json new_comment[ ] = comment.author.username new_comment[ ] = comment.post.forum.admin_only new_comment[ ] = comment.post.forum.admin_only_view new_comment[ ] = DateTime.now returned_comments.push(new_comment) returned_comments results = [] all_pins = Post.pins all_pins.each new_post = p.post_json results.push(new_post) results < ApplicationRecord class Post :forum :subforum optional: true :author class_name: 'User' foreign_key: 'user_id' :comments dependent: :destroy :title length: in: 3 48 presence: true :body length: in: 8 20_000 presence: true :pins 'is_pinned = true' :not_pinned 'is_pinned = false' def post_json 'author' 'subforum' if 'forum' 'admin_only' 'admin_only_view' end . def self author_posts_json (posts_array) do |post| only: 'title' 0 30 'body' 0 32 'author' 'subforum' if 'forum' 'admin_only' 'admin_only_view' end end . def self author_comments_json (comments_array) do |comment| 'author' 'admin_only' 'admin_only_view' 'server_date' end end . def self pins_json do |p| end end end The line is the inverse of the line in the model. belongs_to :author has_many user.rb The method line allows a post to have a null id for its without an error. belongs_to :subforum, optional: true subforum_id The defined allow us to grab all like records from the controller that match the criteria presented in the method clause. scopes where All of the methods in the model follow the same logic as the methods in the model. Post Forum Comment Model Generate model: comment.rb $ rails g model comment body:text user:references post:references comment:references Now in the file comment.rb app/models/comment.rb belongs_to , , belongs_to belongs_to , has_many , validates , { .. }, new_comment = attributes new_comment[ ] = author.username new_comment returned_comments = [] comments_array.each new_comment = comment.as_json new_comment[ ] = comment.post.author.username new_comment[ ] = comment.post.title new_comment[ ] = comment.post.forum.name new_comment[ ] = comment.post.subforum comment.post.subforum.present? new_comment[ ] = comment.author.username returned_comments.push(new_comment) returned_comments < ApplicationRecord class Comment :author class_name: 'User' foreign_key: 'user_id' :post :comment optional: true :comments dependent: :destroy :body length: in: 2 400 presence: true def comment_json 'author' end . def self author_comments_json (comments_array) do |comment| 'post_author' 'post_title' 'forum' 'subforum' if 'author' end end end Nothing new here, so moving along. Creating the Controllers Registrations Controller 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). Let's define some general helper methods first that we will use in a few of this controller's methods. private params. ( ) .permit( , , , ) params. ( ) .permit( , ) def register_params # whitelist params require :user :username :email :password :password_confirmation end def password_params # whitelist params require :user :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. generates... EG. register_params { params[ ][ ], params[ ][ ], params[ ][ ], [ ][ ] } username: :user :username email: :user :email password: :user :password password_confirmation: :user :password_confirmation And this method simply returns that hash. Create method The create method in the registrations controller will handle new user registrations. user = User.create!(register_params) new_activation_key = generate_token(user.id, ) user.update_attribute( , ) User.all.size <= user.update_attribute( , new_activation_key) ActivationMailer.with( user).welcome_email.deliver_now json_response({ }, ) # Register a new user account def create 62 :admin_level 3 if 1 if :activation_key user: end 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 method and create a new record. register_params User Since we want ( ), upon creating this new record, we also generate an / ( ) 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. email confirmation scroll down below to section about setting up Action mailer for more info on that User email_confirmation_token activation_key check down below for setting up the Application controller The line where it updates the User's to 3 is set to happen only if this is the first user created in the database to give this user administrative rights. :admin_level Then ultimately, a response is and the : is sent to the . JSON rendered, message ' Account registered by activation required ' client Activate Account method This method works in tandem with the create method, a route leading to this method is sent in the email to confirm a User's account. GET url = user = User.find(params[ ]) user.activation_key == params[ ] user.update_attribute( , ) redirect_to url # Link used in account activation email def activate_account # Set url variable to the front-end url 'https://arn-forum-cms.netlify.app/login' :id if :activation_key :is_activated true end # json_response(message: 'Successfully activated account') end It checks the parameter supplied is the same as the one saved in the User's record, then updates the attribute, and afterward redirects the user to the login page on the client end. :activation_key :is_activated Forgot Password method Now let's look at a similar chain of methods that also deals and uses tokens. user = User.find_by( params[ ]) user new_token = generate_token(user.id, , ) user.update_attribute( , new_token) user.update_attribute( , DateTime.now) ActivationMailer.with( user).password_reset_email.deliver_now json_response({ user.errors.full_messages }, ) json_response({ }) # Generate password reset token and send to account's associated email def forgot_password email: :email if 32 true if :password_reset_token :password_reset_date user: else errors: 401 end end message: 'Password reset information sent to associated account.' end Follows the same logic as the . A new token is generated and stored on the User record; then, an email is sent out to the containing a link to the route that verifies the token. email_confirm_token / activation_key confirmed email address reset_token = params[ ] url = redirect_to url # Link used in account password reset email def password_reset_account # Set url variable to the front-end url :password_reset_token "https://arn-forum-cms.netlify.app/reset_password?token= " #{reset_token} end This is the method used in the password reset email, which redirects to the page on the (front-end) while also supplying the as a parameter. /reset_password client reset_token Now for the final method used in the chain password_reset token = params[ ] user = User.find_by( token) token.present? user json_response({ }, ) user.password_token_expired? user.update(password_params) user.update_attribute( , ) json_response({ }) json_response({ user.errors.full_messages }, ) json_response({ }, ) # Change a user's password if they have a password reset token def change_password_with_token :password_reset_token password_reset_token: if if # Check if token is still valid return message: 'Token expired' 400 if if :password_reset_token nil message: 'Password changed successfully' else errors: 400 end else errors: 'Invalid Token' 401 end end Once the is received by the front-end client, and the user is redirected to the page through the email link. It is used here in this method to allow the user to reset their password using the method to gather their new password values. The password_reset token is checked for date validity through the model method here: reset_token reset_password password_params User added Now in the file user.rb app/models/user.rb offset = (Time.zone.now - password_reset_date).round offset / .hours >= def password_token_expired? 1 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. Application Controller Here is where we will store methods used frequently in . helper multiple controllers Response ExceptionHandler TokenGenerator CompareDates json_response({ }, ) current_user authorized_user? json_response({ }, ) @current_user.admin_level.positive? private access_token.present? @current_user = User.find_by( access_token) @current_user token_expire?(@current_user.token_date) @current_user date_diff = compare_dates(token_date) date_diff[ ] >= days && date_diff[ ] >= hours && date_diff[ ] >= minutes && date_diff[ ] >= seconds request.headers[ ] < ActionController::API class ApplicationController include include include include # Determine if user is authenticated def authorized_user? errors: 'Account not Authorized' 401 unless end # Determine if user is authenticated administrator def authorized_admin? errors: 'Insufficient Administrative Rights' 401 unless end # Sets a global _user variable if possible @current def current_user return nil unless || token: return nil unless return nil if 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 = , hours = , minutes = , seconds = ) 1 24 0 0 if :days :hrs :mins :secns true end false end # Grabs the token placed in the HTTP Request Header, "Authorization" def access_token :Authorization end end Pretty straightforward here, but notice the included modules at the top of the ? Application Controller Let's take a look at each one... Include Response render object, status module Response def json_response (object, status = ) :ok json: status: end end A simple module that takes arguments, an and a , and then returns the rails method in format using the arguments given as its options. two(2) object, status render JSON Include ExceptionHandler extend ActiveSupport::Concern included rescue_from ActiveRecord::RecordNotFound json_response({ e.message }, ) rescue_from ActiveRecord::RecordInvalid json_response({ e.message }, ) module ExceptionHandler do do |e| errors: :not_found end do |e| errors: :unprocessable_entity end end end Some information here on (Here's some additional info ). ActiveSupport::Concern Here In short, extending allows us access to the which essentially evaluates the given code block in the context of the . ActiveSupport::Concern included method base class So here's an inheritance chain of what happens in each controller since all controllers inherit from the . Application Controller You can see the entire inheritance link by typing in the : EG. RegistrationsController -> ApplicationController -> CompareDates -> TokenGenerator -> ExceptionHandler -> Response -> ActionController::API -> .... rails console $ ancestors . RegistrationsController Include CompareDates diff_secns = date2.to_time - date1.to_time diff_mins = (diff_secns / ).round diff_hrs = (diff_mins / ).round diff_days = (diff_hrs / ).round diff_text = { diff_text, diff_days, diff_hrs, diff_mins, diff_secns } # 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) 60 60 24 " day/s, hour/s, minute/s, second/s" #{diff_days} #{diff_hrs % } 24 #{diff_mins % } 60 #{(diff_secns % ).round} 60 diff_string: days: hrs: mins: 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. Include TokenGenerator random_ascii = [ , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] url_safe_ascii = [ , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] token = [user_id] ( ..token_size - ).each token.push(random_ascii.sample) url_safe token.push(url_safe_ascii.sample) url_safe token.join( ) module TokenGenerator def generate_token (user_id, token_size = , url_safe = ) 32 false 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' 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' 1 1 do unless if end '' end end The method we use and will use to generate the tokens for , , and after logging in. email confirmation password reset confirmation user authentication 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 to each generated token. user's id Sessions Controller This controller will handle allowing a user who has already signed up and confirmed their email address to log in. before_action , user = User.where( params[ ][ ].downcase) . (User.where( params[ ][ ].downcase)) .first json_response({ }, ) user authenticate_user(user) @current_user.update( ) json_response( { }) json_response( user_status(@current_user)) private user_with_status = user.as_json( %i[id username is_activated token admin_level can_post_date can_comment_date]) user_with_status[ ] = user_with_status[ ] = DateTime.now > user.can_post_date user_with_status[ ] = DateTime.now > user.can_comment_date user_with_status user.try( , params[ ][ ]) activated(user) new_token = generate_token(user.id) user.update_attribute( , new_token) user.update_attribute( , DateTime.now) json_response( user_status(user)) json_response({ user.errors.full_messages }, ) json_response({ }, ) user.is_activated json_response({ [ ] }, ) < ApplicationController class SessionsController :authorized_user? except: :create # When a user attempts to log in def create username: :user :username or email: :user :email return errors: 'Incorrect login credentials' 401 unless end # When a user logs out def destroy token: nil user: logged_in: false end # Checks if a user is still logged in def logged_in user: end # Returns a Hash with additional keys for Front-End use def user_status (user) only: 'logged_in' true 'can_post' 'can_comment' end # Returns user Hash after successful authentication def authenticate_user (user) if :authenticate :user :password return unless if :token :token_date user: else errors: 401 end else errors: 'Incorrect login credentials' 401 end end # Checks to make sure a user has confirmed their email address def activated (user) unless errors: 'Account not activated' 401 return false end true end end Fairly straightforward authentication procedure here... The method ensures that a user exists based on the given or . Create username email Afterward, the method ensures that the given is linked to the found, while also calling upon the method to ensure that the user has also confirmed their email address. authenticate_user(user) password user activated(user) Then, if all checks out, the is given a new l which is saved to their record, and a string is returned. user ogin token JSON I would also like to point out the line. This simply calls the method we created in the before every method within the . before_action :authorized_user? authorized_user? Application Controller Sessions Controller This method and its variant 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. ( authorized_admin? ) Forum Controller Holds all of the forum handling methods for creation, updating, deletion, and displaying. before_action , %i[create update destroy] before_action , %i[update destroy] before_action , %i[index show_by_forum show_by_subforum] all_forums = [] Forum.all.each new_forum = forum.attributes new_forum[ ] = forum.subforum_posts(@per_page, @page) new_forum[ ] = return_subforums(forum, @per_page, @page) all_forums.push new_forum json_response( { all_forums, Post.pins_json, @per_page, @page }) json_response( Forum.forum_all_json) forum = Forum.find_by( params[ ]) selected_forum = forum.attributes selected_forum[ ] = forum.subforum_posts(@per_page, @page) selected_forum[ ] = return_subforums(forum, @per_page, @page) json_response( { selected_forum, @per_page, @page }) forum = Forum.find_by( params[ ]) selected_forum = forum.attributes subforum = Subforum.find_by( params[ ]) selected_forum[ ] = [] new_subforum = { subforum.id, subforum.name, subforum.subforum_posts(@per_page, @page) } selected_forum[ ] = [new_subforum] json_response( { selected_forum, @per_page, @page }) forum = Forum.create!(forum_params) all_subforums = params[ ][ ] new_subforums = [] all_subforums.each new_hash = { sub } new_subforums.push(new_hash) forum.subforums.create!(new_subforums) json_response( Forum.forum_all_json) @forum.update(forum_params) json_response( Forum.forum_all_json) json_response({ @forum.errors.full_messages }, ) @forum.destroy json_response( Forum.forum_all_json) private @forum = Forum.find(params[ ]) @per_page = params[ ].present? ? params[ ].to_i : @page = params[ ].present? ? params[ ].to_i : all_subforums = [] forum.subforums.each new_subforum = { subforum.id, subforum.name, subforum.subforum_posts(per_page, page) } all_subforums.push(new_subforum) all_subforums params. ( ) .permit( , , ) < ApplicationController class ForumsController :authorized_admin? only: :set_forum only: :set_page_params only: # Shows all Forum records and appends their posts and subforums by # adding new keys after converting to a hash def index do |forum| 'posts' 'subforums' end results: forums: pinned_posts: per_page: page: end # Shows all Forum records and their related subforums def index_all forums: end # Similar process to the index method but only shows one Forum record # along with its Subforums and their posts def show_by_forum name: :forum 'posts' 'subforums' results: forum: per_page: page: end # Shows not only the Forum record but the posts for a specific Subforum def show_by_subforum name: :forum name: :subforum 'posts' id: subforum: posts: 'subforums' results: forum: per_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 :subforums do |sub| name: end forums: end def update if forums: else errors: 401 end end def destroy forums: end def set_forum :id end def set_page_params :per_page :per_page 5 :page :page 1 end def return_subforums (forum, per_page, page) do |subforum| id: subforum: posts: end end def forum_params require :forum :name :admin_only :admin_only_view end end Nothing much new going on here, the variables with the symbol in the front, , 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 and combined with the method. @ @forum set_forum set_page_params before_action The method plays a part in helping out with paginating the results of a search. This method works in tandem with the methods found in the and models. set_page_params subforum_posts Subforum Forum Subforums Controller before_action , %i[create update destroy] before_action , %i[create] before_action , %i[update destroy] @forum.subforums.create!(subforum_params) json_response( Forum.forum_all_json) @subforum.update(subforum_params) json_response( Forum.forum_all_json) json_response({ @forum.errors.full_messages }, ) @subforum.destroy json_response( Forum.forum_all_json) private @forum = Forum.find(params[ ][ ]) @subforum = Subforum.find(params[ ]) params. ( ).permit( ) < ApplicationController class SubforumsController :authorized_admin? only: :set_forum only: :set_subforum only: def create forums: end def update if forums: else errors: 401 end end def destroy forums: end def set_forum :subforum :forum_id end def set_subforum :id end def subforum_params require :subforum :name end end Nothing new here but I want to touch the method which is run as a for the methods . authorized_admin? before_action create , update , and destroy As mentioned earlier when creating the methods used across multiple controllers, we added them into the . And since all controllers inherit from the we have access to those methods. Application Controller Application Controller essentially just checks that the user calling any of the routes mentioned, has , meaning that their account's attribute is greater than 0. authorized_admin? administrative rights admin_level Posts Controller The posts controller holds all relevant post-related functions like . pin_post , lock_post , and suspended(date) @post.update( !@post.is_locked) json_response( @post.post_json) json_response({ @post.errors.full_messages }, ) @post.update( !@post.is_pinned) json_response( @post.post_json) json_response({ @post.errors.full_messages }, ) date > DateTime.now json_response( [ ]) def lock_post if is_locked: post: else errors: 401 end end def pin_post if is_pinned: post: else errors: 401 end end def suspended (date) if errors: 'Your posting communications are still suspended' return true end false end Both methods and simply toggle the related field of the record grabbed. lock_post pin_post @post While the method runs a check to see if the attempting to create a post doesn't have their communications suspended by an administrator. suspended(date) @current_user Comments Controller Handles all commenting-related features, and allowing users to comment on comments is as simple as including that in the request method. comment's id create params. ( ).permit( , , ) def comment_params require :comment :body :comment_id :user_id end Users Controller This controller handles everything related to the user other than registration and logging in. That entails , to populate their show pages, and housing methods used to , , and . returning user records suspend user communications upload profile images set administrative rights comms_i = comms.map(& ) d = DateTime.now ban_date = DateTime.new(comms_i[ ], comms_i[ ], comms_i[ ], comms_i[ ], comms_i[ ], , d.offset); user.update_attribute(attr, ban_date) def suspend_comms (user, comms, attr) :to_i 0 1 2 3 4 0 end The method is used as a helper method within the main method. The 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 method accepts multiple arguments, the year, month, day, hour, minute, second, and offset. suspend_comms suspend_communication comms DateTime() Setting up Active Storage Go to and sign up for a new account or log in to your already made account. cloudinary.com free Take note of your Cloud name (which you can change), API Key , and API Secret . Now back to your rails app... Create a " file under the directory cloudinary.yml " /config <%= %> <%= %> 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 variables will be when you launch your back-end application to . ENVIRONMENT handled by Heroku heroku.com Next, you will declare the Cloudinary service in your " " in the directory. storage.yml /config/storage.yml <%= %> <%= %> 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 . Finally, in the /config/environments/production.rb # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local Replace " " with " " :local :cloudinary # 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 to manage and access the user's . helpers profile image has_secure_password has_many , , has_many , validates , { .. }, , { } validates , { } VALID_EMAIL_REGEX = .freeze validates , , { }, { VALID_EMAIL_REGEX }, { } validates , { , } has_one_attached before_save { username.downcase! } before_save { email.downcase! } < ApplicationRecord class User :posts inverse_of: 'author' dependent: :destroy :comments dependent: :destroy :username length: in: 4 32 presence: true uniqueness: case_sensitive: false :password length: minimum: 8 /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i :email presence: true length: maximum: 255 format: with: uniqueness: case_sensitive: false :admin_level numericality: only_integer: true less_than_or_equal_to: 3 :profile_image end Added the line " has_one_attached :profile_image " and json_response( user_with_image(@user)) private user_with_attachment = user.attributes user_with_attachment[ ] = user.profile_image_attachment. ? user_with_attachment[ ] = url_for(user.profile_image) user_with_attachment def show user: end # Returns a hash object of a user with their profile_image included def user_with_image (user) 'profile_image' nil unless nil 'profile_image' end end selected_user = User.find(params[ ]) json_response( user_with_image(selected_user)) private user_with_attachment = { user.id, user.username, user.email, , user.can_post_date, user.can_comment_date, user.created_at} user.profile_image_attachment. ? user_with_attachment[ ] = url_for(user.profile_image) user_with_attachment def show :id user: end def user_with_image (user) id: username: email: profile_image: nil can_post_date: can_comment_date: created_at: unless nil 'profile_image' end 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. Don't forget to install with: Rails Active Storage $ rails active_storage:install Using Action Mailer for account confirmation Registrations Controller Now, in the after the user's attribute is updated, send out the : registrations controller, activation_key Activation Email app/controllers/registrations_controller.rb user = User.create!(register_params) new_activation_key = generate_token(user.id, ) user.update_attribute( , ) User.all.size <= user.update_attribute( , new_activation_key) ActivationMailer.with( user).welcome_email.deliver_later json_response({ user }, ) def create 52 :admin_level 3 if 1 if :activation_key user: end user: :created end The method called within the handles account activation by updating the attribute on a record after comparing the given parameter with the saved value. activate_account registrations controller is_activated activation_key app/controllers/registrations_controller.rb user = User.find(params[ ]) user.activation_key == params[ ] user.update_attribute( , ) json_response( ) def activate_account :id if :activation_key :is_activated true end message: 'Successfully activated account' end method accepts two parameters and . activate_account id activation_key Here is how we will shape the route for this method config/routes.rb get , , '/activate_account' to: 'registrations#activate_account' as: 'activate_account' Generate a new rails , I called mine "Activation Mailer". mailer $ rails generate mailer ActivationMailer In the file created, add the following code. app/mailers/activation_mailer.rb default @user = params[ ] mail( @user.email, ) < ApplicationMailer class ActivationMailer from: 'forumCMS@notifications.com' def welcome_email :user to: subject: 'Welcome to the React.js Forum-CMS Demo' end end is simply the email address where the mail will have appeared to have been sent from. default from: 'forumCMS@notifications.com' The method will allow us to link global variables like, , with a mailer template that we will be creating next. welcome_email @user Now in the directory, we will create two files. app/views/activation_mailer .erb The first one is , with the following provided template ( ). welcome_email.html.erb, app/views/activation_mailer/welcome_email.html.erb You can change it up as you please, of course <%= %> <%= %> <!DOCTYPE html> < > html < > head < = = /> meta content 'text/html; charset=UTF-8' http-equiv 'Content-Type' </ > head < > body < > h1 @user.username , Welcome to the React.js Forum-CMS Demo 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: </ > h1 < > h1 </ > h1 < > p < > br </ > p < > p link_to , activate_account_url( => @user.id, => @user.activation_key) "Confirmation link" :id :activation_key Thanks for joining and have a great day! </ > p < > p </ > p </ > body </ > html The helper is the route we assigned to the method that we created in the registrations controller URL activate_account_url activate_account The second one is , , with the following provided template. welcome_email.text.erb app/views/activation_mailer/welcome_email.text.erb <%= %> <%= %> 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 , activate_account_url( => @user.id, => @user.activation_key) "Confirmation link" :id :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 mailer along with its and integration into the (Explained above). forgot_password routes registrations controller Deployment to Heroku Merge recent changes to the git hub master branch. Create a new Heroku application: $ heroku create Then push changes to Heroku master using: $ git push heroku master Then generate database on Heroku application: $ heroku run rails db:migrate Adding Sendgrid to your Heroku App to handle mailing Documentation 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. Next, switch over from the Overview tab to the Resources tab. Click the find more add-ons button... Then scroll down until you see the app or select the category from the side menu and select it. SendGrid Email/SMS Lastly, you will select which of your apps you wish to provision the app for, then click . Heroku SendGrid Submit the Order Form Setting SMTP settings for ActionMailer Create a new file called and place it within the folder. smtp.rb config/initializers/smtp.rb ActionMailer::Base.smtp_settings = { , , , ENV[ ], ENV[ ], , } address: 'smtp.sendgrid.net' port: 587 domain: 'YourAppName.herokuapp.com' user_name: 'SENDGRID_USERNAME' password: 'SENDGRID_PASSWORD' authentication: :login enable_starttls_auto: true Replace with the name of your Heroku app. The and are environment variables that are automatically created when you install the add-on to your Heroku app. "YourAppName" SENDGRID_USERNAME SENDGRID_PASSWORD SendGrid Also, in the file, add these two lines of code config/environments/production.rb config.action_mailer.delivery_method = config.action_mailer.default_url_options = { } :smtp host: "https://YourHerokuApp.herokuapp.com" Once again, replace with the name of your actual Heroku app. "YourHerokuApp" Afterward, your Heroku app should be able to send out emails using . SendGrid However, for me, I was getting an stating that my account was disabled, so I decided to go a step further and generate a . authentication error SendGrid API key Creating a SendGrid API Key Firstly, I went to and signed up for an account. Requiring verification through two-factor authentification (2fa). Sendgrid.com After creating the account, I authorized the use of a which verified me as the owner of an email account that I will use to send the emails from. . Single Sender I decided to create a new email for this project Once the email you provide is verified you can then integrate using the or configurations. We will be using configurations with . WEB API SMTP Relay SMTP Relay Action Mailer Here is where we will create our and access the necessary configuration variables. API Key SMTP Now we will configure our app's environment variables and add a new called " Heroku environment variable SENDGRID_API_KEY" $ heroku config: SENDGRID_API_KEY=YOURAPIKEY set will of course be the same you generated through a few moments ago. "YOURAPIKEY" API Key SendGrid Now go back to the file called that we added earlier located in and configure it so. smtp.rb config/initializers/smtp.rb ActionMailer::Base.smtp_settings = { , , , , ENV[ ], } address: 'smtp.sendgrid.net' port: 587 domain: 'YourHerokuApp.herokuapp.com' user_name: 'apikey' password: 'SENDGRID_API_KEY' authentication: :plain " " will be the name of your app. YourHerokuApp Heroku Now you should be set to send out emails through your Heroku App. Set Heroku config variables Documentation Using terminal Set Cloudinary Key $ heroku config: CLOUDINARY_KEY=YOURCLOUDINARYKEY set Set Cloudiny Secret $ heroku config: CLOUDINARY_SECRET=YOURCLOUDINARYSECRET set . Now, time to reconfigure the Front-End to use the Back-End API Creating the User related components src/components/functional/users/* The User components will consist of , , , , (As seen near the bottom of the page), and . Registration Logging In User Profile Page Forgot Password/Reset Password New Users Notifications Administrator Panel Creating the Registration Component The idea behind the registration component is simple, receive input from the user such as , , and then send this information to the Back-end API's user registration route. username email password, src/components/functional/users/register.js React, { useEffect, useState } ; propTypes ; PinnedPostDisplay ; PostDisplay ; BlogPage = { [pinnedPosts, setPinnedPosts] = useState([]); populatePins = pinnedPosts.map( ( <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 from 'react' import from 'prop-types' import from '../presentational/blogPage/pinnedPostDisplay' import from '../presentational/blogPage/postDisplay' const ( ) => { allPosts } const const => () => post )); const populatePosts = () => allPosts.map(post => ( < = = /> PinnedPostDisplay key {post.id} post {post} React, { useState } ; propTypes ; { userRegister } ; ConfirmPage ; Register = { [username, setUsername] = useState( ); [email, setEmail] = useState( ); [password, setPassword] = useState( ); [passwordConfirm, setPasswordConfirm] = useState( ); [message, setMessage] = useState( ); [userCreds, setUserCreds] = useState({}); [emailConfirm, setEmailConfirm] = useState( ); handleSubmit = { e.preventDefault(); (password !== passwordConfirm) { handleModal([ ]); } user = { : username.trim(), : email.trim(), password, : passwordConfirm, }; setUserCreds({ : username.trim(), : email.trim() }); handleLoader( ); userRegister(user) .then( { (response.success) { setMessage(response.message); setEmailConfirm( ); } (!response.success) handleModal(response.errors); handleLoader( ); }); ; }; emailConfirm ? <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; import from 'react' import from 'prop-types' import from '../../misc/apiRequests' import from '../confirmPage' const ( ) => { handleModal, handleLoader } const '' const '' const '' const '' const '' const const false const => e if return "Password doesn't Match Confirmation!" const username email password_confirmation username email true => response if true if false return null return : ( < = /> ConfirmPage user {userCreds} Take note of the incoming props, , and . These two functions will be passed to all components that make API requests. The function simply determines when the visual cue for a pending request to the API is in progress. The function deals with displaying the returned responses from the API (success messages, errors...etc.) handleModal handleLoader loader modal Both of these props are passed from the main component, App.js src/App.js React, { useState, useEffect, useCallback } ; { Switch, Route, } ; ; Register ; Modal ; Loader ; App = { [errors, setErrors] = useState([]); [isLoading, setIsLoading] = useState( ); [showModal, setShowModal] = useState( ); handleModal = useCallback( { setErrors(errors); }, [setErrors]); handleLoader = useCallback( { setIsLoading(loading); }, [setIsLoading]); useEffect( { setShowModal(errors.length > ); }, [errors]); ( <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; import from 'react' import from 'react-router-dom' import './assets/css/App.css' import from './components/functional/users/register' import from './components/functional/modal' import from './components/presentational/loader' const => () const const true const false // Toggle modal and clear status const ( ) => errors = [] const ( ) => loading = true // open Modal to show errors => () 0 return {/* ...Some code not shown */} < = > div className "App" There are three parts to the process, the component, the two states ( and ), and the which determines the state value of . handleModal Modal errors showModal useEffect showModal The and functions are wrapped with the 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. handleModal handleLoader useCallback The and states are what determine whether or not the respective components are shown, and the and functions are used to set values to these states. showModal isLoading handleLoader handleModal src/components/functional/users/register.js handleSubmit = { e.preventDefault(); (password !== passwordConfirm) { handleModal([ ]); } user = { : username.trim(), : email.trim(), password, : passwordConfirm, }; setUserCreds({ : username.trim(), : email.trim() }); handleLoader( ); userRegister(user) .then( { (response.success) { setMessage(response.message); setEmailConfirm( ); } (!response.success) handleModal(response.errors); handleLoader( ); }); ; }; const => e if return "Password doesn't Match Confirmation!" const username email password_confirmation username email true => response if true if false return null The function, which is set to execute on submission of the user registration form, also holds the function , which sends the request to the to register a . The request is treated as an , and the return value from the API server determines whether an opens or the presentational displays. handleSubmit asynchronous userRegister API new user Async Promise error modal confirmation page Creating the Login/Sign-in component src/components/functional/users/login.js After registration comes logging in, this component takes the same input values from the as in the registration component ( , , ) and uses it as verification. User username email password The structure is similar to the , having a form, function, request , and and functions. Login component layout Registration component handleSubmit asynchronous ( userLogin ) handleModal handleLoader The login page is accessed by clicking on the component. LoginBtn src/components/presentational/users/loginBtn.js And just as with the previous components, the , , and components are all called within the component. Registration Login LoginBtn App There are three API request functions that work together in maintaining a user's login token validity, UserLogin , UserLoggedIn , and UserLogout . The three of these functions utilize the built-in function to store the user's token. window.sessionStorage creates and sets a session item containing the user token and other relevant information returned from the API. userLogin uses the data stored in the session to see if the token stored is still valid. userLoggedIn And Lastly, removes the stored session information out of storage while also sending a request to the API to render the current user token invalid. userLogout Creating the Forgot/Reset Password components src/components/functional/users/forgotPassword.js Once again, this is a following a similar format to the above form components. This time, the only input required from the is their . form component User email And as previously mentioned, while we were building the for this component, this component initiates a request which generates a 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 as a . backend API request reset password token generated token URL parameter Then that's where the component comes in. resetPassword src/components/functional/users/resetPassword.js React, { useEffect, useState } ; { Redirect, useLocation } ; propTypes ; { changePasswordWithToken } ; ResetPassword = { [redirect, setRedirect] = useState( ); [passwordReset, setPasswordReset] = useState( ); [message, setMessage] = useState( ); [password, setPassword] = useState( ); [passwordConfirm, setPasswordConfirm] = useState( ); loginRedirect = ( <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; import from 'react' import from 'react-router-dom' import from 'prop-types' import from '../../misc/apiRequests' const ( ) => { handleModal, handleLoader } const false const false const '' const '' const '' const ); 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 ? ( < = /> Redirect to "/login" ) : ( The function utilizes the built-in JS function the values supplied to this function are the query parameters of the URL in string format. The dependent function returns the URL of the react-app, and its property returns the within that URL. useQuery URLSearchParams react-router-dom useLocation() search query parameters Then a variable called is used to access the returned URLSearchParams function to the value of the parameter named and on form submit this token value is sent to the API through the use of the API request method along with the . query get() 'token' changePasswordWithToken new password Then on a successful response from the server, the state is set to true, which activates the condition for the to redirect the user to the login page (The JSX variable is returned instead of the variable). setPasswordReset useEffect loginRedirect renderMain useEffect( { timer; (passwordReset) { timer = setTimeout( { setRedirect( ); }, ); } clearTimeout(timer); }, [passwordReset]); => () let if => () true 5000 return => () JSX return variable... loginRedirect loginRedirect = ( const ); < = /> Redirect to "/login" Return statement... redirect ? loginRedirect : renderMain; return Creating the New Users/All Users components src/components/functional/users/newUsers.js The component makes an API request which returns all of the latest users signed up. This component then displays the names of the along with a of the of users signed up. newUsers 8 latest new users count total number The display of the count of the total number of users signed up is a to the component. Link allUsers src/components/functional/users/allUsers.js This component displays a list of ordered from to , their , (member/administrator), (posting/commenting bans), and . all users newest oldest name account type communications status profile image Creating the Profile Page component src/components/functional/users/profilePage.js The profile component displays information about the selected user like their , , , , and . name account status communications status latest posts latest comments If the user is looking at their own profile page they are allowed to a and from here. upload profile image sign out Also, the component houses the 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. Profile Page Administrative Panel Creating the Administrative Panel components src/components/functional/users/admin/adminPanel.js The administrative panel consists of multiple modal components... React, { useEffect, useState } ; propTypes ; RenameForumModal ; NewSubforumModal ; NewforumModal ; RenameSubforumModal ; SuspendUser ; PromoteUser ; { fetchAllForums, forumRemove } ; import from 'react' import from 'prop-types' import from './modals/renameForumModal' import from './modals/newSubforumModal' import from './modals/newForum' import from './modals/renameSubforum' import from './modals/suspendUser' import from './modals/promoteUser' import from '../../../misc/apiRequests' The prop passed to this component, normally known as has been renamed to (Passed from component) to allow this component its own function that handles its various modals. handleModal handleMainModal profilePage AdminPanel = ({ user, selectedUser, handleSelectedUser, handleLoader, handleMainModal, }) => { [allForums, setForums] = useState([]); [selectedForum, setSelectedForum] = useState({}); [selectedSubforum, setSelectedSubforum] = useState({}); [showModal, setShowModal] = useState( ); [modalType, setModalType] = useState( ); handleModal = { setSelectedSubforum(subforum); setSelectedForum(forum); setModalType(formType); setShowModal( ); }; ....................... const const const const const false const 'renameForum' const ( ) => forum, formType = , subforum = {} 'renameForum' true The newly defined function is used to determine which modal component should be displayed. It accepts arguments for , , and . These parameters, once supplied, are used to update the different component states, like , , and , some of which are then supplied to the various modal components (as seen below). handleModal forum formType subforum selectedForum modalType selectedSubforum {showModal && ( <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> )} < = > div className "modal" The , , , components are all submission components with similar component structures (the main difference being the API request they each send out to the back-end server). newForum newSubforum renameForum renameSubforum form As mentioned above, from a UI perspective, these administrative actions are only accessible if an administrator clicks onto their own profile page. Administrator Panel (On your own profile page) However, the administrative actions for and are only viewable on that user's profile page in question. suspending communications promoting/demoting a user's account status Administrator Panel (On another user's profile page) Creating the Suspend User component src/components/functional/users/admin/modal/suspendUser.js React, { useState } ; propTypes ; { userSuspendComms } ; { convertRailsDate, convertToRubyDate } ; SuspendUser = ({ user, selectedUser, handleSelectedUser, handleFormReset, handleLoader, handleMainModal, }) => { { can_post_date, can_comment_date } = selectedUser; [suspendPostsExpiryDate, setSuspendPostExpiry] = useState(convertRailsDate(can_post_date)); [ suspendCommentsExpiryDate, setSuspendCommentsExpiry, ] = useState(convertRailsDate(can_comment_date)); import from 'react' import from 'prop-types' import from '../../../../misc/apiRequests' import from '../../../../misc/convertDate' const // eslint-disable-next-line camelcase const const const // ... Some code not shown Take note of the two imported helper functions, and found within . convertRailsDate, convertToRubyDate convertDate.js Both states, and are values populated on component mount/load. The format stored by the Rails PostgreSQL database is different than the formatted value used and accepted by the input type. This is why the function is used on the and the which are values passed from the Rails Database. suspendPostsExpiryDate , suspendCommentsExpiryDate DateTime datetime-local convertRailsDate can_comment_date, can_post_date, DateTime <h4>Suspend Posting abilities until< > <input type= value={suspendCommentsExpiryDate} onChange={e => setSuspendCommentsExpiry(e.target.value)} required /> /h4> <input type="datetime-local" value={suspendPostsExpiryDate} onChange={e => setSuspendPostExpiry(e.target.value)} required / Suspend Commenting abilities until < > h4 </ > h4 "datetime-local" Then in the function called on form submission... handleSubmit handleSubmit = { e.preventDefault(); suspendUser = { : selectedUser.id, : convertToRubyDate(suspendPostsExpiryDate), : convertToRubyDate(suspendCommentsExpiryDate), : user.id, }; // Handle modification of User's suspended activities const => e const id can_post_date can_comment_date admin_id // ...Some code not shown the function is called on the returned value from the type input fields. convertToRubyDate datetime-local src/components/misc/convertDate.js convertRailsDate = date.substring( , date.length - ); convertToRubyDate = { dateArray = date.split( ); timeArray = dateArray[ ].substring(dateArray[ ].indexOf( )); timeArray = timeArray.split( ); timeArray[ ] = timeArray[ ].substring( ); dateArray[ ] = dateArray[ ].substring( , dateArray[ ].indexOf( )); dateArray.concat(timeArray.slice( , )); }; { convertDate, convertRailsDate, convertToRubyDate }; // ...Some code above not shown const => date 0 1 const => date const '-' let 2 2 'T' ':' 0 0 1 2 2 0 2 'T' return 0 2 export The function simply removes the "Z" from the end of the string value returned from the Rails Database. convertRailsDate DateTime And then, the function breaks down the returned value from the HTML input of type into an array and supplies this array to the backend as arguments for Ruby's function. convertToRubyDate date-time local DateTime.new(year, month, date, hour, minute) The component is a fairly straightforward form submission type component with a select box input type, so no need for an in-depth explanation... promoteUser Creating the Forum related components src/components/functional/blogPage/* The Forum components will consist of the landing page , , , , , , and . BlogPage New Post Edit Post Page Show Post Page Topic Forum Page Page Pagination Comments 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). src/components/functional/blogPage.js BlogPage = ({ user, handlePostSelect, handleLoader, handleModal, }) => { [pinnedPosts, setPinnedPosts] = useState([]); [forumTopics, setForumTopics] = useState([]); useEffect( { handleLoader( ); forum = { : , : }; fetchAllForumPosts(forum.per_page, forum.page) .then( { (response.success) { setPinnedPosts(response.pinned_posts.filter( !post.admin_only_view)); setForumTopics(response.forums); } (!response.success) handleModal(response.errors); handleLoader( ); }); }, [handleLoader, handleModal]); const const const // ...Some code not shown // Grab all pinned Posts, and sort all other posts by forum on Component Load => () true const per_page 5 page 1 => response if => post if false // ...Some code not shown The component calls the API request immediately on component load and stores the returned response values into the two states and . BlogPage fetchAllForumPosts pinnedPosts forumTopics React, { useEffect, useState } ; propTypes ; PinnedPostDisplay ; ForumDisplay ; ; { fetchAllForumPosts } ; BlogPage = ({ user, handlePostSelect, handleLoader, handleModal, }) => { [pinnedPosts, setPinnedPosts] = useState([]); [forumTopics, setForumTopics] = useState([]); populatePins = pinnedPosts.map( ( <PinnedPostDisplay post={post} /> </button> <ForumDisplay key={forumData.name} user={user} forum={forumData} handlePostSelect={handlePostSelect} postsPages={5} /> )); import from 'react' import from 'prop-types' import from '../presentational/blogPage/pinnedPostDisplay' import from '../presentational/blogPage/forumDisplay' import '../../assets/css/blogPage.css' import from '../misc/apiRequests' const const const // Populate Pinned Posts const => () => post handlePostSelect(post)}> < = = = = => button type "button" key {post.id} className "bare-btn" onClick {() )); // Populate all subforums and related posts paginated by 5 posts per page const populateAllForums = () => forumTopics.map(forumData => ( The values stored in the two states are then used in tandem with the two methods and . populatePins populateAllForums PopulatePins The method grabs the data from the array value stored within the state and for each index of the array, which would be an object containing various properties, the imported component template is used to create selectable post buttons. PopulatePins pinnedPosts PinnedPostDisplay Take note that the prop , which is passed to the component, is then called for the encasing the component template. handlePostSelect BlogPage onClick button PinnedPostDisplay App = { [selectedPost, setSelectedPost] = useState( ); [redirect, setRedirect] = useState( ); handlePostSelect = { setSelectedPost(post); }; useEffect( { (selectedPost) { { forum, subforum, id } = selectedPost; setRedirect( <div className="App"> <header className="bg-navbar"> <nav className="container"> <div className="flex-row"> // ...Some code not shown const => () const null const null // Handles selection of post when post is clicked const => post // Follow up redirect after a post is selected => () if const ); } }, [selectedPost]); useEffect(() => { setRedirect(null); setSelectedPost(null); }, [redirect]); return redirect || ( < = `/${ }${ ? `/${ }` ''}/ /${ }/ `} /> Redirect to { forum subforum subforum : posts id show src/App.js Now in the component, the main component, the function, is used to give the child components the ability to set the state of the parent component, . App handlePostSelect selectedPost App.js The first hook causes a re-render whenever the state is updated, and upon component mount/load, the state is fed a link made up of the object properties saved in the 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 component. useEffect selectedPost redirect selectedPost App.js The second hook clears the saved state values for and . useEffect redirect selectedPost Also, note that the return statement for the component, which renders the JSX, first checks if there is a value in the state. If there so happens to be a redirect link stored in the state, then the page is redirected to the given link. App.js redirect redirect So, that being said, the function essentially provides the means to redirect a user to the selected post or topic from within the various child components. handlePostSelect PopulateAllForums method The method grabs the data from the array value stored within the state and for each index of the array, which would be an object containing various properties, the imported component template is used to create selectable forums, subforums, and their posts. PopulateAllForums forumTopics ForumDisplay React, { useEffect, useState } ; propTypes ; { Link } ; Paginate ; populatePosts ; SubForumDisplay ; ForumDisplay = ({ user, forum, postsPages, handlePostSelect, isSubforum, }) => { populateSubForums = subForums.map( ( import from 'react' import from 'prop-types' import from 'react-router-dom' import from '../../functional/blogPage/paginatePosts' import from './populatePosts' import from './subForumDisplay' const // ...Some code not shown const => () => 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 The component imports multiple components for templated use. Including the , and components coupled with the function. ForumDisplay PaginatePosts SubForumDisplay populatePosts Since the function is a supplemental function used by the component and a few other components, let's run through its structure first. populatePosts paginatePosts PopulatePosts method React ; PostDisplay ; PopulatePosts = postsArray.map( ( <PostDisplay post={post} isPinned={isPinned} /> </button> import from 'react' import from './postDisplay' const ( ) => postsArray, handlePostSelect, isPinned = false => post handlePostSelect(post)}> < = = = = => button type "button" key {post.id} className "bare-btn row" onClick {() )); export default PopulatePosts; src/components/presentational/blogPage/populatePosts.js This function takes 3 arguments, . postsArray (an array), handlePostSelect (a function), isPinned (a boolean) The array is used to generate buttons, which accesses the function stored in the main component. The boolean is a visual factor that determines if the red star is shown before the title of the post. postsArray HTML onClick handlePostSelect App.js isPinned The component stored within the button wrapper simply displays truncated information about the posts related to a particular forum/subforum. PostDisplay PaginatePosts method React, { useEffect, useState } ; propTypes ; Paginate = ({ posts, populatePosts, postsPages, handlePostSelect, }) => { [pinnedPosts, setPinnedPosts] = useState([]); [selectedPosts, setPosts] = useState([]); [postsPerPage] = useState(postsPages); [page, setPage] = useState( ); [maxPages, setMaxPages] = useState( ); handlePrev = { (page > ) { setPage(page - ); } }; handleNext = { (page < maxPages) { setPage(page + ); } }; useEffect( { pageMax = .ceil(posts.length / postsPerPage); setMaxPages(pageMax || ); }, [posts, postsPerPage]); useEffect( { postsPinned = posts.filter( post.is_pinned); unPinnedPosts = posts.filter( !post.is_pinned); startingIndex = (page * postsPerPage) - postsPerPage; endingIndex = (page * postsPerPage) - ; paginatedPosts = unPinnedPosts.filter( { (index >= startingIndex && index <= endingIndex) { post; } ; }); setPinnedPosts(postsPinned); setPosts(paginatedPosts); }, [page, posts, postsPerPage]); ( <div className="paginate"> <button type="button" onClick={handlePrev}>Prev</button> <span> {page} / {maxPages} </span> <button type="button" onClick={handleNext}>Next</button> </div> ); }; import from 'react' import from 'prop-types' const const const const const 1 const 1 const => () if 1 1 const => () if 1 // Calculates the max amount of pages using the length of the posts and postsPages props => () const Math 1 // Filters and stores the posts prop array into pinned and unpinned posts. // Unpinned posts are then paginated and stored into the selectedPosts state => () const => post const => post const const 1 const ( ) => post, index if return return null return {populatePosts(pinnedPosts, handlePostSelect, true)} {populatePosts(selectedPosts, handlePostSelect)} < > div </ > div // Some code not shown... src/components/functional/blogPage/paginatePosts.js 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) SubForumDisplay method Paginate ; populatePosts ; SubForumDisplay = ({ forum, subforum, handleIcon, handlePostSelect, checkForumContraints, postsPerPage, }) => { [forumTitle, setForumTitle] = useState( ); [posts, setPosts] = useState([]); [showForum, setShowForum] = useState( ); ( <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> <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> import from '../../functional/blogPage/paginatePosts' import from './populatePosts' const const '' const const false // Some code not shown... return < = > div className "forum-section ml-1" {showForum && ( ); }; src/components/presentational/blogPage/subForumDisplay.js Both and 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. forumDisplay subForumDisplay as shown in the above photo, would be the result of the component, and would be the result of the component. would be the result of the component. "Announcements" forumDisplay "Rules" subForumDisplay "Rules of Engagement By Aaron Rory" paginatePosts NewBlogPost Component React, { useState } ; propTypes ; { Link, Redirect, useLocation } ; ReactQuill ; { postNew } ; { modules, formats } ; ; NewBlogPost = ({ match, user, handlePostSelect, handleLoader, handleModal, }) => { [newPostTitle, setPostTitle] = useState( ); [newPostBody, setPostBody] = useState( ); { forum, subforum } = match.params; { URLSearchParams(useLocation().search); } query = useQuery(); handleChangeTitle = { elem = e.target; setPostTitle(elem.value); }; handleSubmitPost = { e.preventDefault(); (!user.can_post) ; formData = FormData(); formData.append( , newPostTitle.trim()); formData.append( , newPostBody); formData.append( , query.get( )); formData.append( , query.get( )); formData.append( , user.id); handleLoader( ); postNew(formData) .then( { (response.success) handlePostSelect(response.post); (!response.success) handleModal(response.errors); handleLoader( ); }); }; renderMain = ( <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" />; }; import from 'react' import from 'prop-types' import from 'react-router-dom' import from 'react-quill' import from '../../misc/apiRequests' import from '../../misc/presets/quillModules' import 'react-quill/dist/quill.snow.css' const const '' const '' const ( ) function useQuery return new const const => e const const => e if return const new 'post[title]' 'post[body]' 'post[forum_id]' 'forum_id' 'post[subforum_id]' 'subforum_id' 'post[user_id]' true => response if if false const < = = > div id "BlogPage" className "bg-main" src/components/functional/blogPage/newPost.js The prop is passed along from the main component, and is used to grab the and variable parameter values. match App.js match.params subforum forum https:// - - / / / / ? =1&& =1 arn forum cms.netlify.app announcements rules posts new forum_id subforum_id Example URL address The parameter would be , and the parameter would be . forum "announcements" subforum "rules" The built-in function, , was first used while creating the component, is used here to grab the parameter values for both and from the current URL web address. URLSearchParams Forgot/Reset Password forum_id subforum_id This component is essentially just another form type component, except this time the form is submitted as a encoded object. The reason being, initially, the idea was to allow image uploads through this form. multipart/form-data FormData postNew = post => { login; (sessionStorage.getItem( )) login = .parse(sessionStorage.getItem( )); axios.post( , post, { : { : , : login.token } }) .then( { { post } = response.data; { post, : }; }) .catch( errorCatch(error)); }; // Create New Post const async let if 'user' JSON 'user' return ` posts` ${URL} headers 'Content-Type' 'multipart/form-data' Authorization => response const return success true => error src/components/misc/apiRequests.js When sending over through the request changes up slightly too. As seen in the an additional header, is required and the object data format sent is slightly different (No need to encase the object within an object). FormData Axios apiRequest postNew Content-Type The component is used just as any other component would be, and the and imported were created using the provided by the ReactQuill npm package. ReactQuill modules formats documentation <Route exact path= render={props => ( <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... // Some code not shown... "/:forum/posts/new" )} /> < = = = = = /> NewPost match {props.match} user {user} handlePostSelect {handlePostSelect} handleLoader {handleLoader} handleModal {handleModal} src/App.js As with all components, don't forget to add a new into the main component to allow proper access to it. Route App.js EditBlogPost Component src/components/functional/blogPage/editPost.js The component is another form type component, and just like the component, the prop is passed along from . editPost.js newBlogPost.js match App.js EG. https://arn-forum-cms.netlify.app/announcements/rules/posts/3/edit useEffect( { isMounted = ; (match.params.id) { postID = (match.params.id, ); handleLoader( ); fetchPost(postID) .then( { (response.success) { (isMounted) setSelectedPost(response.post); } (!response.success) handleModal(response.errors); handleLoader( ); }); } { isMounted = ; }; }, [match.params.id, handleLoader, handleModal]); => () let true if const parseInt 10 true => response if if if false return => () false The react Hook, which runs as soon as the component is loaded, is used to fetch the post data from the API using the (would be using the above example link) retrieved from the address URL through . useEffect ID 3 match.params Here's what the routed link looks like in : App.js <Route exact path= render={props => ( <Route exact path="/:forum/:subforum/posts/:id/edit" render={props => ( <EditPost match={props.match} user={user} handlePostSelect={handlePostSelect} handleLoader={handleLoader} handleModal={handleModal} /> )} /> "/:forum/posts/:id/edit" )} /> < = = = = = /> 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, , , and would be the values retrieved using . :forum :id :subforum match.params ShowPostPage Component src/components/functional/blogPage/postPage.js A functional component more akin to a presentational component, but its functionality lies in allowing the creator of the post to and allowing administrators to either or a post (Disallowing commenting on the post). remove pinning locking Comments Component src/components/functional/comments/commentSection.js The component follows a similar coding structure to the component. It is ultimately a form submission type of component that relies on a pagination component and a display component. commentSection.js blogPage.js React, { useEffect, useRef, useState } ; propTypes ; { Link } ; CommentDisplay ; PaginateComments ; { commentNew, commentEdit, commentRemove } ; import from 'react' import from 'prop-types' import from 'react-router-dom' import from './commentDisplay' import from './paginateComments' import from '../../misc/apiRequests' The pagination component used is called and the display component used is called . paginateComments.js, commentDisplay.js populateComments = commentsArray.map( ( const => commentsArray => comment )); < = = = = = = = /> CommentDisplay key {comment.id} user {user} allComments {postComments} comment {comment} handleSelectComment {handleSelectComment} handleEditComment {handleEditComment} handleRemoveComment {handleRemoveComment} Similar to previously discussed functions like component to structure the fetched comments grabbed on the mount. populatePosts , populateSubForums , and populateSubForums , the populateComments function uses the commentDisplay Overall, the comments component functions very similarly to all the previously mentioned form submission type components. PaginateComments Component src/components/functional/comments/paginateComments.js It follows the same structural pattern of the component mentioned up above. paginatePosts.js Deployment to Netlify Now that we have finished the setup for our front-end application let's get this boy over onto so that we can share a live interactive version of this project with others. Netlify After creating/logging into your Netlify account, we will attempt to create a new Netlify app from our GitHub repository. Netlify has a feature called Continous Development, which updates and rebuilds the Netlify app whenever the GitHub repository is updated. 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. 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. After you select the repository, you will need to input the build command and the publish directory for your project. { : , : , : , : { : , : , : , : , : , : , : , : , : , : }, : { : , : , : , : , : }, } "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 The command would be defined in the under the "scripts" tag, and the folder generated by this build command would be under the directory of build package.json /public/ Then from there, you should just be able to hit the d button. eploy site 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) Live Netlify Site Of Project