Jumpstart Pro - Instant Forum

Adds a functional forum for your Jumpstart Pro application.
Icons/chart bar
Used 35 times
Created by
R Romain Manguin

Usage
This template recreates the Jumpstart Pro forum for JSP users.

I may add the subscribe/unsubscribe to notifications functionality in a near future.

Run this command in your Rails app directory in the terminal:

rails app:template LOCATION="https://railsbytes.com/script/z5OsgJ"
Template Source

Review the code before running this template on your machine.

# Scaffold Discussion and generate Post model
run "rails g scaffold Discussions user:references title --skip-routes"
run "rails g model Post discussion:references user:references body:rich_text"
run "rails db:migrate"

# Generates the Discussion mailer allowing to notify the users
run "rails g mailer Discussion new_post"

# Adds associations to User model
string = <<-RUBY
  has_many :discussions
  has_many :posts
RUBY
inject_into_file "app/models/user.rb", string, after: "class User < ApplicationRecord\n"

# Adds associations, scope, validation, nested attributes and PgSearch to Discussion model
string = <<-RUBY
  include PgSearch::Model
  pg_search_scope :search_full_text, against: :title

  has_many :posts, dependent: :destroy
  has_many :users, through: :posts

  scope :sorted, ->{ order(updated_at: :desc) }

  validates :title, presence: true

  accepts_nested_attributes_for :posts
RUBY
inject_into_file "app/models/discussion.rb", string, after: "class Discussion < ApplicationRecord\n"

# Adds association, scope and notification method to Post model
string = <<-RUBY
  has_rich_text :body

  scope :sorted, ->{ order(created_at: :asc) }

  def send_notifications!
    users = discussion.users.uniq - [user]
    users.each do |user|
      DiscussionMailer.new_post(self, user).deliver_later
    end
  end
RUBY
inject_into_file "app/models/post.rb", string, after: "class Post < ApplicationRecord\n"

# Adds the needed routes
string = <<-RUBY
  resources :discussions do
    get :mine, on: :collection
    get :participating, on: :collection
    resources :posts
  end
RUBY
inject_into_file "config/routes.rb", string, after: "Rails.application.routes.draw do\n"

# Overrides DiscussionsController
run "rm -rf app/controllers/discussions_controller.rb"
run "touch app/controllers/discussions_controller.rb"
inject_into_file "app/controllers/discussions_controller.rb" do <<~EOF
  class DiscussionsController < ApplicationController
    before_action :authenticate_user!, except: [:index, :show]
    before_action :set_discussion, only: [:show, :edit, :update, :destroy]

    # GET /discussions
    def index
      @pagy, @discussions = params[:q] ? pagy(Discussion.search_full_text(params[:q])) : pagy(Discussion.all)
    end

    def mine
      discussions = current_user.discussions
      @pagy, @discussions = params[:q] ? pagy(discussions.search_full_text(params[:q])) : pagy(discussions)
      render template: 'discussions/index'
    end

    def participating
      discussions = Discussion.where(id: current_user.posts.pluck(:discussion_id).uniq)
      @pagy, @discussions = params[:q] ? pagy(discussions.search_full_text(params[:q])) : pagy(discussions)
      render template: 'discussions/index'
    end

    # GET /discussions/1
    def show
    end

    # GET /discussions/new
    def new
      @discussion = Discussion.new
      @discussion.posts.new
    end

    # GET /discussions/1/edit
    def edit
    end

    # POST /discussions
    def create
      @discussion = current_user.discussions.new(discussion_params)
      @discussion.posts.each { |post| post.user = current_user }
      if @discussion.save
        redirect_to @discussion, notice: 'Discussion was successfully created.'
      else
        render :new
      end
    end

    # PATCH/PUT /discussions/1
    def update
      if @discussion.update(discussion_params)
        redirect_to @discussion, notice: 'Discussion was successfully updated.'
      else
        render :edit
      end
    end

    # DELETE /discussions/1
    def destroy
      @discussion.destroy
      redirect_to discussions_url, notice: 'Discussion was successfully destroyed.'
    end

    private
      # Use callbacks to share common setup or constraints between actions.
      def set_discussion
        @discussion = Discussion.find(params[:id])
      end

      # Only allow a trusted parameter "white list" through.
      def discussion_params
        params.require(:discussion).permit(:user_id, :title, posts_attributes: [:body])
      end
  end
  EOF
