mirror of
https://github.com/czhu12/canine.git
synced 2025-12-17 00:44:33 -06:00
Add files via upload
This commit is contained in:
44
CHANGELOG.md
Normal file
44
CHANGELOG.md
Normal file
@@ -0,0 +1,44 @@
|
||||
### 2021-12-27
|
||||
|
||||
* Support Rails 7.0
|
||||
* Drop support for Rails 5.2 and earlier
|
||||
|
||||
### 2021-05-13
|
||||
|
||||
* Upgrade to Bootstrap 5
|
||||
* Remove data-confirm-modal, not yet bootstrap 5 compatible
|
||||
* Drop jQuery
|
||||
* Include devise views directly
|
||||
|
||||
### 2021-03-04
|
||||
|
||||
* Switch to Madmin for admin area
|
||||
|
||||
### 2020-10-24
|
||||
|
||||
* Rescue from git configuration exception
|
||||
|
||||
### 2020-08-20
|
||||
|
||||
* Add tests for generating postgres and mysql apps
|
||||
|
||||
### 2020-08-07
|
||||
|
||||
* Refactor notifications to use the [Noticed gem](https://github.com/excid3/noticed)
|
||||
|
||||
### 2019-02-28
|
||||
|
||||
* Adds support for Rails 6.0
|
||||
* Move all Javascript to Webpacker for Rails 5.2 and 6.0
|
||||
* Use Bootstrap, data-confirm-modal, and local-time from NPM packages
|
||||
* ProvidePlugin sets jQuery, $, and Rails variables for webpacker
|
||||
* Use https://github.com/excid3/administrate fork of Administrate
|
||||
* Adds fix for zeitwerk autoloader in Rails 6
|
||||
* Adds support for virtual attributes
|
||||
* Add Procfile, Procfile.dev and .foreman configs
|
||||
* Add welcome message and instructions after completion
|
||||
|
||||
### 2019-01-02 and before
|
||||
|
||||
* Original version of Jumpstart
|
||||
* Supported Rails 5.2 only
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Chris Oliver
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
Procfile.dev
Normal file
4
Procfile.dev
Normal file
@@ -0,0 +1,4 @@
|
||||
web: bin/rails server -p 3000
|
||||
worker: bundle exec sidekiq
|
||||
js: yarn build --reload
|
||||
css: yarn build:css --watch
|
||||
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
> [!NOTE]
|
||||
> 👉 We've also built [Jumpstart Pro](https://jumpstartrails.com) which is a version of Jumpstart that includes payments with Stripe & Braintree, team accounts, TailwindCSS, and much more.
|
||||
|
||||
# Jumpstart Rails Template
|
||||
|
||||
All your Rails apps should start off with a bunch of great defaults. It's like Laravel Spark, for Rails.
|
||||
|
||||
Want to see how it works? Check out [the Jumpstart walkthrough video](https://www.youtube.com/watch?v=ssOZpISfIfI):
|
||||
|
||||
[](https://www.youtube.com/watch?v=ssOZpISfIfI)
|
||||
|
||||
## Getting Started
|
||||
|
||||
Jumpstart is a Rails template, so you pass it in as an option when creating a new app.
|
||||
|
||||
#### Requirements
|
||||
|
||||
You'll need the following installed to run the template successfully:
|
||||
|
||||
* Ruby 2.5 or higher
|
||||
* bundler - `gem install bundler`
|
||||
* rails - `gem install rails`
|
||||
* Database - we recommend Postgres, but you can use MySQL, SQLite3, etc
|
||||
* Redis - For ActionCable support
|
||||
* ImageMagick or libvips for ActiveStorage variants
|
||||
* Yarn - `brew install yarn` or [Install Yarn](https://yarnpkg.com/en/docs/install)
|
||||
* Foreman (optional) - `gem install foreman` - helps run all your processes in development
|
||||
|
||||
#### Creating a new app
|
||||
|
||||
```bash
|
||||
rails new myapp -d postgresql -m https://raw.githubusercontent.com/excid3/jumpstart/master/template.rb
|
||||
```
|
||||
|
||||
Or if you have downloaded this repo, you can reference template.rb locally:
|
||||
|
||||
```bash
|
||||
rails new myapp -d postgresql -m template.rb
|
||||
```
|
||||
|
||||
❓Having trouble? Try adding `DISABLE_SPRING=1` before `rails new`. Spring will get confused if you create an app with the same name twice.
|
||||
|
||||
#### Running your app
|
||||
|
||||
```bash
|
||||
bin/dev
|
||||
```
|
||||
|
||||
You can also run them in separate terminals manually if you prefer.
|
||||
|
||||
A separate `Procfile` is generated for deploying to production on Heroku.
|
||||
|
||||
#### Authenticate with social networks
|
||||
|
||||
We use the encrypted Rails Credentials for app_id and app_secrets when it comes to omniauth authentication. Edit them as so:
|
||||
|
||||
```
|
||||
EDITOR=vim rails credentials:edit
|
||||
```
|
||||
|
||||
Make sure your file follow this structure:
|
||||
|
||||
```yml
|
||||
secret_key_base: [your-key]
|
||||
development:
|
||||
github:
|
||||
app_id: something
|
||||
app_secret: something
|
||||
options:
|
||||
scope: 'user:email'
|
||||
whatever: true
|
||||
production:
|
||||
github:
|
||||
app_id: something
|
||||
app_secret: something
|
||||
options:
|
||||
scope: 'user:email'
|
||||
whatever: true
|
||||
```
|
||||
|
||||
With the environment, the service and the app_id/app_secret. If this is done correctly, you should see login links
|
||||
for the services you have added to the encrypted credentials using `EDITOR=vim rails credentials:edit`
|
||||
|
||||
#### Enabling Admin Panel
|
||||
App uses `madmin` [gem](https://github.com/excid3/madmin), so you need to run the madmin generator:
|
||||
|
||||
```
|
||||
rails g madmin:install
|
||||
```
|
||||
|
||||
This will install Madmin and generate resources for each of the models it finds.
|
||||
#### Redis set up
|
||||
##### On OSX
|
||||
```
|
||||
brew update
|
||||
brew install redis
|
||||
brew services start redis
|
||||
```
|
||||
##### Ubuntu
|
||||
```
|
||||
sudo apt-get install redis-server
|
||||
```
|
||||
|
||||
#### Cleaning up
|
||||
|
||||
```bash
|
||||
rails db:drop
|
||||
spring stop
|
||||
cd ..
|
||||
rm -rf myapp
|
||||
```
|
||||
9
Rakefile
Normal file
9
Rakefile
Normal file
@@ -0,0 +1,9 @@
|
||||
require "rake/testtask"
|
||||
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << "test"
|
||||
t.libs << "lib"
|
||||
t.test_files = FileList["test/**/*_test.rb"]
|
||||
end
|
||||
|
||||
task :default => :test
|
||||
2
app/assets/config/manifest.js
Normal file
2
app/assets/config/manifest.js
Normal file
@@ -0,0 +1,2 @@
|
||||
//= link_tree ../builds
|
||||
//= link_tree ../images
|
||||
21
app/assets/stylesheets/jumpstart/announcements.scss
Normal file
21
app/assets/stylesheets/jumpstart/announcements.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.announcement {
|
||||
strong {
|
||||
color: $gray-700;
|
||||
font-weight: 900;
|
||||
}
|
||||
}
|
||||
|
||||
.unread-announcements:before {
|
||||
-moz-border-radius: 50%;
|
||||
-webkit-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
-moz-background-clip: padding-box;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
background: $red;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
21
app/channels/application_cable/connection.rb
Normal file
21
app/channels/application_cable/connection.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
identified_by :current_user, :true_user
|
||||
impersonates :user
|
||||
|
||||
def connect
|
||||
self.current_user = find_verified_user
|
||||
logger.add_tags "ActionCable", "User #{current_user.id}"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def find_verified_user
|
||||
if (current_user = env['warden'].user)
|
||||
current_user
|
||||
else
|
||||
reject_unauthorized_connection
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
13
app/controllers/announcements_controller.rb
Normal file
13
app/controllers/announcements_controller.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class AnnouncementsController < ApplicationController
|
||||
before_action :mark_as_read, if: :user_signed_in?
|
||||
|
||||
def index
|
||||
@announcements = Announcement.order(published_at: :desc)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_as_read
|
||||
current_user.update(announcements_last_read_at: Time.zone.now)
|
||||
end
|
||||
end
|
||||
15
app/controllers/application_controller.rb
Normal file
15
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
impersonates :user
|
||||
include Pundit::Authorization
|
||||
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action :configure_permitted_parameters, if: :devise_controller?
|
||||
|
||||
protected
|
||||
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [:name, :avatar])
|
||||
end
|
||||
end
|
||||
10
app/controllers/home_controller.rb
Normal file
10
app/controllers/home_controller.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
end
|
||||
|
||||
def terms
|
||||
end
|
||||
|
||||
def privacy
|
||||
end
|
||||
end
|
||||
14
app/controllers/madmin/impersonates_controller.rb
Normal file
14
app/controllers/madmin/impersonates_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class Madmin::ImpersonatesController < Madmin::ApplicationController
|
||||
impersonates :user
|
||||
|
||||
def impersonate
|
||||
user = User.find(params[:id])
|
||||
impersonate_user(user)
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def stop_impersonating
|
||||
stop_impersonating_user
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
7
app/controllers/notifications_controller.rb
Normal file
7
app/controllers/notifications_controller.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class NotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@notifications = current_user.notifications.includes(:event)
|
||||
end
|
||||
end
|
||||
84
app/controllers/users/omniauth_callbacks_controller.rb
Normal file
84
app/controllers/users/omniauth_callbacks_controller.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
module Users
|
||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
before_action :set_service, except: [:failure]
|
||||
before_action :set_user, except: [:failure]
|
||||
|
||||
attr_reader :service, :user
|
||||
|
||||
def failure
|
||||
redirect_to root_path, alert: "Something went wrong"
|
||||
end
|
||||
|
||||
def facebook
|
||||
handle_auth "Facebook"
|
||||
end
|
||||
|
||||
def twitter
|
||||
handle_auth "Twitter"
|
||||
end
|
||||
|
||||
def github
|
||||
handle_auth "Github"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_auth(kind)
|
||||
if service.present?
|
||||
service.update(service_attrs)
|
||||
else
|
||||
user.services.create(service_attrs)
|
||||
end
|
||||
|
||||
if user_signed_in?
|
||||
flash[:notice] = "Your #{kind} account was connected."
|
||||
redirect_to edit_user_registration_path
|
||||
else
|
||||
sign_in_and_redirect user, event: :authentication
|
||||
set_flash_message :notice, :success, kind: kind
|
||||
end
|
||||
end
|
||||
|
||||
def auth
|
||||
request.env['omniauth.auth']
|
||||
end
|
||||
|
||||
def set_service
|
||||
@service = Service.where(provider: auth.provider, uid: auth.uid).first
|
||||
end
|
||||
|
||||
def set_user
|
||||
if user_signed_in?
|
||||
@user = current_user
|
||||
elsif service.present?
|
||||
@user = service.user
|
||||
elsif User.where(email: auth.info.email).any?
|
||||
# 5. User is logged out and they login to a new account which doesn't match their old one
|
||||
flash[:alert] = "An account with this email already exists. Please sign in with that account before connecting your #{auth.provider.titleize} account."
|
||||
redirect_to new_user_session_path
|
||||
else
|
||||
@user = create_user
|
||||
end
|
||||
end
|
||||
|
||||
def service_attrs
|
||||
expires_at = auth.credentials.expires_at.present? ? Time.at(auth.credentials.expires_at) : nil
|
||||
{
|
||||
provider: auth.provider,
|
||||
uid: auth.uid,
|
||||
expires_at: expires_at,
|
||||
access_token: auth.credentials.token,
|
||||
access_token_secret: auth.credentials.secret,
|
||||
}
|
||||
end
|
||||
|
||||
def create_user
|
||||
User.create(
|
||||
email: auth.info.email,
|
||||
#name: auth.info.name,
|
||||
password: Devise.friendly_token[0,20]
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
19
app/helpers/announcements_helper.rb
Normal file
19
app/helpers/announcements_helper.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module AnnouncementsHelper
|
||||
def unread_announcements(user)
|
||||
last_announcement = Announcement.order(published_at: :desc).first
|
||||
return if last_announcement.nil?
|
||||
|
||||
# Highlight announcements for anyone not logged in, cuz tempting
|
||||
if user.nil? || user.announcements_last_read_at.nil? || user.announcements_last_read_at < last_announcement.published_at
|
||||
"unread-announcements"
|
||||
end
|
||||
end
|
||||
|
||||
def announcement_class(type)
|
||||
{
|
||||
"new" => "text-success",
|
||||
"update" => "text-warning",
|
||||
"fix" => "text-danger",
|
||||
}.fetch(type, "text-success")
|
||||
end
|
||||
end
|
||||
17
app/helpers/avatar_helper.rb
Normal file
17
app/helpers/avatar_helper.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module AvatarHelper
|
||||
def avatar_path(object, options = {})
|
||||
size = options[:size] || 180
|
||||
default_image = options[:default] || "mp"
|
||||
base_url = "https://secure.gravatar.com/avatar"
|
||||
base_url_params = "?s=#{size}&d=#{default_image}"
|
||||
|
||||
if object.respond_to?(:avatar) && object.avatar.attached? && object.avatar.variable?
|
||||
object.avatar.variant(resize_to_fill: [size, size])
|
||||
elsif object.respond_to?(:email) && object.email
|
||||
gravatar_id = Digest::MD5::hexdigest(object.email.downcase)
|
||||
"#{base_url}/#{gravatar_id}#{base_url_params}"
|
||||
else
|
||||
"#{base_url}/00000000000000000000000000000000#{base_url_params}"
|
||||
end
|
||||
end
|
||||
end
|
||||
10
app/helpers/bootstrap_helper.rb
Normal file
10
app/helpers/bootstrap_helper.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
module BootstrapHelper
|
||||
def bootstrap_class_for(flash_type)
|
||||
{
|
||||
success: "alert-success",
|
||||
error: "alert-danger",
|
||||
alert: "alert-warning",
|
||||
notice: "alert-primary"
|
||||
}.stringify_keys[flash_type.to_s] || flash_type.to_s
|
||||
end
|
||||
end
|
||||
28
app/javascript/application.js
Normal file
28
app/javascript/application.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// This file is automatically compiled by Webpack, along with any other files
|
||||
// present in this directory. You're encouraged to place your actual application logic in
|
||||
// a relevant structure within app/javascript and only use these pack files to reference
|
||||
// that code so it'll be compiled.
|
||||
|
||||
import "@hotwired/turbo-rails"
|
||||
require("@rails/activestorage").start()
|
||||
//require("trix")
|
||||
//require("@rails/actiontext")
|
||||
require("local-time").start()
|
||||
require("@rails/ujs").start()
|
||||
|
||||
import './channels/**/*_channel.js'
|
||||
import "./controllers"
|
||||
|
||||
import * as bootstrap from "bootstrap"
|
||||
|
||||
document.addEventListener("turbo:load", () => {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
})
|
||||
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl)
|
||||
})
|
||||
})
|
||||
6
app/javascript/controllers/index.js
Normal file
6
app/javascript/controllers/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { application } from "./application"
|
||||
import controllers from './**/*_controller.js'
|
||||
controllers.forEach((controller) => {
|
||||
application.register(controller.name, controller.module.default)
|
||||
})
|
||||
|
||||
13
app/models/announcement.rb
Normal file
13
app/models/announcement.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class Announcement < ApplicationRecord
|
||||
TYPES = %w{ new fix update }
|
||||
|
||||
after_initialize :set_defaults
|
||||
|
||||
validates :announcement_type, :description, :name, :published_at, presence: true
|
||||
validates :announcement_type, inclusion: { in: TYPES }
|
||||
|
||||
def set_defaults
|
||||
self.published_at ||= Time.zone.now
|
||||
self.announcement_type ||= TYPES.first
|
||||
end
|
||||
end
|
||||
32
app/models/service.rb
Normal file
32
app/models/service.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class Service < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
Devise.omniauth_configs.keys.each do |provider|
|
||||
scope provider, ->{ where(provider: provider) }
|
||||
end
|
||||
|
||||
def client
|
||||
send("#{provider}_client")
|
||||
end
|
||||
|
||||
def expired?
|
||||
expires_at? && expires_at <= Time.zone.now
|
||||
end
|
||||
|
||||
def access_token
|
||||
send("#{provider}_refresh_token!", super) if expired?
|
||||
super
|
||||
end
|
||||
|
||||
|
||||
def twitter_client
|
||||
Twitter::REST::Client.new do |config|
|
||||
config.consumer_key = Rails.application.secrets.twitter_app_id
|
||||
config.consumer_secret = Rails.application.secrets.twitter_app_secret
|
||||
config.access_token = access_token
|
||||
config.access_token_secret = access_token_secret
|
||||
end
|
||||
end
|
||||
|
||||
def twitter_refresh_token!(token); end
|
||||
end
|
||||
12
app/models/user.rb
Normal file
12
app/models/user.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class User < ApplicationRecord
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable and :omniauthable
|
||||
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :omniauthable
|
||||
|
||||
has_one_attached :avatar
|
||||
has_person_name
|
||||
|
||||
has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
|
||||
has_many :notification_mentions, as: :record, dependent: :destroy, class_name: "Noticed::Event"
|
||||
has_many :services
|
||||
end
|
||||
28
app/views/announcements/index.html.erb
Normal file
28
app/views/announcements/index.html.erb
Normal file
@@ -0,0 +1,28 @@
|
||||
<h1>What's New</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<% @announcements.each_with_index do |announcement, index| %>
|
||||
<% if index != 0 %>
|
||||
<div class="row"><div class="col"><hr /></div></div>
|
||||
<% end %>
|
||||
|
||||
<div class="row announcement" id="<%= dom_id(announcement) %>">
|
||||
<div class="col-sm-1 text-center">
|
||||
<%= link_to announcements_path(anchor: dom_id(announcement)) do %>
|
||||
<strong><%= announcement.published_at.strftime("%b %d") %></strong>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="col">
|
||||
<strong class="<%= announcement_class(announcement.announcement_type) %>"><%= announcement.announcement_type.titleize %>:</strong>
|
||||
<strong><%= announcement.name %></strong>
|
||||
<%= simple_format announcement.description %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @announcements.empty? %>
|
||||
<div>Exciting stuff coming very soon!</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
22
app/views/devise/confirmations/new.html.erb
Normal file
22
app/views/devise/confirmations/new.html.erb
Normal file
@@ -0,0 +1,22 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h2 class="text-center">Resend confirmation instructions</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.label :email, class: 'form-label' %><br />
|
||||
<%= f.email_field :email, autofocus: true, class: 'form-control', value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
|
||||
</div>
|
||||
|
||||
<div class="actions d-grid">
|
||||
<%= f.submit "Resend confirmation instructions", class: 'btn btn-primary btn-lg' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="text-center">
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Welcome <%= @email %>!</p>
|
||||
|
||||
<p>You can confirm your account email through the link below:</p>
|
||||
|
||||
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
|
||||
7
app/views/devise/mailer/email_changed.html.erb
Normal file
7
app/views/devise/mailer/email_changed.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<p>Hello <%= @email %>!</p>
|
||||
|
||||
<% if @resource.try(:unconfirmed_email?) %>
|
||||
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
|
||||
<% else %>
|
||||
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
|
||||
<% end %>
|
||||
3
app/views/devise/mailer/password_change.html.erb
Normal file
3
app/views/devise/mailer/password_change.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>We're contacting you to notify you that your password has been changed.</p>
|
||||
@@ -0,0 +1,8 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
|
||||
|
||||
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
|
||||
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
<p>Your password won't change until you access the link above and create a new one.</p>
|
||||
7
app/views/devise/mailer/unlock_instructions.html.erb
Normal file
7
app/views/devise/mailer/unlock_instructions.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<p>Hello <%= @resource.email %>!</p>
|
||||
|
||||
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
|
||||
|
||||
<p>Click the link below to unlock your account:</p>
|
||||
|
||||
<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
|
||||
29
app/views/devise/passwords/edit.html.erb
Normal file
29
app/views/devise/passwords/edit.html.erb
Normal file
@@ -0,0 +1,29 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h2 class="text-center">Change your password</h2>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
<%= f.hidden_field :reset_password_token %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password, autofocus: true, autocomplete: "off", class: 'form-control', placeholder: "Password" %>
|
||||
<% if @minimum_password_length %>
|
||||
<p class="text-muted"><small><%= @minimum_password_length %> characters minimum</small></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: "Confirm Password" %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-grid">
|
||||
<%= f.submit "Change my password", class: 'btn btn-primary btn-lg' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="text-center">
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
20
app/views/devise/passwords/new.html.erb
Normal file
20
app/views/devise/passwords/new.html.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h2 class="text-center">Reset your password</h2>
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
<p class="text-center">Enter your email address below and we will send you a link to reset your password.</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.email_field :email, autofocus: true, placeholder: 'Email address', class: 'form-control' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-grid">
|
||||
<%= f.submit "Send password reset email", class: 'btn btn-primary btn-lg' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="text-center">
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
49
app/views/devise/registrations/edit.html.erb
Normal file
49
app/views/devise/registrations/edit.html.erb
Normal file
@@ -0,0 +1,49 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h1 class="text-center">Account</h1>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.email_field :email, class: 'form-control', placeholder: 'Email Address' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.label :avatar, class: "form-label" %>
|
||||
<%= f.file_field :avatar, accept:'image/*' %>
|
||||
</div>
|
||||
|
||||
<%= image_tag avatar_path(f.object), class: "rounded border shadow-sm d-block mx-auto my-3" %>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div class="alert alert-warning">Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %>
|
||||
<p class="form-text text-muted"><small>Leave password blank if you don't want to change it</small></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :current_password, autocomplete: "off", class: 'form-control', placeholder: 'Current Password' %>
|
||||
<p class="form-text text-muted"><small>We need your current password to confirm your changes</small></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-grid">
|
||||
<%= f.submit "Save Changes", class: 'btn btn-lg btn-primary' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<hr>
|
||||
|
||||
<p class="text-center"><%= link_to "Deactivate my account", registration_path(resource_name), data: { confirm: "Are you sure? You cannot undo this." }, method: :delete %></p>
|
||||
</div>
|
||||
</div>
|
||||
33
app/views/devise/registrations/new.html.erb
Normal file
33
app/views/devise/registrations/new.html.erb
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h1 class="text-center">Sign Up</h1>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.text_field :name, autofocus: false, class: 'form-control', placeholder: "Full name" %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.email_field :email, autofocus: false, class: 'form-control', placeholder: "Email Address" %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password, autocomplete: "off", class: 'form-control', placeholder: 'Password' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control', placeholder: 'Confirm Password' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 d-grid">
|
||||
<%= f.submit "Sign up", class: "btn btn-primary btn-lg" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="text-center">
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
32
app/views/devise/sessions/new.html.erb
Normal file
32
app/views/devise/sessions/new.html.erb
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h1 class="text-center">Log in</h1>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
|
||||
<div class="mb-3">
|
||||
<%= f.email_field :email, autofocus: true, placeholder: 'Email Address', class: 'form-control' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.password_field :password, autocomplete: "off", placeholder: 'Password', class: 'form-control' %>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.rememberable? -%>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<%= f.check_box :remember_me, class: "form-check-input" %>
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<% end -%>
|
||||
|
||||
<div class="mb-3 d-grid">
|
||||
<%= f.submit "Log in", class: "btn btn-primary btn-lg" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="text-center">
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
15
app/views/devise/shared/_error_messages.html.erb
Normal file
15
app/views/devise/shared/_error_messages.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
<% if resource.errors.any? %>
|
||||
<div id="error_explanation" class="alert alert-danger">
|
||||
<h6>
|
||||
<%= I18n.t("errors.messages.not_saved",
|
||||
count: resource.errors.count,
|
||||
resource: resource.class.model_name.human.downcase)
|
||||
%>
|
||||
</h6>
|
||||
<ul>
|
||||
<% resource.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
25
app/views/devise/shared/_links.html.erb
Normal file
25
app/views/devise/shared/_links.html.erb
Normal file
@@ -0,0 +1,25 @@
|
||||
<%- if controller_name != 'sessions' %>
|
||||
<%= link_to "Log in", new_session_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
|
||||
<% end %>
|
||||
|
||||
<%- if devise_mapping.omniauthable? %>
|
||||
<%- resource_class.omniauth_providers.each do |provider| %>
|
||||
<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %><br />
|
||||
<% end %>
|
||||
<% end %>
|
||||
20
app/views/devise/unlocks/new.html.erb
Normal file
20
app/views/devise/unlocks/new.html.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4 offset-lg-4">
|
||||
<h1 class="text-center">Resend unlock instructions</h1>
|
||||
|
||||
<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
|
||||
<%= render "devise/shared/error_messages", resource: resource %>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.label :email, class: "form-label" %>
|
||||
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "form-control" %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= f.submit "Resend unlock instructions", class: "btn btn-lg btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "devise/shared/links" %>
|
||||
</div>
|
||||
</div>
|
||||
2
app/views/home/index.html.erb
Normal file
2
app/views/home/index.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Welcome to Jumpstart!</h1>
|
||||
<p class="lead">Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.</p>
|
||||
2
app/views/home/privacy.html.erb
Normal file
2
app/views/home/privacy.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="lead">Use this for your Privacy Policy</p>
|
||||
2
app/views/home/terms.html.erb
Normal file
2
app/views/home/terms.html.erb
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Terms of Service</h1>
|
||||
<p class="lead">Use this for your Terms of Service</p>
|
||||
19
app/views/layouts/application.html.erb
Normal file
19
app/views/layouts/application.html.erb
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<%= render 'shared/head' %>
|
||||
</head>
|
||||
|
||||
<body class="d-flex flex-column h-100">
|
||||
<main class="flex-shrink-0">
|
||||
<%= render 'shared/navbar' %>
|
||||
<%= render 'shared/notices' %>
|
||||
|
||||
<div class="container mt-4 mx-auto">
|
||||
<%= yield %>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<%= render 'shared/footer' %>
|
||||
</body>
|
||||
</html>
|
||||
10
app/views/notifications/index.html.erb
Normal file
10
app/views/notifications/index.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<h1>Notifications</h1>
|
||||
|
||||
<ul>
|
||||
<% @notifications.each do |notification| %>
|
||||
<%# Customize your notification format here. We typically recommend a `message` and `url` method on the Notifier classes. %>
|
||||
<%#= link_to notification.message, notification.url %>
|
||||
|
||||
<li><%= notification.params %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
10
app/views/shared/_footer.html.erb
Normal file
10
app/views/shared/_footer.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<footer class="bg-light footer mt-auto py-3">
|
||||
<div class="container d-flex justify-content-between mx-auto text-muted">
|
||||
<span>© <%= Date.today.year %> Your Company</span>
|
||||
<ul class="d-inline float-end list-inline mb-0">
|
||||
<li class="list-inline-item mr-3"><%= link_to "Terms", terms_path %></li>
|
||||
<li class="list-inline-item mr-3"><%= link_to "Privacy", privacy_path %></li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
10
app/views/shared/_head.html.erb
Normal file
10
app/views/shared/_head.html.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<title><%= Rails.configuration.application_name %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css" integrity="sha512-YWzhKL2whUzgiheMoBFwW8CKV4qpHQAEuvilg9FAn5VJUDwKZZxkJNuGM4XkWuk94WCrrwslk8yWNGmY1EduTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<%= stylesheet_link_tag "application", media: "all", "data-turbo-track": "reload" %>
|
||||
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
|
||||
52
app/views/shared/_navbar.html.erb
Normal file
52
app/views/shared/_navbar.html.erb
Normal file
@@ -0,0 +1,52 @@
|
||||
<% if current_user != true_user %>
|
||||
<div class="alert alert-warning text-center">
|
||||
You're logged in as <b><%= current_user.name %> (<%= current_user.email %>)</b>
|
||||
<%= link_to stop_impersonating_madmin_impersonates_path, method: :post do %><%= icon("fas", "times") %> Logout <% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<div class="container mx-auto">
|
||||
<%= link_to Rails.configuration.application_name, root_path, class: "navbar-brand" %>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarMain" aria-controls="navbarsExample04" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarMain">
|
||||
<ul class="navbar-nav me-auto mt-2 mt-md-0">
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><%= link_to "What's New", announcements_path, class: "nav-link #{unread_announcements(current_user)}" %></li>
|
||||
<% if user_signed_in? %>
|
||||
|
||||
<li class="nav-item">
|
||||
<%= link_to notifications_path, class: "nav-link" do %>
|
||||
<span><i class="far fa-bell" aria-hidden="true"></i></span>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<%= link_to "#", id: "navbar-dropdown", class: "nav-link dropdown-toggle", data: { target: "nav-account-dropdown", bs_toggle: "dropdown" }, aria: { haspopup: true, expanded: false } do %>
|
||||
<%= image_tag avatar_path(current_user, size: 40), height: 20, width: 20, class: "rounded" %>
|
||||
<% end %>
|
||||
<div id="nav-account-dropdown" class="dropdown-menu dropdown-menu-end" aria-labelledby="navbar-dropdown">
|
||||
<%= link_to "Settings", edit_user_registration_path, class: "dropdown-item" %>
|
||||
<% if current_user.admin? && respond_to?(:madmin_root_path) %>
|
||||
<div class="dropdown-divider"></div>
|
||||
<%= link_to "Admin Area", madmin_root_path, class: "dropdown-item" %>
|
||||
<% end %>
|
||||
<div class="dropdown-divider"></div>
|
||||
<%= button_to "Logout", destroy_user_session_path, method: :delete, class: "dropdown-item" %>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<% else %>
|
||||
<li class="nav-item"><%= link_to "Sign Up", new_user_registration_path, class: "nav-link" %></li>
|
||||
<li class="nav-item"><%= link_to "Login", new_user_session_path, class: "nav-link" %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
8
app/views/shared/_notices.html.erb
Normal file
8
app/views/shared/_notices.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<% flash.each do |msg_type, message| %>
|
||||
<div class="container mt-4 mx-auto">
|
||||
<div class="alert <%= bootstrap_class_for(msg_type) %> alert-dismissible fade show" role="alert">
|
||||
<%= message %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
13
config/cable.yml
Normal file
13
config/cable.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
development:
|
||||
adapter: redis
|
||||
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
|
||||
channel_prefix: streaming_logs_dev
|
||||
|
||||
test:
|
||||
adapter: async
|
||||
|
||||
production:
|
||||
adapter: redis
|
||||
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
|
||||
channel_prefix: streaming_logs_production
|
||||
|
||||
96
esbuild.config.mjs
Normal file
96
esbuild.config.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Esbuild is configured with 3 modes:
|
||||
//
|
||||
// `yarn build` - Build JavaScript and exit
|
||||
// `yarn build --watch` - Rebuild JavaScript on change
|
||||
// `yarn build --reload` - Reloads page when views, JavaScript, or stylesheets change
|
||||
//
|
||||
// Minify is enabled when "RAILS_ENV=production"
|
||||
// Sourcemaps are enabled in non-production environments
|
||||
|
||||
import * as esbuild from "esbuild"
|
||||
import path from "path"
|
||||
import rails from "esbuild-rails"
|
||||
import chokidar from "chokidar"
|
||||
import http from "http"
|
||||
import { setTimeout } from "timers/promises"
|
||||
|
||||
const clients = []
|
||||
const entryPoints = [
|
||||
"application.js"
|
||||
]
|
||||
const watchDirectories = [
|
||||
"./app/javascript/**/*.js",
|
||||
"./app/views/**/*.erb",
|
||||
"./app/assets/builds/**/*.css", // Wait for cssbundling changes
|
||||
]
|
||||
const config = {
|
||||
absWorkingDir: path.join(process.cwd(), "app/javascript"),
|
||||
bundle: true,
|
||||
entryPoints: entryPoints,
|
||||
minify: process.env.RAILS_ENV == "production",
|
||||
outdir: path.join(process.cwd(), "app/assets/builds"),
|
||||
plugins: [rails()],
|
||||
sourcemap: process.env.RAILS_ENV != "production"
|
||||
}
|
||||
|
||||
async function buildAndReload() {
|
||||
// Foreman & Overmind assign a separate PORT for each process
|
||||
const port = parseInt(process.env.PORT)
|
||||
const context = await esbuild.context({
|
||||
...config,
|
||||
banner: {
|
||||
js: ` (() => new EventSource("http://localhost:${port}").onmessage = () => location.reload())();`,
|
||||
}
|
||||
})
|
||||
|
||||
// Reload uses an HTTP server as an even stream to reload the browser
|
||||
http
|
||||
.createServer((req, res) => {
|
||||
return clients.push(
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
Connection: "keep-alive",
|
||||
})
|
||||
)
|
||||
})
|
||||
.listen(port)
|
||||
|
||||
await context.rebuild()
|
||||
console.log("[reload] initial build succeeded")
|
||||
|
||||
let ready = false
|
||||
chokidar
|
||||
.watch(watchDirectories)
|
||||
.on("ready", () => {
|
||||
console.log("[reload] ready")
|
||||
ready = true
|
||||
})
|
||||
.on("all", async (event, path) => {
|
||||
if (ready === false) return
|
||||
|
||||
if (path.includes("javascript")) {
|
||||
try {
|
||||
await setTimeout(20)
|
||||
await context.rebuild()
|
||||
console.log("[reload] build succeeded")
|
||||
} catch (error) {
|
||||
console.error("[reload] build failed", error)
|
||||
}
|
||||
}
|
||||
clients.forEach((res) => res.write("data: update\n\n"))
|
||||
clients.length = 0
|
||||
})
|
||||
}
|
||||
|
||||
if (process.argv.includes("--reload")) {
|
||||
buildAndReload()
|
||||
} else if (process.argv.includes("--watch")) {
|
||||
let context = await esbuild.context({...config, logLevel: 'info'})
|
||||
context.watch()
|
||||
} else {
|
||||
esbuild.build(config)
|
||||
}
|
||||
8
github/workflows/verify.yml
Normal file
8
github/workflows/verify.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
# See https://github.com/andyw8/setup-rails for more information
|
||||
|
||||
name: Verify
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
uses: andyw8/setup-rails/.github/workflows/verify.yml@v1
|
||||
50
lib/templates/erb/scaffold/_form.html.erb
Normal file
50
lib/templates/erb/scaffold/_form.html.erb
Normal file
@@ -0,0 +1,50 @@
|
||||
<%%= form_with(model: <%= model_resource_name %>) do |form| %>
|
||||
<%% if <%= singular_table_name %>.errors.any? %>
|
||||
<div id="error_explanation">
|
||||
<h2><%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
|
||||
|
||||
<ul>
|
||||
<%% <%= singular_table_name %>.errors.full_messages.each do |message| %>
|
||||
<li><%%= message %></li>
|
||||
<%% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<%% end %>
|
||||
|
||||
<% attributes.each do |attribute| -%>
|
||||
<div class="mb-3">
|
||||
<% if attribute.password_digest? -%>
|
||||
<%%= form.label :password, class: 'form-label' %>
|
||||
<%%= form.password_field :password, class: 'form-control' %>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%%= form.label :password_confirmation, class: 'form-label' %>
|
||||
<%%= form.password_field :password_confirmation, class: 'form-control' %>
|
||||
<% else -%>
|
||||
<%%= form.label :<%= attribute.column_name %>, class: 'form-label' %>
|
||||
<% if attribute.field_type == "checkbox" -%>
|
||||
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %>
|
||||
<% else -%>
|
||||
<%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, class: 'form-control' %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
</div>
|
||||
|
||||
<% end -%>
|
||||
<div class="mb-3">
|
||||
<%% if <%= model_resource_name %>.persisted? %>
|
||||
<div class="float-end">
|
||||
<%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, class: "text-danger", data: { confirm: 'Are you sure?' } %>
|
||||
</div>
|
||||
<%% end %>
|
||||
|
||||
<%%= form.submit class: 'btn btn-primary' %>
|
||||
|
||||
<%% if <%= model_resource_name %>.persisted? %>
|
||||
<%%= link_to "Cancel", <%= model_resource_name %>, class: "btn btn-link" %>
|
||||
<%% else %>
|
||||
<%%= link_to "Cancel", <%= index_helper %>_path, class: "btn btn-link" %>
|
||||
<%% end %>
|
||||
</div>
|
||||
<%% end %>
|
||||
3
lib/templates/erb/scaffold/edit.html.erb
Normal file
3
lib/templates/erb/scaffold/edit.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>Edit <%= singular_table_name.capitalize %></h1>
|
||||
|
||||
<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %>
|
||||
51
lib/templates/erb/scaffold/index.html.erb
Normal file
51
lib/templates/erb/scaffold/index.html.erb
Normal file
@@ -0,0 +1,51 @@
|
||||
<% name_attribute = attributes.find{ |a| a.name == "name" } %>
|
||||
<% has_name = !!name_attribute %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h1><%= plural_table_name.capitalize %></h1>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 text-end">
|
||||
<%%= link_to new_<%= singular_table_name %>_path, class: 'btn btn-primary' do %>
|
||||
Add New <%= human_name %>
|
||||
<%% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<% if has_name %>
|
||||
<th>Name</th>
|
||||
<% end %>
|
||||
|
||||
<% attributes.without(name_attribute).each do |attribute| -%>
|
||||
<th><%= attribute.human_name %></th>
|
||||
<% end -%>
|
||||
<% unless has_name %>
|
||||
<th></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<%% @<%= plural_table_name%>.each do |<%= singular_table_name %>| %>
|
||||
<%%= content_tag :tr, id: dom_id(<%= singular_table_name %>), class: dom_class(<%= singular_table_name %>) do %>
|
||||
<% if has_name %>
|
||||
<td><%%= link_to <%= singular_table_name %>.name, <%= singular_table_name %> %></td>
|
||||
<% end %>
|
||||
|
||||
<% attributes.without(name_attribute).each do |attribute| -%>
|
||||
<td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
|
||||
<% end -%>
|
||||
|
||||
<% unless has_name %>
|
||||
<td><%%= link_to 'Show', <%= singular_table_name %> %></td>
|
||||
<% end %>
|
||||
<%% end %>
|
||||
<%% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
3
lib/templates/erb/scaffold/new.html.erb
Normal file
3
lib/templates/erb/scaffold/new.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>New <%= singular_table_name %></h1>
|
||||
|
||||
<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %>
|
||||
16
lib/templates/erb/scaffold/show.html.erb
Normal file
16
lib/templates/erb/scaffold/show.html.erb
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="page-header">
|
||||
<%%= link_to <%= index_helper %>_path, class: 'btn btn-default' do %>
|
||||
All <%= plural_table_name.capitalize %>
|
||||
<%% end %>
|
||||
<%%= link_to edit_<%= singular_table_name %>_path(@<%= singular_table_name %>), class: 'btn btn-primary' do %>
|
||||
Edit
|
||||
<%% end %>
|
||||
<h1>Show <%= singular_table_name %></h1>
|
||||
</div>
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<%- attributes.each do |attribute| -%>
|
||||
<dt><%= attribute.human_name %>:</dt>
|
||||
<dd><%%= @<%= singular_table_name %>.<%= attribute.name %> %></dd>
|
||||
<%- end -%>
|
||||
</dl>
|
||||
280
template.rb
Normal file
280
template.rb
Normal file
@@ -0,0 +1,280 @@
|
||||
require "fileutils"
|
||||
require "shellwords"
|
||||
|
||||
# Copied from: https://github.com/mattbrictson/rails-template
|
||||
# Add this template directory to source_paths so that Thor actions like
|
||||
# copy_file and template resolve against our source files. If this file was
|
||||
# invoked remotely via HTTP, that means the files are not present locally.
|
||||
# In that case, use `git clone` to download them to a local temporary dir.
|
||||
def add_template_repository_to_source_path
|
||||
if __FILE__ =~ %r{\Ahttps?://}
|
||||
require "tmpdir"
|
||||
source_paths.unshift(tempdir = Dir.mktmpdir("jumpstart-"))
|
||||
at_exit { FileUtils.remove_entry(tempdir) }
|
||||
git clone: [
|
||||
"--quiet",
|
||||
"https://github.com/excid3/jumpstart.git",
|
||||
tempdir
|
||||
].map(&:shellescape).join(" ")
|
||||
|
||||
if (branch = __FILE__[%r{jumpstart/(.+)/template.rb}, 1])
|
||||
Dir.chdir(tempdir) { git checkout: branch }
|
||||
end
|
||||
else
|
||||
source_paths.unshift(File.dirname(__FILE__))
|
||||
end
|
||||
end
|
||||
|
||||
def read_gemfile?
|
||||
File.open("Gemfile").each_line do |line|
|
||||
return true if line.strip.start_with?("rails") && line.include?("6.")
|
||||
end
|
||||
end
|
||||
|
||||
def rails_version
|
||||
@rails_version ||= Gem::Version.new(Rails::VERSION::STRING) || read_gemfile?
|
||||
end
|
||||
|
||||
def rails_7_or_newer?
|
||||
Gem::Requirement.new(">= 7.0.0.alpha").satisfied_by? rails_version
|
||||
end
|
||||
|
||||
unless rails_7_or_newer?
|
||||
say "\nJumpstart requires Rails 7 or newer. You are using #{rails_version}.", :green
|
||||
say "Please remove partially installed Jumpstart files #{original_app_name} and try again.", :green
|
||||
exit 1
|
||||
end
|
||||
|
||||
def add_gems
|
||||
add_gem 'cssbundling-rails'
|
||||
add_gem 'devise', '~> 4.9'
|
||||
add_gem 'friendly_id', '~> 5.4'
|
||||
add_gem 'jsbundling-rails'
|
||||
add_gem 'madmin'
|
||||
add_gem 'name_of_person', github: "basecamp/name_of_person" # '~> 1.1'
|
||||
add_gem 'noticed', '~> 2.0'
|
||||
add_gem 'omniauth-facebook', '~> 8.0'
|
||||
add_gem 'omniauth-github', '~> 2.0'
|
||||
add_gem 'omniauth-twitter', '~> 1.4'
|
||||
add_gem 'pretender', '~> 0.3.4'
|
||||
add_gem 'pundit', '~> 2.1'
|
||||
add_gem 'sidekiq', '~> 6.2'
|
||||
add_gem 'sitemap_generator', '~> 6.1'
|
||||
add_gem 'whenever', require: false
|
||||
add_gem 'responders', github: 'heartcombo/responders', branch: 'main'
|
||||
end
|
||||
|
||||
def set_application_name
|
||||
# Add Application Name to Config
|
||||
environment "config.application_name = Rails.application.class.module_parent_name"
|
||||
|
||||
# Announce the user where they can change the application name in the future.
|
||||
puts "You can change application name inside: ./config/application.rb"
|
||||
end
|
||||
|
||||
def add_users
|
||||
route "root to: 'home#index'"
|
||||
generate "devise:install"
|
||||
|
||||
environment "config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }", env: 'development'
|
||||
generate :devise, "User", "first_name", "last_name", "announcements_last_read_at:datetime", "admin:boolean"
|
||||
|
||||
# Set admin default to false
|
||||
in_root do
|
||||
migration = Dir.glob("db/migrate/*").max_by { |f| File.mtime(f) }
|
||||
gsub_file migration, /:admin/, ":admin, default: false"
|
||||
end
|
||||
|
||||
gsub_file "config/initializers/devise.rb", / # config.secret_key = .+/, " config.secret_key = Rails.application.credentials.secret_key_base"
|
||||
|
||||
inject_into_file("app/models/user.rb", "omniauthable, :", after: "devise :")
|
||||
end
|
||||
|
||||
def add_authorization
|
||||
generate 'pundit:install'
|
||||
end
|
||||
|
||||
def default_to_esbuild
|
||||
return if options[:javascript] == "esbuild"
|
||||
unless options[:skip_javascript]
|
||||
@options = options.merge(javascript: "esbuild")
|
||||
end
|
||||
end
|
||||
|
||||
def add_javascript
|
||||
run "yarn add local-time esbuild-rails trix @hotwired/stimulus @hotwired/turbo-rails @rails/activestorage @rails/ujs @rails/request.js chokidar"
|
||||
end
|
||||
|
||||
def copy_templates
|
||||
remove_file "app/assets/stylesheets/application.css"
|
||||
remove_file "app/javascript/application.js"
|
||||
remove_file "app/javascript/controllers/index.js"
|
||||
remove_file "Procfile.dev"
|
||||
|
||||
copy_file "Procfile"
|
||||
copy_file "Procfile.dev"
|
||||
copy_file ".foreman"
|
||||
copy_file "esbuild.config.mjs"
|
||||
copy_file "app/javascript/application.js"
|
||||
copy_file "app/javascript/controllers/index.js"
|
||||
|
||||
directory "app", force: true
|
||||
directory "config", force: true
|
||||
directory "lib", force: true
|
||||
|
||||
route "get '/terms', to: 'home#terms'"
|
||||
route "get '/privacy', to: 'home#privacy'"
|
||||
end
|
||||
|
||||
def add_sidekiq
|
||||
environment "config.active_job.queue_adapter = :sidekiq"
|
||||
|
||||
insert_into_file "config/routes.rb",
|
||||
"require 'sidekiq/web'\n\n",
|
||||
before: "Rails.application.routes.draw do"
|
||||
|
||||
content = <<~RUBY
|
||||
authenticate :user, lambda { |u| u.admin? } do
|
||||
mount Sidekiq::Web => '/sidekiq'
|
||||
|
||||
namespace :madmin do
|
||||
resources :impersonates do
|
||||
post :impersonate, on: :member
|
||||
post :stop_impersonating, on: :collection
|
||||
end
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
insert_into_file "config/routes.rb", "#{content}\n", after: "Rails.application.routes.draw do\n"
|
||||
end
|
||||
|
||||
def add_announcements
|
||||
generate "model Announcement published_at:datetime announcement_type name description:text"
|
||||
route "resources :announcements, only: [:index]"
|
||||
end
|
||||
|
||||
def add_notifications
|
||||
rails_command "noticed:install:migrations"
|
||||
route "resources :notifications, only: [:index]"
|
||||
end
|
||||
|
||||
def add_multiple_authentication
|
||||
insert_into_file "config/routes.rb", ', controllers: { omniauth_callbacks: "users/omniauth_callbacks" }', after: " devise_for :users"
|
||||
|
||||
generate "model Service user:references provider uid access_token access_token_secret refresh_token expires_at:datetime auth:text"
|
||||
|
||||
template = """
|
||||
env_creds = Rails.application.credentials[Rails.env.to_sym] || {}
|
||||
%i{ facebook twitter github }.each do |provider|
|
||||
if options = env_creds[provider]
|
||||
config.omniauth provider, options[:app_id], options[:app_secret], options.fetch(:options, {})
|
||||
end
|
||||
end
|
||||
""".strip
|
||||
|
||||
insert_into_file "config/initializers/devise.rb", " " + template + "\n\n", before: " # ==> Warden configuration"
|
||||
end
|
||||
|
||||
def add_whenever
|
||||
run "wheneverize ."
|
||||
end
|
||||
|
||||
def add_friendly_id
|
||||
generate "friendly_id"
|
||||
insert_into_file(Dir["db/migrate/**/*friendly_id_slugs.rb"].first, "[5.2]", after: "ActiveRecord::Migration")
|
||||
end
|
||||
|
||||
def add_sitemap
|
||||
rails_command "sitemap:install"
|
||||
end
|
||||
|
||||
def add_bootstrap
|
||||
rails_command "css:install:bootstrap"
|
||||
end
|
||||
|
||||
def add_announcements_css
|
||||
insert_into_file 'app/assets/stylesheets/application.bootstrap.scss', '@import "jumpstart/announcements";'
|
||||
end
|
||||
|
||||
def add_esbuild_script
|
||||
build_script = "node esbuild.config.mjs"
|
||||
|
||||
case `npx -v`.to_f
|
||||
when 7.1...8.0
|
||||
run %(npm set-script build "#{build_script}")
|
||||
run %(yarn build)
|
||||
when (8.0..)
|
||||
run %(npm pkg set scripts.build="#{build_script}")
|
||||
run %(yarn build)
|
||||
else
|
||||
say %(Add "scripts": { "build": "#{build_script}" } to your package.json), :green
|
||||
end
|
||||
end
|
||||
|
||||
def add_github_actions_ci
|
||||
copy_file "github/workflows/verify.yml", ".github/workflows/verify.yml"
|
||||
end
|
||||
|
||||
def add_gem(name, *options)
|
||||
gem(name, *options) unless gem_exists?(name)
|
||||
end
|
||||
|
||||
def gem_exists?(name)
|
||||
IO.read("Gemfile") =~ /^\s*gem ['"]#{name}['"]/
|
||||
end
|
||||
|
||||
# Main setup
|
||||
add_template_repository_to_source_path
|
||||
default_to_esbuild
|
||||
add_gems
|
||||
|
||||
after_bundle do
|
||||
set_application_name
|
||||
add_users
|
||||
add_authorization
|
||||
add_javascript
|
||||
add_announcements
|
||||
add_notifications
|
||||
add_multiple_authentication
|
||||
add_sidekiq
|
||||
add_friendly_id
|
||||
add_bootstrap
|
||||
add_whenever
|
||||
add_sitemap
|
||||
add_announcements_css
|
||||
add_github_actions_ci
|
||||
rails_command "active_storage:install"
|
||||
|
||||
# Make sure Linux is in the Gemfile.lock for deploying
|
||||
run "bundle lock --add-platform x86_64-linux"
|
||||
|
||||
copy_templates
|
||||
|
||||
add_esbuild_script
|
||||
|
||||
# Commit everything to git
|
||||
unless ENV["SKIP_GIT"]
|
||||
git :init
|
||||
git add: "."
|
||||
# git commit will fail if user.email is not configured
|
||||
begin
|
||||
git commit: %( -m 'Initial commit' )
|
||||
rescue StandardError => e
|
||||
puts e.message
|
||||
end
|
||||
end
|
||||
|
||||
say
|
||||
say "Jumpstart app successfully created!", :blue
|
||||
say
|
||||
say "To get started with your new app:", :green
|
||||
say " cd #{original_app_name}"
|
||||
say
|
||||
say " # Update config/database.yml with your database credentials"
|
||||
say
|
||||
say " rails db:create"
|
||||
say " rails db:migrate"
|
||||
say " rails g madmin:install # Generate admin dashboards"
|
||||
say " gem install foreman"
|
||||
say " bin/dev"
|
||||
end
|
||||
39
test/template_test.rb
Normal file
39
test/template_test.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
require "minitest/autorun"
|
||||
|
||||
class TemplateTest < Minitest::Test
|
||||
def setup
|
||||
system("[ -d test_app ] && rm -rf test_app")
|
||||
end
|
||||
|
||||
def teardown
|
||||
setup
|
||||
end
|
||||
|
||||
def test_generator_succeeds
|
||||
output, _err = capture_subprocess_io do
|
||||
system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb")
|
||||
end
|
||||
assert_includes output, "Jumpstart app successfully created!"
|
||||
|
||||
output, _err = capture_subprocess_io do
|
||||
system("cd test_app && yarn build")
|
||||
end
|
||||
assert_includes output, "Done in "
|
||||
end
|
||||
|
||||
# TODO: Fix these tests on CI so they don't fail on db:create
|
||||
#
|
||||
# def test_generator_with_postgres_succeeds
|
||||
# output, err = capture_subprocess_io do
|
||||
# system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb -d postgresql")
|
||||
# end
|
||||
# assert_includes output, "Jumpstart app successfully created!"
|
||||
# end
|
||||
|
||||
# def test_generator_with_mysql_succeeds
|
||||
# output, err = capture_subprocess_io do
|
||||
# system("DISABLE_SPRING=1 SKIP_GIT=1 rails new test_app -m template.rb -d mysql")
|
||||
# end
|
||||
# assert_includes output, "Jumpstart app successfully created!"
|
||||
# end
|
||||
end
|
||||
Reference in New Issue
Block a user