Making a Rails 6 app in 2020
Tags: rails, webdev
Published: 2020-09-14 20:54:51

The idea of ​​this article is that someone can read it and by executing all the steps have a simple blog with:

* Authentication with Devise
* Readable URL for Blogs with FriendlyId
* Platform administration panel with Active Admin
* React component with React-Rails for the comments of the blog.
  * Add CSRF token to post request
* Soft-Delete with Discard
* Restore feature in Active Admin
* Rich text with ActionText for Blog content
  * Modify Trix editor styles
    

### Introduction
I've spent the last few years making all kinds of applications, from desktop apps using Java, Python or Qt to web applications using mostly a MERN stack. Lately, the architecture of the web apps that I have been working on had a separate client and server. Which means having to develop and maintain two applications. I like this approach but a couple of months ago in my work we had to make a simple application in a short amount of time, similar to a blog, it had to be fast and simple, and I wanted to try a technology I haven't used before. For this reasons we decided not to complicate the project too much and keep a monolith architecture using Ruby on Rails integrating the `React-Rails` gem to render some React components.

### Why I'm writing this?
Sometimes it was a bit frustrating because all the articles and tutorials I found online were over 5 years old. At some point, to find the solution to a problem, I had to dig deep online or I end up implementing workaround solutions.

In this article, I am creating a very simple blog app with the features that cause me the most trouble as a Rails newbie, such as adding the Active Admin panel, soft-deletes, modifying the Trix editor, and more.


### What did I learn?
1. Rails has very good official documentation.
2. Some of the problems that I encounter had already been solved 5 years ago.
3. New Rails features have little coverage online. Most articles and tutorials date more that 4 years.
4. Questions I made on Stack Overflow and Reddit had little or no answer.
5. Writing this article taught me that there is more information than I realized but I did not know what to look for at the moment.

##### Note
One thing I'm not going to be doing is adding styles. In the real project we used Tailwind CSS, which I liked but in my opinion if you are not careful the `.erb` files can become too clutter. One of the future refactors I have in mind would be extracting css components like this https://tailwindcss.com/docs/extracting-components.

### Prerequisites
* `Ruby 2.6.6`
* `Rails 6.0.3.2`
* `PostgreSQL -> psql (PostgreSQL) 11.5`

### Create a Ruby on Rails application* 
*without test and with a postgresql database

* `$ rails new PersonalBlog -T -d=postgresql`