end

# Overrides PostsController
run "rm -rf app/controllers/posts_controller.rb"
run "touch app/controllers/posts_controller.rb"
inject_into_file "app/controllers/posts_controller.rb" do <<~EOF
  class PostsController < ApplicationController
    before_action :set_discussion
    before_action :set_post, only: [:edit, :update, :destroy]

    def create
      @post = @discussion.posts.new(post_params)
      @post.user = current_user

      if @post.save
        @post.send_notifications!
        redirect_to discussion_path(@discussion, anchor: @post.id)
      else
        redirect_to @discussion, alert: @post.errors.full_messages.first
      end
    end

    def edit
    end

    def update
      if @post.update(post_params)
        redirect_to discussion_path(@discussion, anchor: @post.id)
      else
        redirect_to @discussion, alert: @post.errors.full_messages.first
      end
    end

    def destroy
      @post.destroy
      redirect_to @discussion
    end

    private

    def set_discussion
      @discussion = Discussion.find(params[:discussion_id])
    end

    def set_post
      @post = @discussion.posts.find_by(id: params[:id], user: current_user)
    end

    def post_params
      params.require(:post).permit(:body)
    end
  end
  EOF
end

# Overrides DiscussionMailer
run "rm -rf app/mailers/discussion_mailer.rb"
run "touch app/mailers/discussion_mailer.rb"
inject_into_file "app/mailers/discussion_mailer.rb" do <<~EOF
  class DiscussionMailer < ApplicationMailer
    # Subject can be set in your I18n file at config/locales/en.yml
    # with the following lookup:
    #
    #   en.discussion_mailer.new_post.subject
    #
    def new_post(post, user)
      @post, @user, @discussion = post, user, post.discussion

      mail to: \"\#{user.name} <\#{user.email}>\",
           from: \"\#{post.user.name} <[email protected]\#{Jumpstart.config.domain}>\",
           subject: \"[\#{Jumpstart.config.business_name}] \#{@discussion.title}\"
    end
  end
  EOF
end

# Overrides the discussion mailer view
run "rm -rf app/views/discussion_mailer/new_post.html.erb"
run "touch app/views/discussion_mailer/new_post.html.erb"
inject_into_file "app/views/discussion_mailer/new_post.html.erb" do <<~EOF
  <p>
  <%= image_tag avatar_url_for(@post.user), height: 20, width: 20, style: "border-radius:50%; vertical-align:middle;" %>
  <%= @post.user.name %> just posted a reply in <%= link_to @discussion.title, discussion_url(@discussion, anchor: @post.id), style: "font-weight:bold;" %>
  </p>

  <%= @post.body %>

  <p style="font-size:small;color:#666">
  -<br>
  You are receiving this because you are subscribed to this thread.<br>
  <%= link_to "View the discussion", discussion_url(@discussion, anchor: @post.id), target: :_blank %>
  </p>
  EOF
end

# Creates app/views/posts folder
run "mkdir app/views/posts"

# Creates app/views/posts/edit.html.erb view
run "touch app/views/posts/edit.html.erb"
inject_into_file "app/views/posts/edit.html.erb" do <<~EOF
  <div class="container mx-auto my-8 max-w-3xl bg-white shadow p-8">
    <h1 class="h2 mb-6">Editing Post</h1>

    <%= form_with model: [@discussion, @post], class: "flex-1" do |form| %>
      <div class="form-group">
        <%= form.rich_text_area :body, data: { controller: "mentions", target: "mentions.field" }  %>  
      </div>

      <div class="form-group flex justify-between">
        <%= link_to "Delete", [@discussion, @post], method: :delete, class: "btn btn-link text-red-700", data: { remote: true, confirm: "Are you sure?" } %>
        <div>
          <%= link_to "Cancel", @discussion, class: "btn btn-link" %>
          <%= form.submit "Save Changes", class: "btn btn-tertiary", data: { disable_with: "Saving..." } %>
        </div>
      </div>
    <% end %>
  </div>
  EOF
