Jumpstart Pro - Instant Forum
Adds a functional forum for your Jumpstart Pro application.
Used 43 times
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.
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} <noreply@\#{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!"