This will create lots of files.
* `$ cd PersonalBlog`
![Screen Shot 2020-09-09 at 20.38.04](https://dev-to-uploads.s3.amazonaws.com/i/qscmd8uxrd7kaagfkxc2.png)
* Start the server: `$ rails server` or `$ rails s`
If everything is okay the app will be running on `http://localhost:3000/`
![Screen Shot 2020-09-09 at 20.40.21](https://dev-to-uploads.s3.amazonaws.com/i/y6mpzvxhqpid8flxzzwq.png)
 
* Create the database: `$ rails db:create`

#### Create a Controller for static pages
For more info on Rails controllers here are some resources: https://guides.rubyonrails.org/action_controller_overview.html
https://www.tutorialspoint.com/ruby-on-rails/rails-controllers.htm

`$ rails g controller Pages home about`
This will create a controller without the CRUD actions. In this case the actions will be `home` and `about` actions. It will also create the routes and the views.

Routes: `pages/home`, `pages/about`

```ruby
class PagesController < ApplicationController
  def home
  end

  def about
  end
end
```

#### Implement custom routes
With `root to: 'pages#home'` in `routes.rb` means the root of the application will go to `pages#home`.
If I add `get 'about', to: 'pages#about'` I re-route `/pages/about` to `/about`.

Example:
```ruby
Rails.application.routes.draw do
  get "about-me", to: "pages#about"
  root to: "pages#home"
end
```
`http://localhost:3000/`
![Screen Shot 2020-09-10 at 12.44.14](https://dev-to-uploads.s3.amazonaws.com/i/k3rgjzyh1vkiggc7p2uh.png)
 
http://localhost:3000/about-me
![Screen Shot 2020-09-10 at 12.45.24](https://dev-to-uploads.s3.amazonaws.com/i/d0qyzn2pb23bgia98gun.png)
 
#### Implement the Blog
1. `$ rails g scaffold Blog title:string body:text`

Scaffold is a rails generator. For more of rails generators https://guides.rubyonrails.org/command_line.html#rails-generate

This will create
* migration file
* model
* controller
* views
* helpers
* default stylesheets
* add routes to `route.rb`

2. To run the migrations: `$ rails db:migrate`

Because this is the first time running this command it will create the `schema.rb` file. You shouldn't directly modify this file, it will be updated when running the migrations.

3. To check the routes: `$ rake routes`
![Screen Shot 2020-09-09 at 20.54.44](https://dev-to-uploads.s3.amazonaws.com/i/5fh6t4u38c9ljtrn49yt.png)

Now if you go to `http://localhost:3000/blogs` you are going to see a simple UI to blogs.
![Screen Shot 2020-09-09 at 20.56.28](https://dev-to-uploads.s3.amazonaws.com/i/oe6cacr1mjdt6awecv45.png)

You can create new blogs, edit the blogs, see them and destroy them.
![Screen Shot 2020-09-09 at 20.57.50](https://dev-to-uploads.s3.amazonaws.com/i/9kl7bdswh13heypaadx1.png)
![Screen Shot 2020-09-09 at 20.58.22](https://dev-to-uploads.s3.amazonaws.com/i/aj7ndsydea273d57holq.png)
  
#### Implementing Data Validations

In the model add `validates_presence_of :title, :body`. This means a blog can't be created without `title` and `body`.

#### Add Comments to Blog

Because I don't think we need views or a controller for comments right now, let's use the model generator.

1. `$ rails g model Comment content:text`
2. `$ rails db:migrate`

If you forgot and want to add blog reference to comments
3. `$ rails g migration add_blog_reference_to_comments blog:references`
4. `rails db:migrate`

Not only do you have to run a migration to add the connection between the tables, you have to specify the connection in the models (`belongs_to` and `has_many`)

```ruby
class Blog < ApplicationRecord
  validates_presence_of :title, :body

  has_many :comments, dependent: :destroy
end
```

```ruby
class Comment < ApplicationRecord
  validates_presence_of :content

  belongs_to :blog
end
```
  
Now you can do things like this:
`$ rails c`
`> blog = Blog.first`
`> blog.comments.create!(content: "First comment")`
`> blog.comments`
```
 => #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, content: "First comment", created_at: "2020-09-10 16:18:05", updated_at: "2020-09-10 16:18:05", blog_id: 1>]> 
```
And it will return all the comments associated to the blog.
You can also get the `blog` a `comment` is from.
`> Comment.last.blog`
```
 => #<Blog id: 1, title: "My first blog", body: "Blog body", created_at: "2020-09-09 23:57:38", updated_at: "2020-09-09 23:57:38"> 
```

#### Implementing the User

Let's create the user with the Devise gem. Follow the documentation instructions at https://github.com/heartcombo/devise#getting-started

* Go to `https://rubygems.org/gems/devise`, copy the gem version, in my case `gem 'devise', '~> 4.7', '>= 4.7.2'`, and  add it to the `Gemfile`. 

* `$ bundle install`
* `$ rails g devise:install`

After fallowing the instructions you will be ready to add Devise to any of your models using the generator.

* `$ rails generate devise User`
* `$ rails db:migrate`

![Screen Shot 2020-09-10 at 13.43.23](https://dev-to-uploads.s3.amazonaws.com/i/lngta0lx75i6ufysv4q4.png)

* `/registrations` → signing up into the application
* `/sessions` → Singing into the application

This will create a migration to add Users to the database, a `User` model and add the routes to `routes.rb`.

![Screen Shot 2020-09-10 at 12.50.59](https://dev-to-uploads.s3.amazonaws.com/i/1xsf0w4cvtezgo70obog.png)
 
```ruby
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end
```

Now you can register users, login, logout and more.

![Screen Shot 2020-09-10 at 12.51.41](https://dev-to-uploads.s3.amazonaws.com/i/rfynu4a0rtmlhbg20ssr.png)

![Screen Shot 2020-09-10 at 13.46.16](https://dev-to-uploads.s3.amazonaws.com/i/3hbpyxycxz2elqkj4gdi.png)

![Screen Shot 2020-09-10 at 13.46.32](https://dev-to-uploads.s3.amazonaws.com/i/dmdmdy4q3xhek09moa8p.png)
 
#### Building Custom Routes for Authentication Pages with Devise

- The default routes are: 
  * `users/sign_up`
  * `users/sign_in`
  * `users/sign_out`

We can change that.
Changing `devise_for :users` for:
`devise_for :users, path: "", path_names: { sign_in: "login", sign_out: "logout", sign_up: "register" }`

Now to check the new routes: `$ rake routes` we can see that:
* `users/sign_up` -> `register`
* `users/sign_in` -> `login`
* `users/sign_out` -> `logout`

#### Enable Users to Logout and Dynamically Render View Content

```erb
<% if current_user %>
  <%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to "Register", new_user_registration_path %>
  <%= link_to "Login", new_user_session_path %>
<% end %>
```

`current_user` is a helper method from devise that gives the current signed in user. You can see more Devise helpers at https://www.rubydoc.info/github/plataformatec/devise/Devise/Controllers/Helpers

#### How to Add Custom Attributes to a Devise Based Authentication System

What if we want to add `name` to the register form?

1. Add a `name` column to our `User` table.
`$ rails g migration AddNameColumnToUsers name:string`
`$ rails db:migrate`

2. Add the `name` field in the forms.
In `app/views/devise/registrations/edit.html.erb` and `app/views/devise/registrations/new.html.erb` add:
  ```erb
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  ```
3. Lastly, we have to permit `:name`

In `application_controller.rb`

```ruby
class ApplicationController < ActionController::Base

  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
    devise_parameter_sanitizer.permit(:account_update, keys: [:name])
  end
end
``` 

#### Add validation to the User model
```ruby
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :name, :email, presence: true
  validates :email, uniqueness: true

  validates :password, :password_confirmation, presence: true, on: :create
  validates :password, confirmation: true
end
```

##### Refactor the User Validation into a Concern

We can refactor `application_controller.rb` and move the devise code into a [concern](https://stackoverflow.com/a/15078070).

Create `app/controllers/concerns/devise_whitelist.rb` and put the code there:

```ruby
module DeviseWhitelist
  extend ActiveSupport::Concern

  included do
    before_action :configure_permitted_parameters, if: :devise_controller?
  end

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :role])
    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :role])
  end
end
```

Then in `application_controller.rb` add:
`include DeviseWhitelist`

#### Integrate Virtual Attributes to Extract First and Last Name Data from a User

In `app/models/user.rb` add:

```ruby
  # Virtual Attributes
  def first_name
    self.name.split.first
  end

  def last_name
    self.name.split.last
  end
```

#### Add guest user

Create the model `app/models/guest_user.rb`:

```ruby
class GuestUser < User
  attr_accessor :name, :first_name, :last_name, :email
end
```

In `app/controllers/concerns/current_user_concern.rb`:

```ruby
module CurrentUserConcern
  extend ActiveSupport::Concern
  # Override devise current_user
  def current_user
    super || guest_user
  end

  def guest_user
    guest = GuestUser.new
    guest.name = "Guest User"
    guest.first_name = "Guest"
    guest.last_name = "User"
    guest.email = "guest@example.com"
    guest
  end
end
```

And then in `application_controller.rb` add
`include CurrentUserConcern`

Then I have to update the login/logout functionality:

```erb
<% if !current_user.is_a?(GuestUser) %>
  <%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
  <%= link_to "Register", new_user_registration_path %>
  <%= link_to "Login", new_user_session_path %>
<% end %>
```

#### Add the relationship between Blog, Comments and Users.

* `$ rails g migration AddUserReferenceToComments user:references`
* `$ rails db:migrate`

```ruby
class AddUserReferenceToComments < ActiveRecord::Migration[6.0]
  def change
    add_reference :comments, :user, null: false, foreign_key: true
  end
end
```
* In `app/models/comment.rb`

```ruby
class Comment < ApplicationRecord
  validates_presence_of :content

  belongs_to :blog
  belongs_to :user
end
```

* `$ rails g migration AddUserReferenceToBlogs user:references`
* `$ rails db:migrate`

```ruby
class AddUserReferenceToBlogs < ActiveRecord::Migration[6.0]
  def change
    add_reference :blogs, :user, null: false, foreign_key: true
  end
end
```

* In `app/models/blog.rb`
```ruby
class Blog < ApplicationRecord
  validates_presence_of :title, :body

  has_many :comments, dependent: :destroy
  belongs_to :user
end
```

* In `app/models/user.rb`
```ruby
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :name, :email, presence: true
  validates :email, uniqueness: true

  validates :password, :password_confirmation, presence: true, on: :create
  validates :password, confirmation: true

  has_many :blogs
  has_many :comments
end
```

### Guest users can only see blogs.
For this we are going to user a devise method: `authenticate_user!`

* In `blogs_controller.rb` let's add a before_action filter
```ruby
class BlogsController < ApplicationController
  before_action :set_blog, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, except: [:index, :show]

  # GET /blogs
  # GET /blogs.json
  def index
    @blogs = Blog.all
  end

  # GET /blogs/1
  # GET /blogs/1.json
  def show
  end

  # GET /blogs/new
  def new
    @blog = Blog.new
  end

  # GET /blogs/1/edit
  def edit
  end

  # POST /blogs
  # POST /blogs.json
  def create
    @blog = Blog.new(blog_params)

    respond_to do |format|
      if @blog.save
        format.html { redirect_to @blog, notice: "Blog was successfully created." }
        format.json { render :show, status: :created, location: @blog }
      else
        format.html { render :new }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /blogs/1
  # PATCH/PUT /blogs/1.json
  def update
    respond_to do |format|
      if @blog.update(blog_params)
        format.html { redirect_to @blog, notice: "Blog was successfully updated." }
        format.json { render :show, status: :ok, location: @blog }
      else
        format.html { render :edit }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /blogs/1
  # DELETE /blogs/1.json
  def destroy
    @blog.destroy
    respond_to do |format|
      format.html { redirect_to blogs_url, notice: "Blog was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_blog
    @blog = Blog.find(params[:id])
  end

  # Only allow a list of trusted parameters through.
  def blog_params
    params.require(:blog).permit(:title, :body)
  end
end
```

This means that before the actions `index` and `show` the devise method `authenticate_user!` will be executed.

* We are also going to redirect login users to blogs index page
```ruby
class PagesController < ApplicationController
  def home
    if !current_user.is_a?(GuestUser)
      redirect_to blogs_path
    end
  end

  def about
  end
end
```

#### Add current_user to Blog `create` action
```ruby
  def create
    @blog = Blog.new(blog_params.merge(user_id: current_user.id))

    respond_to do |format|
      if @blog.save
        format.html { redirect_to @blog, notice: "Blog was successfully created." }
        format.json { render :show, status: :created, location: @blog }
      else
        format.html { render :new }
        format.json { render json: @blog.errors, status: :unprocessable_entity }
      end
    end
  end
```

#### Add content to seeds file, `db/seeds.rb`, to have some data to work with.

```ruby
User.create!(email: "test@test.com",
             name: "test",
             password: "123123",
             password_confirmation: "123123") if Rails.env.development?

3.times do |blog|
  Blog.create!(
    title: "My blog #{blog}",
    body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    user_id: 1,
  ) if Rails.env.development?
end

Blog.last.comments.create!([{ content: "This is a very good post", user_id: 1 }, 
                            { content: "I agree with all of this", user_id: 1 }, 
                            { content: "I don't agree with all of this", user_id: 1 }])
```

Now, every time we empty the database we can fill it executing:
 `$ rails db:seed`.

One command that I often use is `rails db:setup`. This will run `db:create`, `db:schema:load` and `db:seed`.

#### Create a controller for the comments
Before I created the model. Now I realized I also need a controller.

1. `$ rails g controller comments`

2. Open `comments_controller.rb` and add the `create` and `destroy` actions.

```ruby
class CommentsController < ApplicationController
  before_action :set_comment, only: [:destroy]

  def create
    comment = current_user.comments.new(content: params[:newComment], blog_id: params[:blog_id])

    if comment.save
      render json: { comment: comment }, status: :ok
    else
      render json: { error: comment.errors.message }, status: 422
    end
  end

  def destroy
    if @comment.destroy
      head :no_content
    else
      render json: { error: @comment.errors.message }, status: 422
    end
  end

  private

  def set_comment
    @comment = Comment.find(params[:id])
  end

  def comment_params
    params.require(:comment).permit(:content, :blog_id)
  end
end
```

#### Add an action to blogs controller to get the comments of a blog.

* In `app/controllers/blogs_controller.rb`
```ruby
  def get_comments
    comments = @blog.comments.select("comments.*, users.name").joins(:user).by_created_at
    render json: { comments: comments }
  end
```

* Update before_action filter
```ruby
  before_action :authenticate_user!, except: [:index, :show, :get_comments]
```

* Add the routes
```ruby
  resources :blogs do
    resources :comments, only: [:create]
    member do
      get :get_comments
    end
  end
```

#### Create a scope to get the comments of a blog in descending order by the creation date.

In `app/models/comment.rb`
```ruby
class Comment < ApplicationRecord
  validates_presence_of :content

  belongs_to :blog
  belongs_to :user

  def self.by_created_at
    order("created_at DESC")
  end
end
```

### Add react-rails gem 
Fallow the instructions from https://github.com/reactjs/react-rails

1. Add `gem 'react-rails', '~> 2.6', '>= 2.6.1'` to `Gemfile`
2. `$ bundle install`
3. `$ rails webpacker:install`
4. `$ rails webpacker:install:react`
5. `$ rails generate react:install`
6. `$ rails g react:component HelloWorld greeting:string`

This will create a `HelloWorld` component at `app/javascript/components/HelloWorld.js`

Now you can add the component in the view like this:

```ruby
<%= react_component("HelloWorld", { greeting: "Hello from react-rails." }) %>
```

### Implement Comments component to render the comments of a blog

I used `axios`. For that, go to the axios npm page, get the version and add it to `package.json`:

```json
{
  "name": "PersonalBlog",
  "private": true,
  "dependencies": {
    "@babel/preset-react": "^7.10.4",
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "4.3.0",
    "axios": "0.20.0",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
    "prop-types": "^15.7.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react_ujs": "^2.6.1",
    "turbolinks": "^5.2.0"
  },
  "version": "0.1.0",
  "devDependencies": {
    "webpack-dev-server": "^3.11.0"
  }
}
```
Then run `$ yarn install`

Now in `app/javascript/components` create a file for the comments component, in my case I called it `Comments.js`.

Here we are going to use axios to make http request to create 
new comments and get the comments of a blog.
We are going to use `useEffect` to fetch the comments when the component first render and every time a new comment is created.
We also are going to add the `csrf token` to the post request to avoid Rails error `ActionController::InvalidAuthenticityToken`.

#### Comments component

![Screen Shot 2020-09-15 at 10.53.01](https://dev-to-uploads.s3.amazonaws.com/i/3j9dm2gifyvq589hjl9a.png)
 
```javascript
// Comments.js
import React, { useEffect, useState } from "react";
import axios from "axios";
import CommentForm from "./CommentForm";
import Comment from "./Comment";

export default function Comments({ user, blogId }) {
  const [comments, setComments] = useState([]);
  const [updateComments, setUpdateComments] = useState(0);
  const [newComment, setNewComment] = useState("");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    axios
      .get(`/blogs/${blogId}/get_comments`)
      .then((data) => {
        setComments(data.data.comments);
        setLoading(true);
      })
      .catch((err) => {
        console.log(err);
      });
  }, [updateComments]);

  const handleSubmit = (e) => {
    e.preventDefault();
    var token = document.getElementsByName("csrf-token")[0].content;
    axios.defaults.headers.common["X-CSRF-TOKEN"] = token;

    axios
      .post(`/blogs/${blogId}/comments`, {
        newComment,
      })
      .then((response) => {
        console.log("Server response:", response);
        setNewComment("");
        setUpdateComments(updateComments + 1);
      })
      .catch((err) => {
        console.log("Error:", err);
      });
  };

  const commentsComp = comments.map((comment) => {
    return <Comment comment={comment} key={comment.id} />;
  });

  return (
    <div className="comments-container">
      {user.id !== null && (
        <CommentForm
          handleSubmit={handleSubmit}
          setNewComment={setNewComment}
          blogId={blogId}
        />
      )}
      <p className="comments-heading">Comments</p>
      {comments.length === 0 && <p>There are no comments!</p>}
      {loading ? commentsComp : <p>Loading</p>}
    </div>
  );
}
```

If you want to use `fetch`

```javascript
  useEffect(() => {
    fetch(`/blogs/${blogId}/comments`)
      .then((res) => res.json())
      .then((data) => {
        setComments(data.comments);
        setLoading(true);
      })
      .catch((err) => {
        console.log(err);
      });
  }, [updateComments]);
```


```javascript
// Comment.js
export default function Comment({ comment }) {
  if (!comment) {
    return <div />;
  }

  return (
    <div className="comment">
      <div className="user-name">{comment.name} | </div>
      <div className="comment-actions">{comment.content}</div>
    </div>
  );
}
```

```javascript
// CommentForm.js
import React, { useState, useEffect } from "react";

export default function CommentForm({ handleSubmit, setNewComment }) {
  const [comment, setComment] = useState("");

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <p>Write a comment</p>

        <textarea
          maxLength="200"
          type="text"
          name="comment"
          className="comment-textarea"
          value={comment}
          onChange={(e) => {
            setComment(e.target.value);
            setNewComment(e.target.value);
          }}
        >
          Enter text here...
        </textarea>
      </div>

      <button type="submit" onClick={() => setComment("")}>
        Confirm
      </button>
    </form>
  );
}
```

#### Adding some styles to `blogs.scss`

```css
.comments-container {
  padding: 2px;

  .comments-heading {
    font-size: 18px;
  }
}

.comment {
  display: flex;
  border: 1px solid grey;
  padding: 8px;
  margin: 8px 0;
  width: 400px;

  .user-name {
    margin-right: 4px;
  }
}

.comment-textarea {
  width: 400px;
  height: 100px;
  padding: 8px;
}
```
 
#### Some screenshots of the result so far

![Screen Shot 2020-09-14 at 12.03.31](https://dev-to-uploads.s3.amazonaws.com/i/m6t5gkkkjpuhlvkj7xv6.png)
![Screen Shot 2020-09-14 at 12.04.12](https://dev-to-uploads.s3.amazonaws.com/i/phinengfmoea3cr11osm.png)
![Screen Shot 2020-09-14 at 12.05.17](https://dev-to-uploads.s3.amazonaws.com/i/t6rvyy2bt0ujjm2x0b6i.png)
![Screen Shot 2020-09-14 at 12.11.04](https://dev-to-uploads.s3.amazonaws.com/i/f3qqn2x774r6ta113ehw.png)
![Screen Shot 2020-09-14 at 12.14.01](https://dev-to-uploads.s3.amazonaws.com/i/8i8ryich0uw0b2t35tgw.png)
  
 
### Add Active Admin gem to manage the application
https://activeadmin.info/documentation.html

1. Add `gem 'activeadmin', '~> 2.8'` to the `Gemfile`
2. `$ bundle install`
3. `$ rails generate active_admin:install`
4. `$ rails db:migrate`

This will create:
* `app/models/admin_user.rb`
* `config/initializers/active_admin.rb`
* `db/migrate/20200914152255_devise_create_admin_users.rb`
* `db/migrate/20200914152304_create_active_admin_comments.rb`
* `app/assets/stylesheets/active_admin.scss`
* `app/assets/javascripts/active_admin.js`
* `app/admin/dashboard.rb`
* `app/admin/admin_users.rb`

And it will add an `admin user` to the seed file
```ruby
AdminUser.create!(email: "admin@example.com", password: "password", password_confirmation: "password") if Rails.env.development?
```

It's also going to add the routes to `routes.rb`
```ruby
 devise_for :admin_users, ActiveAdmin::Devise.config
  ActiveAdmin.routes(self)
```

After this you will be able to access the admin dashboard:

Visit http://localhost:3000/admin and log in using:

```ruby
User: admin@example.com
Password: password
```

##### Note: You may have to run the seed file to create the Admin User.

By default this is going to create an `AdminUser` model and a `AdminUser` table, different that the `User` table you already have.
In my case I wanted to unified those two tables. For this I based my solution on this article about how to [implement a single user model with rails active admin and devise](http://dan.doezema.com/2012/02/how-to-implement-a-single-user-model-with-rails-activeadmin-and-devise/):

* Add `superuser?` boolean flag to `User`.
`$ rails generate migration AddSuperadminToUser superadmin:boolean`
* Add `null: false, default: false` to the migration created in step 1.
```ruby
class AddSuperadminToUser < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :superadmin, :boolean, null: false, default: false
  end
end
```
* Configure ActiveAdmin
In `/initializers/active_admin.rb`
```ruby
config.authentication_method = :authenticate_active_admin_user!
config.current_user_method   = :current_user
config.logout_link_path      = :destroy_user_session_path
config.logout_link_method    = :delete
```
* Create the method `authenticate_active_admin_user!` in ApplicationController
```ruby
class ApplicationController < ActionController::Base
  include DeviseWhitelist
  include CurrentUserConcern

  def authenticate_active_admin_user!
    authenticate_user!
    unless current_user.superadmin?
      flash[:alert] = "Unauthorized Access!"
      redirect_to root_path
    end
  end
end
```

*  Cleaning up the code left behind by ActiveAdmin
`$ rails destroy model AdminUser`
`$ rm ./db/migrate/*_devise_create_admin_users.rb`

* The Devise routes that Admin Admin generated for the `AdminUser` model still exist and must be removed. For this delete `devise_for :admin_users, ActiveAdmin::Devise.config` from `routes.rb`

* Rename `app/admin/admin_users.rb` to `app/admin/users.rb`, add the `name` and `superadmin` params 
```ruby
ActiveAdmin.register User do
  permit_params :email, :name, :password, :password_confirmation, :superadmin

  index do
    selectable_column
    id_column
    column :email
    column :name
    column :superadmin
    column :current_sign_in_at
    column :sign_in_count
    column :created_at
    actions
  end

  filter :email
  filter :name
  filter :superadmin
  filter :current_sign_in_at
  filter :sign_in_count
  filter :created_at

  form do |f|
    f.inputs do
      f.input :email
      f.input :name
      f.input :password
      f.input :password_confirmation
      f.input :superadmin, :label => "Super Administrator"
    end
    f.actions
  end
end
```

* Update the seeds file to change the `AdminUser` to create an admin user.
```ruby
User.create!(email: "admin@example.com", name: "Admin Example", password: "password", password_confirmation: "password", superadmin: true) if Rails.env.development?
```

Let's reset the database `rails db:setup` and restart the server and see what's happens.

If everything worked, now the `AdminUser` and `User` is one. Now you can `create`, `edit` and `destroy` users in the Active Admin dashboard.

![Screen Shot 2020-09-14 at 14.21.32](https://dev-to-uploads.s3.amazonaws.com/i/gx6yh7t97yj6499sxgye.png)
![Screen Shot 2020-09-14 at 14.22.03](https://dev-to-uploads.s3.amazonaws.com/i/r0dcl0ph02ikf9a7d524.png)
  
###### Note: Installing Active Admin gem may do some changes to the styles.
You can avoid this adding `*= stub "active_admin"` to `app/assets/stylesheets/application.css`

```css
/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 *= stub "active_admin"
 */
```

#### Add Blogs and Comments to Active Admin
* `$ rails g active_admin:resource Blog`

```ruby
# app/admin/blogs.rb
ActiveAdmin.register Blog do
  permit_params :title, :body, :user_id
end
```

* `$ rails g active_admin:resource Comment`
Because ActiveAdmin has it's own built-in Comment model/class we have to change the name of the `Comment` resource.

```ruby
# app/admin/comments.rb
ActiveAdmin.register Comment, as: "BlogsComment" do
  permit_params :content, :blog_id, :user_id
end
```

#### Update the Active Admin dashboard with the Users, Blogs and Comments.

```ruby
# app/admin/dashboard.rb
ActiveAdmin.register_page "Dashboard" do
  menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }

  content title: proc { I18n.t("active_admin.dashboard") } do

    # Here is an example of a simple dashboard with columns and panels.

    columns do
      panel "Info" do
        para "Welcome to ActiveAdmin."
      end
      column do
        panel "Recent Users" do
          ul do
            User.last(5).reverse.map do |user|
              li link_to(user.email, admin_user_path(user))
            end
          end
        end
        panel "Recent Blogs" do
          ul do
            Blog.last(5).reverse.map do |blog|
              li link_to(blog.title, admin_blog_path(blog))
            end
          end
        end
      end

      column do
        panel "Recent Comments" do
          ul do
            Comment.last(5).reverse.map do |comment|
              li link_to(comment.content, admin_comment_path(comment))
            end
          end
        end
      end
    end
  end # content
end
```
![Screen Shot 2020-09-14 at 16.01.10](https://dev-to-uploads.s3.amazonaws.com/i/jfndhrlldk2wwm3qqo57.png)

 
#### Add link to admin panel in the application for admins users

In `app/views/blogs/index.html.erb` add
```ruby
<%= link_to 'Admin Panel',  admin_root_path, class: "adminlink", target: :_blank if current_user.superadmin? %>
```

Now, if you are an admin a link to access the Active Admin panel will appear.

### Create pretty URL’s for blogs instead of numeric ids.

For this we are going to use the `friendly-id` gem https://github.com/norman/friendly_id.

1. Add `gem 'friendly_id', '~> 5.1'` to the `Gemfile`
2. `$ rails g migration AddSlugToBlogs slug:uniq`
3. `$ rails generate friendly_id`
4. `$ rails db:migrate`
5. Add to blog model:
```ruby
extend FriendlyId
friendly_id :title, use: :slugged
```
Lastly let's edit the `app/controllers/blogs_controller.rb` file and replace `Blog.find` for `Blog.friendly.find`

Now when you create a blog with the title "my-blog-0", to access the blog show page you use the URL `http://localhost:3000/blogs/my-blog-0`

#### Update Active Admin config to work with friendlyId gem

In `config/initializers/active_admin.rb` add 
```ruby
  # == Friendly Id
  ActiveAdmin::ResourceController.class_eval do
    def find_resource
      resource_class.is_a?(FriendlyId) ? scoped_collection.friendly.find(params[:id]) : scoped_collection.find(params[:id])
    end
  end
```

### Let's add soft-delete to hide the records instead of delete them from the database.

For this I initially used `paranoia` gem but I encounter lots of issues so I change to [discard](https://github.com/jhawthorn/discard).

1. Add `gem 'discard', '~> 1.2'` to the `Gemfile`.
2. `$ bundle install`
3. Add `include Discard::Model` to the models you want to discard. In my case: `blog.rb`, `comment.rb` and `user.rb`
4. Generate the migrations to add `discarded_at` column.
  * `$ rails g migration add_discarded_at_to_blogs discarded_at:datetime:index`
  * `$ rails g migration add_discarded_at_to_users discarded_at:datetime:index`
  * `$ rails g migration add_discarded_at_to_comments discarded_at:datetime:index`

5. `$ rails db:migrate`
6. Update `Blog` and `Comments` controller to `discard` instead of `destroy`.
7. Add `kept` discard method to just bring the records that are not discarded.

```ruby
# blogs_controller.rb
class BlogsController < ApplicationController
  ...

  # GET /blogs
  # GET /blogs.json
  def index
    @blogs = Blog.kept.all
  end

  ...

  # DELETE /blogs/1
  # DELETE /blogs/1.json
  def destroy
    @blog.discard
    respond_to do |format|
      format.html { redirect_to blogs_url, notice: "Blog was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  def get_comments
    comments = @blog.comments.kept.select("comments.*, users.name").joins(:user).by_created_at
    render json: { comments: comments }
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_blog
    begin
      @blog = Blog.kept.friendly.find(params[:id])
    rescue ActiveRecord::RecordNotFound
      redirect_to root_path, alert: "Blog does not exists!"
      return
    end
  end

  ...
end
```

```ruby
# comments_controller.rb
class CommentsController < ApplicationController
  ...

  def destroy
    if @comment.discard
      head :no_content
    else
      render json: { error: @comment.errors.message }, status: 422
    end
  end

  private

  def set_comment
    @comment = Comment.kept.find(params[:id])
  end

  ...
end
```

##### If you want to discard the comments after discarding the blog you can add the `discard` callback `after_discard`. 

Example blogs model:

```ruby
class Blog < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged

  include Discard::Model

  validates_presence_of :title, :body

  has_many :comments, dependent: :destroy
  belongs_to :user

  after_discard do
    comments.discard_all
  end

  after_undiscard do
    comments.undiscard_all
  end
end
```

##### Add discard to Active Admin
For this we have to override the destroy action in Active Admin.

```ruby
# app/admin/blogs.rb
ActiveAdmin.register Blog do
  controller do
    def destroy
      @blog = Blog.friendly.find(params[:id])
      @blog.discard
      redirect_to admin_blogs_path
    end
  end

  permit_params :title, :body, :user_id
end
```

```ruby
# app/admin/comments.rb
ActiveAdmin.register Comment, as: "BlogsComment" do
  controller do
    def destroy
      @comment = Comment.find(params[:id])
      @comment.discard
      redirect_to admin_blogs_comments_path
    end
  end

  permit_params :content, :blog_id, :user_id
end
```

Now when we delete a `blog` or `comment` it doesn't get erase from the database, instead the column `discarded_at` gets populate with a timestamp.

For example, here we delete a blog with three comments. The blog is going to be discarded and all the comments too.
![Screen Shot 2020-09-15 at 14.09.38](https://dev-to-uploads.s3.amazonaws.com/i/ui8piledkwjgbb4pd45v.png)
![Screen Shot 2020-09-15 at 14.09.56](https://dev-to-uploads.s3.amazonaws.com/i/1dc0m4hecnrhk6i0f8xk.png)
  
* Do the same thing for users.
   * Add to `app/admin/users.rb`
```ruby
  controller do
    def destroy
      @user = User.find_by_id(params[:id])
      if @user != current_user
        @user.discard
        redirect_to admin_users_path
      end
    end
  end
```

   * Add to `app/models/user.rb`
```ruby
  after_discard do
    blogs.discard_all
    comments.discard_all
  end

  after_undiscard do
    blogs.undiscard_all
    comments.undiscard_all
  end
```
   * We also have to add `dependent: :destroy` to User relationship with `blogs` and `comments`. 

```ruby
# app/models/user.rb
  has_many :blogs, dependent: :destroy
  has_many :comments, dependent: :destroy
```

### Add restore feature to the Active Admin
For this we are going to add a `deleted_by` column to `User`, `Blog` and `Comment`, and then we are going to update it with the admin user that delete the record. 
We also are going to add a `Restore` button in Active Admin.

* `$ rails g migration AddDeleteByToUsers`
```ruby
class AddDeleteByToUsers < ActiveRecord::Migration[6.0]
  def change
    add_reference :users, :deleted_by
  end
end
```
* `$ rails db:migrate`
* Add `belongs_to :deleted_by, class_name: "User", optional: true` to `app/models/user.rb`.
* Add `@user.update(deleted_by: current_user)` to destroy action in `app/admin/users.rb`.
* Finally, add the restore button and functionally to `app/admin/users.rb`
```ruby
  action_item :restore, only: :show do
    link_to "Restore User", restore_admin_user_path(user), method: :put if user.discarded?
  end

  member_action :restore, method: :put do
    @user = User.find_by_id(params[:id])
    if @user != current_user
      @user.update(deleted_by: nil)
      @user.undiscard
      redirect_to admin_users_path
    end
  end
```
![Screen Shot 2020-09-15 at 15.24.34](https://dev-to-uploads.s3.amazonaws.com/i/uzscf7xezy8ummhp62kq.png)


* Do the same thing for Blogs and Comments.
  * `$ rails g migration AddDeleteByToBlogs`
  ```ruby
   class AddDeleteByToBlogs < ActiveRecord::Migration[6.0]
     def change
       add_reference :blogs, :deleted_by
     end
   end
  ```
  * `$ rails db:migrate`
  * Add `belongs_to :deleted_by, class_name: "User", optional: true` to `app/models/blog.rb`.
  * How `app/admin/blogs.rb` is looking.
  ```ruby
ActiveAdmin.register Blog do
  controller do
    def destroy
      @blog = Blog.friendly.find(params[:id])
      @blog.update(deleted_by: current_user)
      @blog.discard
      redirect_to admin_blogs_path
    end
  end

  action_item :restore, only: :show do
    link_to "Restore Blog", restore_admin_blog_path(blog), method: :put if blog.user.undiscarded? && blog.discarded?
  end

  member_action :restore, method: :put do
    @blog = Blog.friendly.find(params[:id])
    @blog.update(deleted_by: nil)
    @blog.undiscard
    redirect_to admin_blogs_path
  end

  permit_params :title, :body, :user_id
end
  ```

```ruby
# app/admin/comments.rb
ActiveAdmin.register Comment, as: "BlogsComment" do
  controller do
    def destroy
      @comment = Comment.find(params[:id])
      @comment.update(deleted_by: current_user)
      @comment.discard
      redirect_to admin_blogs_comments_path
    end
  end

  action_item :restore, only: :show do
    link_to "Restore Comment", restore_admin_blogs_comment_path(blogs_comment), method: :put if blogs_comment.user.undiscarded? && blogs_comment.discarded?
  end

  member_action :restore, method: :put do
    @comment = Comment.find(params[:id])
    @comment.update(deleted_by: nil)
    @comment.undiscard
    redirect_to admin_blogs_comments_path
  end

  permit_params :content, :blog_id, :user_id
end
```

#### One thing we have to adjust is when we call `undiscard_all`.

We need to tell discard to undiscard the records that where not discard by an admin user.

```ruby
# app/models/user.rb
 after_undiscard do
    blogs.where(deleted_by: nil).undiscard_all
    comments.where(deleted_by: nil).undiscard_all
  end
```
```ruby
# app/models/blog.rb
  after_undiscard do
    comments.where(deleted_by: nil).undiscard_all
  end
```

### Get started with [Action Text](https://edgeguides.rubyonrails.org/action_text_overview.html)

I used this video as a reference [how to use action text](https://gorails.com/episodes/how-to-use-action-text) 

1. Install Active Storage: `$ rails active_storage:install`
2. Install Active Text: `$ rails action_text:install`
3. `$ rails db:migrate`

This will create tables to store the rich text and attachments like images and audio files.

#### Add rich text to Blog
1. In `app/models/blog.rb` add `has_rich_text :body`
2. In `app/views/blogs/_form.html.erb` replace `<%= form.text_area :body %>` for `<%= form.rich_text_area :body %>`

If everything goes well it's going to look like this:
![Screen Shot 2020-09-15 at 18.22.33](https://dev-to-uploads.s3.amazonaws.com/i/cimd9wqe746ixgfovy99.png)
 
Now you can create blogs with more styles.
![Screen Shot 2020-09-15 at 18.27.08](https://dev-to-uploads.s3.amazonaws.com/i/ct933t0i4cfo91yxrcsv.png)

The image here is broken because you need to also add the gem `image_processing`.

#### Add images to Blog rich text body
1. Uncomment `gem 'image_processing', '~> 1.2'` in the `Gemfile`
2. `$ bundle install`
3. Restart server

Now we can see the image:
![Screen Shot 2020-09-15 at 18.32.48](https://dev-to-uploads.s3.amazonaws.com/i/qfklttjlhi5hajpf2o7v.png)
 
#### Truncate Blog body in index
If you don't want to show all the blog content in `app/views/blogs/index.html.erb` you can replace `<%= blog.body %>` for something like `<%= truncate(blog.body.to_plain_text , length: 300, omission: '...') { link_to 'Read More', blog } %>`

![Screen Shot 2020-09-15 at 18.38.49](https://dev-to-uploads.s3.amazonaws.com/i/k5b2uiszbokjab5zrl4l.png)
 
##### Let's clean up a little bit
1. Remove the old `body` column from the `blog` table.
  * `$ rails g migration remove_body_from_blogs`
  * Add `remove_column :blogs, :body`
2. `$ rails db:migrate`
3. Remove old `admin_users` table from the database.
  * `$ rails g migration delete_admin_user_table`
  * Add `drop_table :admin_users`
4. `$ rails db:migrate`


### Change styles of Trix editor
There may be other ways of doing this but honestly the only way we found was going to the [Trix source code](https://github.com/basecamp/trix/) and edit it.
What we needed to do was change the colors and remove some of the toolbar options. 

* Create `app/javascript/utils/trix.js`
* Create `app/javascript/stylesheets/application.scss`
* In `app/javascript/packs/application.js` replace `require("trix")` with the one you just created: `require("../utils/trix")`
* In `app/javascript/packs/application.js` add `require("../stylesheets/application.scss")`

In Trix source code there is this file `src/trix/config/toolbar.coffee`. Copy the content and change it to javascript. The result `app/javascript/utils/trix.js`

```javascript
import Trix from "trix";

if (Trix) {
  const { lang } = Trix.config;
  Trix.config.toolbar.getDefaultHTML = () => `
    <div class="trix-button-row">
      <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-bold my-button" data-trix-attribute="bold" data-trix-key="b" title="${lang.bold}" tabindex="-1">${lang.bold}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${lang.italic}" tabindex="-1">${lang.italic}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${lang.strike}" tabindex="-1">${lang.strike}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${lang.link}" tabindex="-1">${lang.link}</button>
      </span>

      <span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-heading-1" data-trix-attribute="heading1" title="${lang.heading1}" tabindex="-1">${lang.heading1}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-quote" data-trix-attribute="quote" title="${lang.quote}" tabindex="-1">${lang.quote}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-code" data-trix-attribute="code" title="${lang.code}" tabindex="-1">${lang.code}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${lang.bullets}" tabindex="-1">${lang.bullets}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${lang.numbers}" tabindex="-1">${lang.numbers}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-decrease-nesting-level" data-trix-action="decreaseNestingLevel" title="${lang.outdent}" tabindex="-1">${lang.outdent}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-increase-nesting-level" data-trix-action="increaseNestingLevel" title="${lang.indent}" tabindex="-1">${lang.indent}</button>
      </span>

      <span class="trix-button-group trix-button-group--file-tools" data-trix-button-group="file-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-attach" data-trix-action="attachFiles" title="${lang.attachFiles}" tabindex="-1">${lang.attachFiles}</button>
      </span>

      <span class="trix-button-group-spacer"></span>

      <span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
        <button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${lang.undo}" tabindex="-1">${lang.undo}</button>
        <button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${lang.redo}" tabindex="-1">${lang.redo}</button>
      </span>
    </div>

    <div class="trix-dialogs" data-trix-dialogs>
      <div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
        <div class="trix-dialog__link-fields">
          <input type="url" name="href" class="trix-input trix-input--dialog" placeholder="${lang.urlPlaceholder}" aria-label="${lang.url}" required data-trix-input>
          <div class="trix-button-group">
            <input type="button" class="trix-button trix-button--dialog" value="${lang.link}" data-trix-method="setAttribute">
            <input type="button" class="trix-button trix-button--dialog" value="${lang.unlink}" data-trix-method="removeAttribute">
          </div>
        </div>
      </div>
    </div>
      `;
} else {
  console.error(`Trix not yet loaded, toolbar not customized`);
}
```

Then in `app/javascript/stylesheets/application.scss` find the css classes and override the one you want. For example:
```scss
$opacity-active: 1;

trix-editor {
  min-height: 10em;
}

trix-toolbar {
  .trix-button-group {
    border: 1px solid #bbb;
    border-top-color: #ccc;
    border-bottom-color: #888;
  }

  .trix-button {
    border-bottom: 1px solid paleturquoise;
    background: paleturquoise;

    &:not(:first-child) {
      border-left: 1px solid #ccc;
    }

    &.trix-active {
      background: pink;
      color: rgba(0, 0, 0, $opacity-active);
    }
  }
}

.trix-content {
  max-height: 20rem !important;
  max-width: 100%;
  overflow-y: auto;
  overflow-x: auto;
  background-color: #edf2f7;
  border-radius: 0.5rem;
  padding: 1rem;
}
```

In `app/javascript/utils/trix.js` you can also remove elements that you don't want from the toolbar, like **bold** or *italics*. Or you can remove the Trix classes and create your own.

Example:
![Screen Shot 2020-09-15 at 19.57.27](https://dev-to-uploads.s3.amazonaws.com/i/2lqw0f7soq43q6ofgl7l.png)
 
### Uploading images to Amazon S3
One thing that is not mentioned in this article, otherwise it would be longer than it already is, is uploading images and audio files to, for example, Amazon S3. But with Active Storage it was pretty easy and straightforward.

## Conclusion

Overall I liked Rails. I love the simplicity and all the out-of-the-box solutions that already come with it like Action Text and Action Storage. But this experience didn't motivate me in continue to study the language. From little to none new articles, gems not being updated, and not a very active community on sites like Reddit or Stack Overflow makes me wonder: Is it worth it? I don't have an answer right now. 

What do you think?

Update: It's 2023 and I'm still using Rails :)