end

# Creates app/views/posts/_post.html.erb partial
run "touch app/views/posts/_post.html.erb"
inject_into_file "app/views/posts/_post.html.erb" do <<~EOF
  <div id="<%= dom_id(post) %>" class="flex pb-6 border-b mb-6 group">
    <div class="mr-6">
      <%= image_tag avatar_url_for(post.user), class: "rounded-full", height: 40, width: 40 %>
    </div>

    <div class="flex-1">
      <div class="mb-4">
        <span class="font-semibold"><%= post.user.name %></span>
        <span class="text-gray-600">•</span> <%= link_to local_time_ago(post.created_at), discussion_path(@discussion, anchor: post.id), class: "ml-1 hover:underline text-gray-700" %>
        <% if user_signed_in? && current_user == post.user %>
          <span class="text-gray-600">•</span> <%= link_to "Edit", edit_discussion_post_path(@discussion, post), class: "ml-1 hover:underline text-gray-700" %>
        <% end %>
      </div>
      <div>
        <%= post.body %>
      </div>
    </div>
  </div>
  EOF
end

# Overrides app/views/discussions/_form.html.erb partial
run "rm -rf app/views/discussions/_form.html.erb"
run "touch app/views/discussions/_form.html.erb"
inject_into_file "app/views/discussions/_form.html.erb" do <<~EOF
  <%= form_with(model: discussion) do |form| %>
    <%= render "shared/error_messages", resource: form.object %>

    <div class="form-group">
      <%= form.label :title %>
      <%= form.text_field :title, class: "form-control" %>
    </div>

    <% if form.object.new_record? %>
      <%= form.fields_for :posts do |post| %>
        <div class="form-group">
          <%= post.label :body %>
          <%= post.rich_text_area :body, data: { controller: "mentions", target: "mentions.field" } %>
        </div>
      <% end %>
    <% end %>

    <div class="form-group flex justify-between">
      <%= form.button class: "btn btn-primary" %>

      <% if form.object.persisted? %>
        <%= link_to 'Delete', form.object, class: "btn btn-danger outline", method: :delete, data: { remote: true, confirm: "Are you sure?" } %>
      <% end %>
    </div>
  <% end %>
  EOF
end

# Creates app/views/discussions/_search.html.erb partial
run "touch app/views/discussions/_search.html.erb"
inject_into_file "app/views/discussions/_search.html.erb" do <<~EOF
  <div class="lg:w-1/4 w-full">
    <%= form_with url: discussions_path, method: :get, local: true do |form| %>
      <div class="border border-gray-300 rounded-lg relative mb-2">
        <%= form.text_field :q, placeholder: "Search...", class: "transition focus:outline-0 border border-transparent focus:bg-white focus:border-gray-300 placeholder-gray-600 rounded-lg bg-gray-200 py-2 pr-4 pl-10 block w-full appearance-none leading-normal ds-input" %>
        <div class="pointer-events-none absolute inset-y-0 left-0 pl-4 flex items-center">
          <svg class="fill-current pointer-events-none text-gray-600 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"></path></svg>
        </div>
      </div>
    <% end %>
    <div class="bg-white border border-gray-300 rounded-lg">
      <%= nav_link_to "My Discussions", mine_discussions_path, class: "block no-underline p-3 px-6 rounded-t-lg", active_class: "bg-gray-200", inactive_class: " text-gray-700 hover:bg-gray-100" %>
      <%= nav_link_to "Participating", participating_discussions_path, class: "block no-underline p-3 px-6", active_class: "bg-gray-200", inactive_class: " text-gray-700 hover:bg-gray-100" %>
      <%= nav_link_to "All Discussions", discussions_path, class: "block no-underline p-3 px-6 rounded-b-lg", active_class: "bg-gray-200", inactive_class: " text-gray-700 hover:bg-gray-100" %>
    </div>
  </div>
  EOF
end

# Overrides app/views/discussions/index.html.erb view
run "rm -rf app/views/discussions/index.html.erb"
run "touch app/views/discussions/index.html.erb"
inject_into_file "app/views/discussions/index.html.erb" do <<~EOF
  <div class="container mx-auto my-8 px-4">
    <div class="flex justify-between items-center mb-4">
      <h1 class="h3">Discussions</h1>

      <% if @discussions.exists? %>
        <%= link_to 'New Discussion', new_discussion_path, class: "btn btn-primary" %>
      <% end %>
    </div>

    <% if @discussions.exists? %>
      <div class="flex items-start justify-between flex-wrap">
        <div class="lg:w-3/4 w-full lg:pr-8 lg:mb-0 mb-6">
          <div class="bg-white rounded-lg border">
            <table class="w-full">
              <tbody>
                <% @discussions.each do |discussion| %>
                  <tr class="group hover:bg-gray-100 border-b border-gray-200">
                    <td class="p-4 lg:w-1/4">
                      <%= link_to discussion, class: "flex items-center justify-start" do %>
                        <%= image_tag avatar_url_for(current_user), class: "rounded-full", height: 32, width: 32 %>
                        <span class="ml-4"><%= discussion.user.name %></span>
                      <% end %>
                    </td>
                    <td class="p-4"><%= link_to discussion.title, discussion %></td>
                    <td class="p-4 text-center"><%= discussion.posts.size %></td>
                  </tr>
                <% end %>
              </tbody>
            </table>
          </div>

          <% if @pagy.pages > 1 %>
            <div class="text-center my-6">
              <%== pagy_nav(@pagy) %>
            </div>
          <% end %>
        </div>

        <%= render partial: "search" %>
      </div>

    <% else %>
      <div class="bg-white rounded shadow flex flex-col items-center justify-between p-8 lg:flex-row">
        <%= image_tag "empty.svg", class: "mb-4 lg:w-1/2" %>
        <div class="flex-1 text-center">
          <p class="h3 mb-4">Create your first Discussion</p>
          <%= link_to 'New Discussion', new_discussion_path, class: "btn btn-primary" %>
        </div>
      </div>
    <% end %>
  </div>
  EOF
end

# Overrides app/views/discussions/show.html.erb view
run "rm -rf app/views/discussions/show.html.erb"
run "touch app/views/discussions/show.html.erb"
inject_into_file "app/views/discussions/show.html.erb" do <<~EOF
  <div class="container mx-auto my-8 px-4">
    <div class="flex justify-between items-center mb-4">
      <h1 class="h3"><%= link_to 'Discussions', discussions_path %> > <%= @discussion.title %></h1>
      <%= link_to 'Edit', edit_discussion_path(@discussion), class: "btn btn-link" %>
    </div>

    <div class="flex flex-wrap items-start justify-between">
      <div class="lg:w-3/4 w-full lg:pr-8 lg:mb-0 mb-6">
        <div class="bg-white border p-8 rounded-lg">
          <h1 class="h3 border-b pb-4 mb-4 font-black"><%= @discussion.title %></h1>

          <%= render @discussion.posts.sorted %>

          <% if user_signed_in? %>
            <div class="md:flex">
              <div class="mr-4 hidden md:flex-shrink-0 md:block">
                <%= image_tag avatar_url_for(current_user), class: "rounded-full", height: 40, width: 40 %>
              </div>

              <%= form_with model: [@discussion, Post.new], class: "flex-1" do |form| %>
                <div class="form-group">
                  <%= form.rich_text_area :body, data: { controller: "mentions", target: "mentions.field" }  %>  
                </div>

                <div class="form-group text-right">
                  <%= form.submit "Reply", class: "btn btn-tertiary", data: { disabled_with: "Submitting..." } %>
                </div>
              <% end %>
            </div>
          <% end %>
        </div>
      </div>

      <%= render partial: "search" %>
    </div>
  </div>
  EOF
end

puts "🥳 Your forum is here!"
Comments

Sign up or Login to leave a comment.

© 2022 GoRails, LLC