├── .dockerignore
├── log
└── .keep
├── storage
└── .keep
├── tmp
├── .keep
├── pids
│ └── .keep
└── storage
│ └── .keep
├── vendor
└── .keep
├── lib
└── tasks
│ ├── .keep
│ └── auto_annotate_models.rake
├── test
├── mailers
│ ├── .keep
│ ├── previews
│ │ └── user_verification_mailer_preview.rb
│ └── user_verification_mailer_test.rb
├── models
│ ├── .keep
│ └── user_test.rb
├── controllers
│ ├── .keep
│ └── api
│ │ └── v1
│ │ └── users_controller_test.rb
├── integration
│ └── .keep
├── channels
│ └── application_cable
│ │ └── connection_test.rb
├── test_helper.rb
└── factories
│ └── users.rb
├── .ruby-version
├── app
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── json_web_token.rb
│ └── user.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── application_controller.rb
│ └── api
│ │ └── v1
│ │ └── users_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.html.erb
│ │ └── mailer.text.erb
│ └── user_verification_mailer
│ │ └── verify.html.erb
├── policies
│ ├── user_policy.rb
│ └── application_policy.rb
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── mailers
│ ├── application_mailer.rb
│ └── user_verification_mailer.rb
├── interactors
│ ├── mail
│ │ └── verify_user.rb
│ └── user_interactor
│ │ ├── add_to_system.rb
│ │ └── create.rb
└── jobs
│ ├── user_mailer_job.rb
│ └── application_job.rb
├── coverage
├── .resultset.json.lock
├── .last_run.json
├── assets
│ └── 0.12.3
│ │ ├── loading.gif
│ │ ├── magnify.png
│ │ ├── favicon_red.png
│ │ ├── colorbox
│ │ ├── border.png
│ │ ├── controls.png
│ │ ├── loading.gif
│ │ └── loading_background.png
│ │ ├── favicon_green.png
│ │ ├── favicon_yellow.png
│ │ ├── images
│ │ ├── ui-icons_222222_256x240.png
│ │ ├── ui-icons_2e83ff_256x240.png
│ │ ├── ui-icons_454545_256x240.png
│ │ ├── ui-icons_888888_256x240.png
│ │ ├── ui-icons_cd0a0a_256x240.png
│ │ ├── ui-bg_flat_0_aaaaaa_40x100.png
│ │ ├── ui-bg_flat_75_ffffff_40x100.png
│ │ ├── ui-bg_glass_55_fbf9ee_1x400.png
│ │ ├── ui-bg_glass_65_ffffff_1x400.png
│ │ ├── ui-bg_glass_75_dadada_1x400.png
│ │ ├── ui-bg_glass_75_e6e6e6_1x400.png
│ │ ├── ui-bg_glass_95_fef1ec_1x400.png
│ │ └── ui-bg_highlight-soft_75_cccccc_1x100.png
│ │ ├── DataTables-1.10.20
│ │ └── images
│ │ │ ├── sort_asc.png
│ │ │ ├── sort_both.png
│ │ │ ├── sort_desc.png
│ │ │ ├── sort_asc_disabled.png
│ │ │ └── sort_desc_disabled.png
│ │ └── application.css
└── .resultset.json
├── frontend
├── .npmrc
├── .eslintrc.json
├── constants
│ └── index.js
├── styles
│ ├── globals.css
│ └── Home.module.css
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── postcss.config.js
├── next.config.js
├── pages
│ ├── api
│ │ └── hello.js
│ ├── _app.js
│ ├── auth
│ │ ├── signup.js
│ │ └── signin.js
│ └── index.js
├── components
│ ├── shared
│ │ ├── index.js
│ │ ├── TextInput.js
│ │ ├── Card.js
│ │ ├── Button.js
│ │ └── Modal.js
│ ├── Footer.js
│ ├── Layout.js
│ ├── Header.js
│ └── themes
│ │ └── default.js
├── tailwind.config.js
├── store
│ └── user.js
├── .gitignore
├── utils
│ └── index.js
├── package.json
└── README.md
├── .DS_Store
├── .github
└── FUNDING.yml
├── Procfile
├── .env
├── public
└── robots.txt
├── bin
├── rake
├── rails
├── setup
└── bundle
├── config
├── initializers
│ ├── premailer_rails.rb
│ ├── generators.rb
│ ├── cors.rb
│ ├── filter_parameter_logging.rb
│ └── inflections.rb
├── environment.rb
├── cable.yml
├── boot.rb
├── routes.rb
├── locales
│ └── en.yml
├── credentials.yml.enc
├── application.rb
├── storage.yml
├── puma.rb
├── environments
│ ├── development.rb
│ ├── test.rb
│ └── production.rb
└── database.yml
├── db
├── migrate
│ ├── 20220313190647_enable_uuid.rb
│ ├── 20220314040904_create_users.rb
│ └── 20220315160413_create_good_jobs.rb
├── seeds.rb
└── schema.rb
├── config.ru
├── spec
└── interactors
│ └── user
│ └── create_spec.rb
├── .gitattributes
├── docker
├── frontend.Dockerfile.dev
└── rails.Dockerfile.dev
├── Rakefile
├── entrypoint.sh
├── .gitignore
├── LICENSE
├── Gemfile
├── docker-compose.yml
├── README.md
└── Gemfile.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.1.2
2 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/coverage/.resultset.json.lock:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers=true
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/.DS_Store
--------------------------------------------------------------------------------
/coverage/.last_run.json:
--------------------------------------------------------------------------------
1 | {
2 | "result": {
3 | "line": 93.93
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/constants/index.js:
--------------------------------------------------------------------------------
1 | export const API_URL = process.env.NEXT_PUBLIC_API_URL;
2 |
--------------------------------------------------------------------------------
/frontend/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: akhilgautam
4 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: rails server
2 | worker: bundle exec good_job start
3 | ui: cd frontend && pnpm run dev
4 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | POSTGRES_HOST=localhost
2 | POSTGRES_USER=''
3 | POSTGRES_PASSWORD=''
4 | DATABASE=nextjs_on_rails_development
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/loading.gif
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/magnify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/magnify.png
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/favicon_red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/favicon_red.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/colorbox/border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/colorbox/border.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/favicon_green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/favicon_green.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/favicon_yellow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/favicon_yellow.png
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require_relative '../config/boot'
5 | require 'rake'
6 | Rake.application.run
7 |
--------------------------------------------------------------------------------
/config/initializers/premailer_rails.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Premailer::Rails.config.merge!(preserve_styles: true, remove_ids: true)
4 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/colorbox/controls.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/colorbox/controls.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/colorbox/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/colorbox/loading.gif
--------------------------------------------------------------------------------
/app/policies/user_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserPolicy < ApplicationPolicy
4 | def update?
5 | user.id == record.id
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/colorbox/loading_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/colorbox/loading_background.png
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Channel < ActionCable::Channel::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-icons_222222_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-icons_222222_256x240.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-icons_2e83ff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-icons_2e83ff_256x240.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-icons_454545_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-icons_454545_256x240.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-icons_888888_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-icons_888888_256x240.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-icons_cd0a0a_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-icons_cd0a0a_256x240.png
--------------------------------------------------------------------------------
/config/initializers/generators.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.config.generators do |g|
4 | g.orm :active_record, primary_key_type: :uuid
5 | end
6 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_both.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_both.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_flat_0_aaaaaa_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_flat_0_aaaaaa_40x100.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_flat_75_ffffff_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_flat_75_ffffff_40x100.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_glass_55_fbf9ee_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_glass_55_fbf9ee_1x400.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_glass_65_ffffff_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_glass_65_ffffff_1x400.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_glass_75_dadada_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_glass_75_dadada_1x400.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_glass_75_e6e6e6_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_glass_75_e6e6e6_1x400.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_glass_95_fef1ec_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_glass_95_fef1ec_1x400.png
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: 'from@example.com'
5 | layout 'mailer'
6 | end
7 |
--------------------------------------------------------------------------------
/frontend/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | };
6 |
7 | module.exports = nextConfig;
8 |
--------------------------------------------------------------------------------
/db/migrate/20220313190647_enable_uuid.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EnableUuid < ActiveRecord::Migration[7.0]
4 | def change
5 | enable_extension 'pgcrypto'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | APP_PATH = File.expand_path('../config/application', __dir__)
5 | require_relative '../config/boot'
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc_disabled.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc_disabled.png
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/images/ui-bg_highlight-soft_75_cccccc_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akhil-gautam/nextjs-on-rails/HEAD/coverage/assets/0.12.3/images/ui-bg_highlight-soft_75_cccccc_1x100.png
--------------------------------------------------------------------------------
/frontend/pages/api/hello.js:
--------------------------------------------------------------------------------
1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
2 |
3 | export default function handler(req, res) {
4 | res.status(200).json({ name: 'John Doe' })
5 | }
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 | Rails.application.load_server
9 |
--------------------------------------------------------------------------------
/frontend/components/shared/index.js:
--------------------------------------------------------------------------------
1 | export { default as Button } from './Button';
2 | export { default as TextInput } from './TextInput';
3 | export { default as Modal } from './Modal';
4 | export { default as Card } from './Card';
5 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | primary_abstract_class
5 |
6 | def error_string
7 | errors.full_messages.join(' and ')
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/mailers/previews/user_verification_mailer_preview.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Preview all emails at http://localhost:3000/rails/mailers/user_verification_mailer
4 | class UserVerificationMailerPreview < ActionMailer::Preview
5 | end
6 |
--------------------------------------------------------------------------------
/test/mailers/user_verification_mailer_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | class UserVerificationMailerTest < ActionMailer::TestCase
6 | # test "the truth" do
7 | # assert true
8 | # end
9 | end
10 |
--------------------------------------------------------------------------------
/app/interactors/mail/verify_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Mail
4 | class VerifyUser
5 | include Interactor
6 |
7 | def call
8 | UserMailerJob.perform_later(context.user.id)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/user_mailer_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserMailerJob < ApplicationJob
4 | queue_as :default
5 |
6 | def perform(user_id)
7 | UserVerificationMailer.verify(User.find(user_id)).deliver_now
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: nextjs_on_rails_production
11 |
--------------------------------------------------------------------------------
/app/interactors/user_interactor/add_to_system.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module UserInteractor
4 | class AddToSystem
5 | include Interactor::Organizer
6 |
7 | organize UserInteractor::Create, Mail::VerifyUser
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/frontend/components/Footer.js:
--------------------------------------------------------------------------------
1 | export default function Footer() {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
4 |
5 | require 'bundler/setup' # Set up gems listed in the Gemfile.
6 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
7 |
--------------------------------------------------------------------------------
/spec/interactors/user/create_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe User::Create, type: :interactor do
6 | describe '.call' do
7 | pending "add some examples to (or delete) #{__FILE__}"
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2 |
3 | # Mark the database schema as having been generated.
4 | db/schema.rb linguist-generated
5 |
6 | # Mark any vendored files as having been vendored.
7 | vendor/* linguist-vendored
8 |
--------------------------------------------------------------------------------
/docker/frontend.Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:16.14.0
2 |
3 | ENV APP_PATH /var/app
4 | ENV NEXT_PORT 8080
5 |
6 | WORKDIR $APP_PATH
7 |
8 | COPY package.json package-lock.json ./
9 | RUN npm install
10 |
11 | COPY . ./
12 |
13 | EXPOSE $NEXT_PORT
14 | CMD ["npm", "run", "dev"]
15 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative 'config/application'
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './pages/**/*.{js,ts,jsx,tsx}',
5 | './components/**/*.{js,ts,jsx,tsx}',
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [require('daisyui')],
11 | };
12 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | bin/rails db:prepare
5 |
6 | # Remove a potentially pre-existing server.pid for Rails.
7 | if [ -e tmp/pids/server.pid ]; then
8 | rm tmp/pids/server.pid
9 | fi
10 |
11 | # exec the container's main process (what's set as CMD in the Dockerfile).
12 | exec "$@"
13 |
--------------------------------------------------------------------------------
/app/mailers/user_verification_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserVerificationMailer < ApplicationMailer
4 | default from: 'akhilgautam123@gmail.com'
5 |
6 | def verify(user)
7 | @user = user
8 | mail(to: @user.email,
9 | subject: 'Please verify your email!')
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | # Automatically retry jobs that encountered a deadlock
5 | # retry_on ActiveRecord::Deadlocked
6 |
7 | # Most jobs are safe to ignore if the underlying records are no longer available
8 | # discard_on ActiveJob::DeserializationError
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/cors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Read more: https://github.com/cyu/rack-cors
4 | Rails.application.config.middleware.insert_before 0, Rack::Cors do
5 | allow do
6 | origins '*'
7 |
8 | resource '*',
9 | headers: :any,
10 | methods: %i[get post put patch delete options head]
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Route Map
4 | #
5 |
6 | Rails.application.routes.draw do
7 | namespace :api do
8 | namespace :v1 do
9 | resources :users, only: %i[create show update] do
10 | post :login, on: :collection
11 | end
12 | end
13 | end
14 | get 'users/verify', to: 'api/v1/users#verify'
15 | end
16 |
--------------------------------------------------------------------------------
/frontend/store/user.js:
--------------------------------------------------------------------------------
1 | import create from 'zustand';
2 | import { persist } from 'zustand/middleware';
3 |
4 | export const useUser = create(
5 | persist(
6 | (set, _get) => ({
7 | nextRailsUser: null,
8 | addUser: (user) => set({ nextRailsUser: user }),
9 | }),
10 | {
11 | name: 'nextRailsUser',
12 | getStorage: () => localStorage,
13 | }
14 | )
15 | );
16 |
--------------------------------------------------------------------------------
/test/channels/application_cable/connection_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | module ApplicationCable
6 | class ConnectionTest < ActionCable::Connection::TestCase
7 | # test "connects with cookies" do
8 | # cookies.signed[:user_id] = 42
9 | #
10 | # connect
11 | #
12 | # assert_equal connection.user_id, "42"
13 | # end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # This file should contain all the record creation needed to seed the database with its default values.
3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
4 | #
5 | # Examples:
6 | #
7 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
8 | # Character.create(name: "Luke", movie: movies.first)
9 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | user:
3 | errors:
4 | verify: User not found with the given token.
5 | login: Email or password is incorrect.
6 | not_found: User not found with the given ID.
7 | generic:
8 | record_not_found: Record not found.
9 | create:
10 | success: Created successfully.
11 | update:
12 | success: Updated successfully.
13 | authorization:
14 | error: Unauthorized access.
--------------------------------------------------------------------------------
/frontend/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { useCookies } from 'react-cookie';
2 |
3 | import Footer from './Footer';
4 | import Header from './Header';
5 |
6 | export default function Layout({ children }) {
7 | const [cookies, setCookie, removeCookie] = useCookies(['user']);
8 |
9 | return (
10 |
11 |
12 | {children}
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | 7vPl4uw6Eaut6UCDJu8usulzpzCvSdB0AhyxOh/bYIjbIHaekBFweSmAXUqSuA1oT6R3J02kR76QPjPOmVffK93xfZqs9s6zVx5Ch4jqNIYiOwuBYCiUtUVrwiur8/AHs4K/OiG3J/iQXSlYSt13jTvEmZ5z+8r/o5eqqz3Xds2PD4m5s3BavNzJ6lCBQqdqiGncqA6p2RS9rkIr0ujxQSOOUkMpCJ0XD8VhuS87I8dVhbnp3IY9fjTFxy9Wx9lSTrzhqkvls8HjjeBdW6pe+xYdjSrJ3PFbFRstegB0GilWZp7JzpDBWT4LvVneC2HgdIV5ri7b7Qfid4tzGfmzXQuEAps6ol7B+6YhpkKN+aNLUWi/XdOBxzq08EzHJaaYvC1M3i0Rr3wKXawdMrDv2zwBwVfpYJhJ3Jqp--4LcPIP5+mAuO+Y3D--OFS4s1VnTXB337STs8YCXA==
--------------------------------------------------------------------------------
/frontend/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css';
2 | import { CookiesProvider } from 'react-cookie';
3 | import { Toaster } from 'react-hot-toast';
4 | import Layout from '../../client/components/Layout';
5 |
6 | function MyApp({ Component, pageProps }) {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default MyApp;
18 |
--------------------------------------------------------------------------------
/app/models/json_web_token.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class JsonWebToken
4 | SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
5 |
6 | def self.encode(payload, exp = 100.hours.from_now)
7 | payload[:exp] = exp.to_i
8 | JWT.encode(payload, SECRET_KEY, 'HS256')
9 | end
10 |
11 | def self.decode(token)
12 | decoded = JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256' })[0]
13 | HashWithIndifferentAccess.new decoded
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
6 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
7 | # notations and behaviors.
8 | Rails.application.config.filter_parameters += %i[
9 | passw secret token _key crypt salt certificate otp ssn
10 | ]
11 |
--------------------------------------------------------------------------------
/app/interactors/user_interactor/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module UserInteractor
4 | class Create
5 | include Interactor
6 |
7 | def call
8 | params = context.params.merge(
9 | reset_password_token: SecureRandom.urlsafe_base64,
10 | reset_password_sent_at: Time.now
11 | )
12 | user = User.new(params)
13 | if user.save
14 | context.user = user
15 | else
16 | context.fail!(error: { errors: user.error_string })
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'simplecov'
4 | SimpleCov.start
5 |
6 | ENV['RAILS_ENV'] ||= 'test'
7 | require_relative '../config/environment'
8 | require 'rails/test_help'
9 | require 'mocha/minitest'
10 | require 'webmock/minitest'
11 |
12 | module ActiveSupport
13 | class TestCase
14 | parallelize(workers: :number_of_processors)
15 | include FactoryBot::Syntax::Methods
16 |
17 | def get_auth_headers(user)
18 | token = JsonWebToken.encode(user_id: user.id)
19 | { 'Authorization' => "Token #{token}" }
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/db/migrate/20220314040904_create_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateUsers < ActiveRecord::Migration[7.0]
4 | def change
5 | create_table :users, id: :uuid do |t|
6 | t.integer :role, null: false, index: true, default: 0
7 | t.string :email, null: false, index: { unique: true }
8 | t.string :first_name, default: ''
9 | t.string :last_name, default: ''
10 | t.string :password_digest, null: false
11 | t.string :reset_password_token, index: { unique: true }
12 | t.datetime :reset_password_sent_at
13 | t.integer :sign_in_count, default: 0, null: false
14 | t.string :provider, default: 'email', null: false
15 | t.timestamps
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/docker/rails.Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM ruby:3.1.2
2 |
3 | ENV APP_PATH /var/app
4 | ENV BUNDLE_VERSION 2.3.5
5 | ENV RAILS_PORT 3000
6 |
7 |
8 | RUN apt-get update -qq && apt-get install -y nodejs postgresql-client
9 | RUN gem install bundler --version "$BUNDLE_VERSION"
10 |
11 | WORKDIR $APP_PATH
12 |
13 | COPY Gemfile Gemfile.lock ./
14 | RUN bundle check || bundle install --jobs 20 --retry 5
15 |
16 | COPY . .
17 |
18 | # Add a script to be executed every time the container starts.
19 | COPY entrypoint.sh /usr/bin/
20 | RUN chmod +x /usr/bin/entrypoint.sh
21 |
22 | ENTRYPOINT ["entrypoint.sh"]
23 | EXPOSE 3000
24 |
25 | # Configure the main process to run when running the image
26 | CMD ["rails", "server", "-b", "0.0.0.0", "-p", $RAILS_PORT]
27 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | # Be sure to restart your server when you modify this file.
3 |
4 | # Add new inflection rules using the following format. Inflections
5 | # are locale specific, and you may define rules for as many different
6 | # locales as you wish. All of these examples are active by default:
7 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
8 | # inflect.plural /^(ox)$/i, "\\1en"
9 | # inflect.singular /^(ox)en/i, "\\1"
10 | # inflect.irregular "person", "people"
11 | # inflect.uncountable %w( fish sheep )
12 | # end
13 |
14 | # These inflection rules are supported but not enabled by default:
15 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
16 | # inflect.acronym "RESTful"
17 | # end
18 |
--------------------------------------------------------------------------------
/frontend/components/shared/TextInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TextInput = React.forwardRef(
4 | ({ label, type = 'text', helperText, ...rest }, ref) => {
5 | return (
6 |
7 |
8 | {label}
9 |
10 |
16 |
17 | {helperText}
18 |
19 |
20 | );
21 | }
22 | );
23 |
24 | export default TextInput;
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore all logfiles and tempfiles.
11 | /log/*
12 | /tmp/*
13 | !/log/.keep
14 | !/tmp/.keep
15 |
16 | # Ignore pidfiles, but keep the directory.
17 | /tmp/pids/*
18 | !/tmp/pids/
19 | !/tmp/pids/.keep
20 |
21 | # Ignore uploaded files in development.
22 | /storage/*
23 | !/storage/.keep
24 | /tmp/storage/*
25 | !/tmp/storage/
26 | !/tmp/storage/.keep
27 |
28 | # Ignore master key for decrypting credentials and more.
29 | /config/master.key
30 |
--------------------------------------------------------------------------------
/frontend/utils/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cookie from 'cookie';
3 | import Axios from 'axios';
4 |
5 | import defaultTheme from '../components/themes/default';
6 |
7 | export function userFromCookie(req) {
8 | const raw = cookie.parse(
9 | req ? req.headers.cookie || '' : document.cookie
10 | ).nextRailsUser;
11 | if (!raw) return null;
12 | return JSON.parse(raw);
13 | }
14 |
15 | export const ThemeContext = React.createContext({ theme: defaultTheme });
16 |
17 | export const API_URL = process.env.NEXT_PUBLIC_API_URL;
18 |
19 | function authRequestInterceptor(config) {
20 | config.headers.Accept = 'application/json';
21 | return config;
22 | }
23 |
24 | export const axios = Axios.create({
25 | baseURL: process.env.NEXT_PUBLIC_API_URL,
26 | });
27 |
28 | axios.interceptors.request.use(authRequestInterceptor);
29 |
--------------------------------------------------------------------------------
/app/policies/application_policy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationPolicy
4 | attr_reader :user, :record
5 |
6 | def initialize(user, record)
7 | @user = user
8 | @record = record
9 | end
10 |
11 | def index?
12 | false
13 | end
14 |
15 | def show?
16 | false
17 | end
18 |
19 | def create?
20 | false
21 | end
22 |
23 | def new?
24 | create?
25 | end
26 |
27 | def update?
28 | false
29 | end
30 |
31 | def edit?
32 | update?
33 | end
34 |
35 | def destroy?
36 | false
37 | end
38 |
39 | class Scope
40 | def initialize(user, scope)
41 | @user = user
42 | @scope = scope
43 | end
44 |
45 | def resolve
46 | raise NotImplementedError, "You must define #resolve in #{self.class}"
47 | end
48 |
49 | private
50 |
51 | attr_reader :user, :scope
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^1.5.0",
13 | "axios": "^0.26.1",
14 | "classnames": "^2.3.1",
15 | "cookie": "^0.4.2",
16 | "daisyui": "^2.13.4",
17 | "next": "12.2.5",
18 | "react": "18.2.0",
19 | "react-cookie": "^4.1.1",
20 | "react-dom": "18.2.0",
21 | "react-feather": "^2.0.9",
22 | "react-hook-form": "^7.28.0",
23 | "react-hot-toast": "^2.3.0",
24 | "zustand": "3.7.1"
25 | },
26 | "devDependencies": {
27 | "autoprefixer": "^10.4.8",
28 | "eslint": "8.22.0",
29 | "eslint-config-next": "12.2.5",
30 | "postcss": "^8.4.16",
31 | "tailwindcss": "^3.1.8"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Akhil Gautam
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'boot'
4 |
5 | require 'rails/all'
6 |
7 | # Require the gems listed in Gemfile, including any gems
8 | # you've limited to :test, :development, or :production.
9 | Bundler.require(*Rails.groups)
10 |
11 | module NextjsOnRails
12 | class Application < Rails::Application
13 | # Initialize configuration defaults for originally generated Rails version.
14 | config.load_defaults 7.0
15 |
16 | # Configuration for the application, engines, and railties goes here.
17 | #
18 | # These settings can be overridden in specific environments using the files
19 | # in config/environments, which are processed later.
20 | #
21 | # config.time_zone = "Central Time (US & Canada)"
22 | # config.eager_load_paths << Rails.root.join("extras")
23 |
24 | # Only loads a smaller set of middleware suitable for API only apps.
25 | # Middleware like session, flash, cookies can be added back manually.
26 | # Skip views, helpers and assets when generating a new resource.
27 | config.api_only = true
28 | config.active_job.queue_adapter = :good_job
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'fileutils'
5 |
6 | # path to your application root.
7 | APP_ROOT = File.expand_path('..', __dir__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | FileUtils.chdir APP_ROOT do
14 | # This script is a way to set up or update your development environment automatically.
15 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
16 | # Add necessary setup steps to this file.
17 |
18 | puts '== Installing dependencies =='
19 | system! 'gem install bundler --conservative'
20 | system('bundle check') || system!('bundle install')
21 |
22 | # puts "\n== Copying sample files =="
23 | # unless File.exist?("config/database.yml")
24 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
25 | # end
26 |
27 | puts "\n== Preparing database =="
28 | system! 'bin/rails db:prepare'
29 |
30 | puts "\n== Removing old logs and tempfiles =="
31 | system! 'bin/rails log:clear tmp:clear'
32 |
33 | puts "\n== Restarting application server =="
34 | system! 'bin/rails restart'
35 | end
36 |
--------------------------------------------------------------------------------
/frontend/components/shared/Card.js:
--------------------------------------------------------------------------------
1 | // card component
2 |
3 | function CardContainer({ className = '', children, ...rest }) {
4 | return (
5 |
9 | {children}
10 |
11 | );
12 | }
13 |
14 | function CardHeader({ children, ...rest }) {
15 | return (
16 |
20 | {children}
21 |
22 | );
23 | }
24 |
25 | function CardBody({ children, ...rest }) {
26 | return (
27 |
31 | {children}
32 |
33 | );
34 | }
35 |
36 | function CardFooter({ children, className = '', ...rest }) {
37 | return (
38 |
42 | {children}
43 |
44 | );
45 | }
46 |
47 | let Card = Object.assign(CardContainer, {
48 | Header: CardHeader,
49 | Body: CardBody,
50 | Footer: CardFooter,
51 | });
52 |
53 | export default Card;
54 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::API
4 | include Pundit::Authorization
5 |
6 | before_action :authenticate_user
7 | rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
8 | rescue_from Pundit::NotAuthorizedError, with: :unauthorized_access
9 | # rescue_from UnauthorizedError, with: :unauthorized_access
10 |
11 | def authenticate_user
12 | header = request.headers['Authorization']
13 | header = header.split(' ').last if header
14 | if header
15 | begin
16 | @decoded = JsonWebToken.decode(header)
17 | @current_user = User.find(@decoded[:user_id])
18 | rescue ActiveRecord::RecordNotFound => e
19 | render json: { errors: e.message }, status: :unauthorized
20 | rescue JWT::DecodeError => e
21 | render json: { errors: 'Session expired. Please login again!' }, status: :unauthorized
22 | end
23 | else
24 | render json: { errors: 'Authentication token not provided' }, status: :unauthorized
25 | end
26 | end
27 |
28 | attr_reader :current_user
29 |
30 | private
31 |
32 | def record_not_found
33 | render json: { errors: I18n.t('generic.record_not_found') }, status: :not_found
34 | end
35 |
36 | def unauthorized_access
37 | render json: { errors: I18n.t('authorization.error') }, status: :forbidden
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: users
6 | #
7 | # id :uuid not null, primary key
8 | # email :string not null
9 | # first_name :string default("")
10 | # last_name :string default("")
11 | # password_digest :string not null
12 | # provider :string default("email"), not null
13 | # reset_password_sent_at :datetime
14 | # reset_password_token :string
15 | # role :integer default("customer"), not null
16 | # sign_in_count :integer default(0), not null
17 | # created_at :datetime not null
18 | # updated_at :datetime not null
19 | #
20 | # Indexes
21 | #
22 | # index_users_on_email (email) UNIQUE
23 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
24 | # index_users_on_role (role)
25 | #
26 | class User < ApplicationRecord
27 | has_secure_password
28 | enum role: %i[customer admin]
29 |
30 | validates :email, presence: true, uniqueness: { case_sensitive: false }
31 | validates :password, length: { minimum: 8 }, on: :create
32 |
33 | scope :verified, -> { where(reset_password_token: nil) }
34 |
35 | def full_name
36 | "#{first_name} #{last_name}"
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/factories/users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: users
6 | #
7 | # id :uuid not null, primary key
8 | # email :string not null
9 | # first_name :string default("")
10 | # last_name :string default("")
11 | # password_digest :string not null
12 | # provider :string default("email"), not null
13 | # reset_password_sent_at :datetime
14 | # reset_password_token :string
15 | # role :integer default("customer"), not null
16 | # sign_in_count :integer default(0), not null
17 | # created_at :datetime not null
18 | # updated_at :datetime not null
19 | #
20 | # Indexes
21 | #
22 | # index_users_on_email (email) UNIQUE
23 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
24 | # index_users_on_role (role)
25 | #
26 | FactoryBot.define do
27 | factory :user do
28 | role { 'customer' }
29 | first_name { Faker::Name.first_name }
30 | last_name { Faker::Name.last_name }
31 | email { Faker::Internet.email }
32 | reset_password_token { nil }
33 | reset_password_sent_at { DateTime.now }
34 | password { Faker::Internet.password }
35 |
36 | trait :unverified do
37 | reset_password_token { SecureRandom.uuid }
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | ruby '3.1.2'
7 | gem 'bcrypt', '~> 3.1.7'
8 | gem 'good_job'
9 | gem 'interactor', '~> 3.0'
10 | gem 'interactor-rails', '~> 2.0'
11 | gem 'jb'
12 | gem 'jwt'
13 | gem 'pagy', '~> 5.10'
14 | gem 'pg', '1.4.3'
15 | gem 'premailer-rails'
16 | gem 'puma'
17 | gem 'pundit'
18 | gem 'rack-cors'
19 | gem 'rails', '7.0.3.1'
20 | gem 'strong_migrations'
21 |
22 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
23 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
24 |
25 | # Reduces boot times through caching; required in config/boot.rb
26 | gem 'bootsnap', require: false
27 |
28 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
29 | # gem "image_processing", "~> 1.2"
30 |
31 | group :development, :test do
32 | gem 'bullet', '~> 7.0.1'
33 | gem 'dotenv-rails'
34 | gem 'factory_bot_rails', '~> 6.2.0'
35 | gem 'faker', '~> 2.20.0'
36 | gem 'pry-rails'
37 | end
38 |
39 | group :development do
40 | gem 'annotate', '~> 3.2'
41 | gem 'letter_opener', '~> 1.8'
42 | gem 'rubocop-github'
43 | gem 'rubocop-performance', require: false
44 | gem 'rubocop-rails', require: false
45 | end
46 |
47 | group :test do
48 | gem 'mocha'
49 | gem 'simplecov', '~> 0.21.2'
50 | gem 'webmock'
51 | end
52 |
53 | group :development do
54 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
55 | # gem "spring"
56 | end
57 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 | networks:
3 | development:
4 | volumes:
5 | db_data:
6 | gem_cache:
7 | node_modules:
8 | services:
9 | db:
10 | image: postgres:14.1-alpine
11 | container_name: nextjs_rails_db
12 | volumes:
13 | - db_data:/var/lib/postgresql/data
14 | networks:
15 | - development
16 | ports:
17 | - 5432:5432
18 | environment:
19 | POSTGRES_USER: postgres
20 | POSTGRES_PASSWORD: password
21 | web:
22 | build:
23 | context: .
24 | dockerfile: ./docker/rails.Dockerfile.dev
25 | image: nextjs_rails_server:development
26 | tty: true
27 | stdin_open: true
28 | command: bundle exec rails s -p 3000 -b '0.0.0.0'
29 | volumes:
30 | - .:/var/app:cached
31 | - gem_cache:/usr/local/bundle/gems
32 | networks:
33 | - development
34 | ports:
35 | - 3000:3000
36 | environment:
37 | POSTGRES_USER: postgres
38 | POSTGRES_PASSWORD: password
39 | POSTGRES_HOST: db
40 | DATABASE: nextjs_on_rails_development
41 | RAILS_ENV: development
42 | depends_on:
43 | - db
44 | - frontend
45 | frontend:
46 | build:
47 | context: ./frontend
48 | dockerfile: ../docker/frontend.Dockerfile.dev
49 | image: nextjs_rails_frontend:development
50 | networks:
51 | - development
52 | volumes:
53 | - ./frontend:/var/app:cached
54 | - node_modules:/var/app/node_modules
55 | ports:
56 | - 8000:8000
57 | environment:
58 | NODE_ENV: development
59 | NEXT_TELEMETRY_DISABLED: 1
60 | command: npm run dev
61 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | pnpm install && pnpm run dev
9 | # or
10 | npm install && npm run dev
11 | # or
12 | yarn install && yarn dev
13 | ```
14 |
15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16 |
17 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
18 |
19 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
20 |
21 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/db/migrate/20220315160413_create_good_jobs.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateGoodJobs < ActiveRecord::Migration[7.0]
4 | def change
5 | enable_extension 'pgcrypto'
6 |
7 | create_table :good_jobs, id: :uuid do |t|
8 | t.text :queue_name
9 | t.integer :priority
10 | t.jsonb :serialized_params
11 | t.timestamp :scheduled_at
12 | t.timestamp :performed_at
13 | t.timestamp :finished_at
14 | t.text :error
15 |
16 | t.timestamps
17 |
18 | t.uuid :active_job_id
19 | t.text :concurrency_key
20 | t.text :cron_key
21 | t.uuid :retried_good_job_id
22 | t.timestamp :cron_at
23 | end
24 |
25 | create_table :good_job_processes, id: :uuid do |t|
26 | t.timestamps
27 | t.jsonb :state
28 | end
29 |
30 | add_index :good_jobs, :scheduled_at, where: '(finished_at IS NULL)', name: 'index_good_jobs_on_scheduled_at'
31 | add_index :good_jobs, %i[queue_name scheduled_at], where: '(finished_at IS NULL)',
32 | name: :index_good_jobs_on_queue_name_and_scheduled_at
33 | add_index :good_jobs, %i[active_job_id created_at], name: :index_good_jobs_on_active_job_id_and_created_at
34 | add_index :good_jobs, :concurrency_key, where: '(finished_at IS NULL)',
35 | name: :index_good_jobs_on_concurrency_key_when_unfinished
36 | add_index :good_jobs, %i[cron_key created_at], name: :index_good_jobs_on_cron_key_and_created_at
37 | add_index :good_jobs, %i[cron_key cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true
38 | add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id
39 | add_index :good_jobs, [:finished_at], where: 'retried_good_job_id IS NULL AND finished_at IS NOT NULL',
40 | name: :index_good_jobs_jobs_on_finished_at
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/views/user_verification_mailer/verify.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
48 |
49 |
50 |
51 |
Thanks for signing up <%= @user.full_name || @user.email %>!
52 |
53 | Click here to verify your account!
54 |
55 |
OR copy & paste the following URL in your browser
56 |
<%= "http://localhost:3000/users/verify?token=#{@user.reset_password_token}" %>
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Puma can serve each request in a thread from an internal thread pool.
4 | # The `threads` method setting takes two numbers: a minimum and maximum.
5 | # Any libraries that use thread pools should be configured to match
6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
7 | # and maximum; this matches the default thread size of Active Record.
8 | #
9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
11 | threads min_threads_count, max_threads_count
12 |
13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
14 | # terminating a worker in development environments.
15 | #
16 | worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'
17 |
18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
19 | #
20 | port ENV.fetch('PORT', 3000)
21 |
22 | # Specifies the `environment` that Puma will run in.
23 | #
24 | environment ENV.fetch('RAILS_ENV', 'development')
25 |
26 | # Specifies the `pidfile` that Puma will use.
27 | pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
28 |
29 | # Specifies the number of `workers` to boot in clustered mode.
30 | # Workers are forked web server processes. If using threads and workers together
31 | # the concurrency of the application would be max `threads` * `workers`.
32 | # Workers do not work on JRuby or Windows (both of which do not support
33 | # processes).
34 | #
35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
36 |
37 | # Use the `preload_app!` method when specifying a `workers` number.
38 | # This directive tells Puma to first boot the application and load code
39 | # before forking the application. This takes advantage of Copy On Write
40 | # process behavior so workers use less memory.
41 | #
42 | # preload_app!
43 |
44 | # Allow puma to be restarted by `bin/rails restart` command.
45 | plugin :tmp_restart
46 |
--------------------------------------------------------------------------------
/frontend/components/shared/Button.js:
--------------------------------------------------------------------------------
1 | import { Loader } from 'react-feather';
2 |
3 | // button variant based on color
4 | const COLOR_VARIANT_MAP = {
5 | solid: {
6 | primary: 'bg-blue-500 hover:bg-blue-700 text-white',
7 | secondary: 'bg-gray-500 hover:bg-gray-700 text-white',
8 | success: 'bg-green-500 hover:bg-green-700 text-white',
9 | danger: 'bg-red-500 hover:bg-red-700 text-white',
10 | warning: 'bg-orange-500 hover:bg-orange-700 text-white',
11 | info: 'bg-teal-500 hover:bg-teal-700 text-white',
12 | light: 'bg-gray-100 hover:bg-gray-200 text-gray-800',
13 | dark: 'bg-gray-800 hover:bg-gray-900 text-white',
14 | },
15 | outlined: {
16 | primary: 'border-2 border-blue-800 text-blue-800',
17 | secondary: 'border-2 border-gray-800 text-gray-800',
18 | success: 'border-2 border-green-800 text-green-800',
19 | danger: 'border-2 border-red-800 text-red-800',
20 | warning: 'border-2 border-orange-800 text-orange-800',
21 | info: 'border-2 border-teal-800 text-teal-800',
22 | light: 'border-2 border-gray-100 text-gray-800',
23 | dark: 'border-2 border-gray-900 text-black',
24 | },
25 | };
26 |
27 | const COMMON_CLASS =
28 | 'px-6 py-2 flex justify-center uppercase tracking-wider font-bold shadow-lg transition-all focus-visible:ring-2 ring-offset-2 ring-black focus:outline-none focus:shadow-outline hover:scale-95';
29 |
30 | export default function Button({
31 | loading,
32 | children,
33 | block = false,
34 | color = 'dark',
35 | variant = 'solid',
36 | className = '',
37 | ...rest
38 | }) {
39 | return (
40 |
48 | {children}
49 | {loading && (
50 |
51 |
52 |
53 | )}
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: users
6 | #
7 | # id :uuid not null, primary key
8 | # email :string not null
9 | # first_name :string default("")
10 | # last_name :string default("")
11 | # password_digest :string not null
12 | # provider :string default("email"), not null
13 | # reset_password_sent_at :datetime
14 | # reset_password_token :string
15 | # role :integer default("customer"), not null
16 | # sign_in_count :integer default(0), not null
17 | # created_at :datetime not null
18 | # updated_at :datetime not null
19 | #
20 | # Indexes
21 | #
22 | # index_users_on_email (email) UNIQUE
23 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
24 | # index_users_on_role (role)
25 | #
26 | require 'test_helper'
27 |
28 | class UserTest < ActiveSupport::TestCase
29 | def setup
30 | @user = create(:user)
31 | end
32 |
33 | def test_valid
34 | assert @user.valid?
35 | end
36 |
37 | def test_email_is_required
38 | user = build(:user, email: nil)
39 | assert_raises ActiveRecord::RecordInvalid do
40 | user.save!
41 | end
42 | assert_includes user.errors[:email], "can't be blank"
43 | end
44 |
45 | def test_email_is_unique
46 | user = build(:user, email: @user.email)
47 | assert_raises ActiveRecord::RecordInvalid do
48 | user.save!
49 | end
50 | assert_includes user.errors[:email], 'has already been taken'
51 | end
52 |
53 | def test_password_is_required
54 | user = build(:user, password: nil)
55 | assert_raises ActiveRecord::RecordInvalid do
56 | user.save!
57 | end
58 | assert_includes user.errors[:password], "can't be blank"
59 |
60 | user.password = '12345678'
61 | assert user.save!
62 | end
63 |
64 | def test_authenticate_password
65 | user = create(:user, password: '12345678')
66 | assert user.authenticate('12345678')
67 | refute user.authenticate('123456789')
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/users_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | module V1
5 | class UsersController < ApplicationController
6 | skip_before_action :authenticate_user, only: %i[create login verify]
7 | before_action :set_user, only: [:update]
8 |
9 | # serves as signup
10 | def create
11 | result = UserInteractor::AddToSystem.call(params: user_params)
12 | if result.success?
13 | render json: result.user, status: :created
14 | else
15 | render json: result.error, status: :unprocessable_entity
16 | end
17 | end
18 |
19 | def update
20 | authorize @user
21 | if @user.update(user_params)
22 | render json: { message: I18n.t('generic.update.success') }, status: :ok
23 | else
24 | render json: { errors: @user.error_string }, status: :unprocessable_entity
25 | end
26 | end
27 |
28 | def login
29 | user = User.verified.where(email: login_params[:email]).first
30 | if user&.authenticate(login_params[:password])
31 | token = JsonWebToken.encode(user_id: user.id)
32 | time = Time.zone.now + 100.hours
33 | render json: {
34 | token: token,
35 | exp: time,
36 | email: user.email
37 | }
38 | else
39 | render json: { errors: I18n.t('user.errors.login') }, status: :unauthorized
40 | end
41 | end
42 |
43 | def verify
44 | user = User.find_by(reset_password_token: params[:token])
45 | return render json: { errors: I18n.t('user.errors.verify') }, status: :not_found unless user
46 |
47 | user.update(reset_password_token: nil)
48 | render json: { success: 'Verified successfully, please login now!' }, status: :ok
49 | end
50 |
51 | private
52 |
53 | def set_user
54 | @user = User.find_by(id: params[:id])
55 | return render json: { error: I18n.t('user.errors.not_found') }, status: :not_found unless @user
56 | end
57 |
58 | def user_params
59 | params.permit(:email, :first_name, :last_name, :password)
60 | end
61 |
62 | def login_params
63 | params.permit!
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/frontend/components/shared/Modal.js:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from '@headlessui/react';
2 | import { Fragment } from 'react';
3 | import { XSquare } from 'react-feather';
4 |
5 | export default function Modal({ children, title = '', closeModal }) {
6 | return (
7 |
8 |
13 |
14 |
23 |
24 |
25 |
26 | {/* This element is to trick the browser into centering the modal contents. */}
27 |
31 |
32 |
33 |
42 |
43 |
47 | {title}
48 |
49 |
50 |
{children}
51 |
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/lib/tasks/auto_annotate_models.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # NOTE: only doing this in development as some production environments (Heroku)
4 | # NOTE: are sensitive to local FS writes, and besides -- it's just not proper
5 | # NOTE: to have a dev-mode tool do its thing in production.
6 | if Rails.env.development?
7 | require 'annotate'
8 | task :set_annotation_options do
9 | # You can override any of these by setting an environment variable of the
10 | # same name.
11 | Annotate.set_defaults(
12 | 'active_admin' => 'false',
13 | 'additional_file_patterns' => [],
14 | 'routes' => 'false',
15 | 'models' => 'true',
16 | 'position_in_routes' => 'before',
17 | 'position_in_class' => 'before',
18 | 'position_in_test' => 'before',
19 | 'position_in_fixture' => 'before',
20 | 'position_in_factory' => 'before',
21 | 'position_in_serializer' => 'before',
22 | 'show_foreign_keys' => 'true',
23 | 'show_complete_foreign_keys' => 'false',
24 | 'show_indexes' => 'true',
25 | 'simple_indexes' => 'false',
26 | 'model_dir' => 'app/models',
27 | 'root_dir' => '',
28 | 'include_version' => 'false',
29 | 'require' => '',
30 | 'exclude_tests' => 'false',
31 | 'exclude_fixtures' => 'false',
32 | 'exclude_factories' => 'false',
33 | 'exclude_serializers' => 'false',
34 | 'exclude_scaffolds' => 'true',
35 | 'exclude_controllers' => 'true',
36 | 'exclude_helpers' => 'true',
37 | 'exclude_sti_subclasses' => 'false',
38 | 'ignore_model_sub_dir' => 'false',
39 | 'ignore_columns' => nil,
40 | 'ignore_routes' => nil,
41 | 'ignore_unknown_models' => 'false',
42 | 'hide_limit_column_types' => 'integer,bigint,boolean',
43 | 'hide_default_column_types' => 'json,jsonb,hstore',
44 | 'skip_on_db_migrate' => 'false',
45 | 'format_bare' => 'true',
46 | 'format_rdoc' => 'false',
47 | 'format_yard' => 'false',
48 | 'format_markdown' => 'false',
49 | 'sort' => 'false',
50 | 'force' => 'false',
51 | 'frozen' => 'false',
52 | 'classified_sort' => 'true',
53 | 'trace' => 'false',
54 | 'wrapper_open' => nil,
55 | 'wrapper_close' => nil,
56 | 'with_comment' => 'true'
57 | )
58 | end
59 |
60 | Annotate.load_tasks
61 | end
62 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_support/core_ext/integer/time'
4 |
5 | Rails.application.configure do
6 | # Settings specified here will take precedence over those in config/application.rb.
7 |
8 | # In the development environment your application's code is reloaded any time
9 | # it changes. This slows down response time but is perfect for development
10 | # since you don't have to restart the web server when you make code changes.
11 | config.cache_classes = false
12 |
13 | # Do not eager load code on boot.
14 | config.eager_load = false
15 |
16 | # Show full error reports.
17 | config.consider_all_requests_local = true
18 |
19 | # Enable server timing
20 | config.server_timing = true
21 |
22 | # Enable/disable caching. By default caching is disabled.
23 | # Run rails dev:cache to toggle caching.
24 | if Rails.root.join('tmp/caching-dev.txt').exist?
25 | config.cache_store = :memory_store
26 | config.public_file_server.headers = {
27 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
28 | }
29 | else
30 | config.action_controller.perform_caching = false
31 |
32 | config.cache_store = :null_store
33 | end
34 |
35 | # Store uploaded files on the local file system (see config/storage.yml for options).
36 | config.active_storage.service = :local
37 |
38 | # Don't care if the mailer can't send.
39 | config.action_mailer.raise_delivery_errors = false
40 |
41 | config.action_mailer.perform_caching = false
42 |
43 | # Print deprecation notices to the Rails logger.
44 | config.active_support.deprecation = :log
45 |
46 | # Raise exceptions for disallowed deprecations.
47 | config.active_support.disallowed_deprecation = :raise
48 |
49 | # Tell Active Support which deprecation messages to disallow.
50 | config.active_support.disallowed_deprecation_warnings = []
51 |
52 | # Raise an error on page load if there are pending migrations.
53 | config.active_record.migration_error = :page_load
54 |
55 | # Highlight code that triggered database queries in logs.
56 | config.active_record.verbose_query_logs = true
57 |
58 | config.action_mailer.delivery_method = :letter_opener
59 | config.action_mailer.perform_deliveries = true
60 |
61 | # Raises error for missing translations.
62 | # config.i18n.raise_on_missing_translations = true
63 |
64 | # Annotate rendered view with file names.
65 | # config.action_view.annotate_rendered_view_with_filenames = true
66 |
67 | # Uncomment if you wish to allow Action Cable access from any origin.
68 | # config.action_cable.disable_request_forgery_protection = true
69 | end
70 |
--------------------------------------------------------------------------------
/frontend/components/Header.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import Link from 'next/link';
3 | import { Menu } from 'react-feather';
4 | import { useUser } from '../store/user';
5 | import { Button } from './shared';
6 | import { useCookies } from 'react-cookie';
7 |
8 | export default function Header() {
9 | const { nextRailsUser: user, addUser } = useUser();
10 | const [showMenu, setShowMenu] = useState(false);
11 | const toggleMenu = () => setShowMenu(!showMenu);
12 | const [_cookies, _setCookie, removeCookie] = useCookies(['nextRailsUser']);
13 |
14 | const signOut = () => {
15 | removeCookie('nextRailsUser');
16 | addUser(null);
17 | };
18 |
19 | return (
20 | <>
21 |
58 | {showMenu && (
59 |
60 |
61 |
62 |
63 | Dashboard
64 |
65 |
66 |
67 |
68 | About
69 |
70 |
71 |
72 |
73 | )}
74 | >
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_support/core_ext/integer/time'
4 |
5 | # The test environment is used exclusively to run your application's
6 | # test suite. You never need to work with it otherwise. Remember that
7 | # your test database is "scratch space" for the test suite and is wiped
8 | # and recreated between test runs. Don't rely on the data there!
9 |
10 | Rails.application.configure do
11 | # Settings specified here will take precedence over those in config/application.rb.
12 |
13 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
14 | config.cache_classes = true
15 |
16 | # Eager loading loads your whole application. When running a single test locally,
17 | # this probably isn't necessary. It's a good idea to do in a continuous integration
18 | # system, or in some way before deploying your code.
19 | config.eager_load = ENV['CI'].present?
20 |
21 | # Configure public file server for tests with Cache-Control for performance.
22 | config.public_file_server.enabled = true
23 | config.public_file_server.headers = {
24 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
25 | }
26 |
27 | # Show full error reports and disable caching.
28 | config.consider_all_requests_local = true
29 | config.action_controller.perform_caching = false
30 | config.cache_store = :null_store
31 |
32 | config.active_job.queue_adapter = :good_job
33 | config.good_job.execution_mode = :inline
34 |
35 | # Raise exceptions instead of rendering exception templates.
36 | config.action_dispatch.show_exceptions = false
37 |
38 | # Disable request forgery protection in test environment.
39 | config.action_controller.allow_forgery_protection = false
40 |
41 | # Store uploaded files on the local file system in a temporary directory.
42 | config.active_storage.service = :test
43 |
44 | config.action_mailer.perform_caching = false
45 |
46 | # Tell Action Mailer not to deliver emails to the real world.
47 | # The :test delivery method accumulates sent emails in the
48 | # ActionMailer::Base.deliveries array.
49 | config.action_mailer.delivery_method = :test
50 |
51 | # Print deprecation notices to the stderr.
52 | config.active_support.deprecation = :stderr
53 |
54 | # Raise exceptions for disallowed deprecations.
55 | config.active_support.disallowed_deprecation = :raise
56 |
57 | # Tell Active Support which deprecation messages to disallow.
58 | config.active_support.disallowed_deprecation_warnings = []
59 |
60 | # Raises error for missing translations.
61 | # config.i18n.raise_on_missing_translations = true
62 |
63 | # Annotate rendered view with file names.
64 | # config.action_view.annotate_rendered_view_with_filenames = true
65 | end
66 |
--------------------------------------------------------------------------------
/frontend/pages/auth/signup.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useForm } from 'react-hook-form';
3 | import toast from 'react-hot-toast';
4 | import { useRouter } from 'next/router';
5 | import Link from 'next/link';
6 |
7 | import { Button, TextInput } from '../../components/shared';
8 | import { API_URL } from '../../constants';
9 | import { axios } from '../../utils';
10 |
11 | export default function SignUp() {
12 | const router = useRouter();
13 | const [loading, setLoading] = useState(false);
14 |
15 | const {
16 | register,
17 | handleSubmit,
18 | formState: { errors },
19 | } = useForm();
20 |
21 | const onSubmit = async (data) => {
22 | if (loading) return;
23 | setLoading(true);
24 | try {
25 | await axios.post(`users`, data);
26 | toast.success('Account created successfully!');
27 | router.push('/auth/signin');
28 | } catch (e) {
29 | !e.response?.data && toast.error(e.message);
30 | e.response?.data &&
31 | Object.values(e.response?.data)
32 | .filter((el) => typeof el != 'object')
33 | .forEach(toast.error);
34 | } finally {
35 | setLoading(false);
36 | }
37 | };
38 |
39 | return (
40 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/frontend/pages/auth/signin.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useForm } from 'react-hook-form';
3 | import toast from 'react-hot-toast';
4 | import { useRouter } from 'next/router';
5 | import axios from 'axios';
6 | import Link from 'next/link';
7 | import { useCookies } from 'react-cookie';
8 |
9 | import { Button, TextInput } from '../../components/shared';
10 | import { API_URL } from '../../constants';
11 | import { useUser } from '../../store/user';
12 |
13 | export default function SignUp() {
14 | const router = useRouter();
15 | const { addUser } = useUser();
16 | const [_cookie, setCookie] = useCookies(['nextRailsUser']);
17 | const [loading, setLoading] = useState(false);
18 |
19 | const {
20 | register,
21 | handleSubmit,
22 | formState: { errors },
23 | } = useForm();
24 |
25 | const onSubmit = async (formData) => {
26 | if (loading) return;
27 | setLoading(true);
28 | try {
29 | const { data } = await axios.post(`${API_URL}users/login`, formData);
30 | setCookie('nextRailsUser', JSON.stringify(data), {
31 | path: '/',
32 | maxAge:
33 | new Date(data.exp.replace(/\s/, 'T')).getTime() -
34 | new Date().getTime(),
35 | sameSite: true,
36 | });
37 | addUser(data);
38 |
39 | toast.success('Successfully logged in!');
40 | router.push('/');
41 | } catch (e) {
42 | !e.response?.data && toast.error(e.message);
43 | e.response?.data &&
44 | Object.values(e.response?.data)
45 | .filter((el) => typeof el != 'object')
46 | .forEach(toast.error);
47 | } finally {
48 | setLoading(false);
49 | }
50 | };
51 |
52 | return (
53 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Next.js + Ruby on Rails API
2 |
3 | 
4 |
5 |
6 | #### Note: Everything is crafted for my pet projects!
7 |
8 | Next.js on Rails is an opinionated template for quickly setting up a project with Next.js as the frontend and Ruby on Rails as the backend API.
9 |
10 | It follows the best practices of [Next.js](https://nextjs.org/docs/getting-started/introduction) and [Ruby on Rails](https://rubyonrails.org/). It is actively maintained and is a great starting point for new projects.
11 |
12 | We have added Docker support to this template. You can find the Dockerfile in the `docker` directory.
13 |
14 | *For a quick start, you can run the following command:*
15 |
16 | ```bash
17 | docker-compse up
18 | ```
19 |
20 | ## Features:
21 | #### Backend 🚆
22 | - [x] User Authentication using JWT
23 | - [x] User Authorization using Pundit
24 | - [ ] Organizations support(Feature in progress)
25 | - [x] Interactor Pattern for API using interactor gem
26 | - [x] premailer-rails for styling emails with stylesheets
27 | - [x] pagy for faster pagination
28 | - [x] jb for a fast JSON API builder
29 | - [x] MiniTest for testing
30 | - [x] SimpleCov for code coverage
31 |
32 | ### Frontend 🖥
33 | - [x] User Signup and Login
34 | - [x] User Profile
35 | - [x] All basic components under `components/shared`
36 | - [x] DaisyUI for creating additional components
37 | - [x] react-cookie 🍪 for sharing tokens in SSR
38 | - [x] zustand as minimal state management library
39 | - [x] @headlessui/react for accessibile components like `modal/dialog`
40 |
41 |
42 | ## Setup instructions 🔌💡
43 |
44 | ### Manual
45 | - Ruby 3.0.3
46 | - Node >= 16.x.x
47 |
48 | ```bash
49 | # run in the root directory
50 | $ bundle install
51 |
52 | # create database, migrate & seed
53 | $ rails db:prepare
54 |
55 | # install packages and come back to root directory
56 | $ cd frontend && npm install && cd ..
57 |
58 | # run the application using Foreman
59 | # services are defined in the Procfile
60 | $ foreman start
61 | ```
62 |
63 | ### Docker 🚢
64 | ```bash
65 | $ docker-compose up
66 | ```
67 |
68 | ### Screenshots(desktop 🖥 & mobile 📱)
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | ## Contribution
93 | Contributions are appreciated! It can be as simple as fixing a typo.
94 |
95 | If you're facing any difficulty, create issues in the github repo and I will be happy to help.
96 |
--------------------------------------------------------------------------------
/frontend/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 |
3 | import { Button, Card, TextInput } from '../components/shared';
4 |
5 | import { userFromCookie } from '../utils';
6 |
7 | const Home = ({}) => {
8 | return (
9 |
10 |
11 |
Demo: Next.js on Rails
12 |
13 |
14 |
15 |
16 |
25 |
26 |
27 | Buttons
28 |
29 |
30 | Default
31 | Primary
32 | Secondary
33 | Success
34 | Danger
35 | Warning
36 | Info
37 | Light
38 | Dark
39 |
40 |
Card
41 |
42 |
43 |
44 |
45 | Samsung refrigerator
46 |
47 |
48 | Energy efficiency simply means using less energy to perform the
49 | same task
50 |
51 |
52 |
53 |
54 | Are you ready to buy?
55 |
56 | Buy now
57 |
58 |
59 |
Input
60 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export async function getServerSideProps({ req }) {
77 | const user = userFromCookie(req);
78 | return {
79 | props: {
80 | loggedIn: user ? user.token.length > 0 : false,
81 | },
82 | };
83 | }
84 |
85 | export default Home;
86 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 9.3 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On macOS with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On macOS with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem "pg"
16 | #
17 | default: &default
18 | adapter: postgresql
19 | encoding: unicode
20 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
21 | host: <%= ENV.fetch("POSTGRES_HOST") %>
22 | username: <%= ENV.fetch("POSTGRES_USER") %>
23 | password: <%= ENV.fetch("POSTGRES_PASSWORD") %>
24 |
25 |
26 | development:
27 | <<: *default
28 | database: <%= ENV.fetch("DATABASE") %>
29 |
30 | # The specified database role being used to connect to postgres.
31 | # To create additional roles in postgres see `$ createuser --help`.
32 | # When left blank, postgres will use the default role. This is
33 | # the same name as the operating system user running Rails.
34 | #username: nextjs_on_rails
35 |
36 | # The password associated with the postgres role (username).
37 | #password:
38 |
39 | # Connect on a TCP socket. Omitted by default since the client uses a
40 | # domain socket that doesn't need configuration. Windows does not have
41 | # domain sockets, so uncomment these lines.
42 | #host: localhost
43 |
44 | # The TCP port the server listens on. Defaults to 5432.
45 | # If your server runs on a different port number, change accordingly.
46 | #port: 5432
47 |
48 | # Schema search path. The server defaults to $user,public
49 | #schema_search_path: myapp,sharedapp,public
50 |
51 | # Minimum log levels, in increasing order:
52 | # debug5, debug4, debug3, debug2, debug1,
53 | # log, notice, warning, error, fatal, and panic
54 | # Defaults to warning.
55 | #min_messages: notice
56 |
57 | # Warning: The database defined as "test" will be erased and
58 | # re-generated from your development database when you run "rake".
59 | # Do not set this db to the same as development or production.
60 | test:
61 | <<: *default
62 | database: nextjs_on_rails_test
63 |
64 | # As with config/credentials.yml, you never want to store sensitive information,
65 | # like your database password, in your source code. If your source code is
66 | # ever seen by anyone, they now have access to your database.
67 | #
68 | # Instead, provide the password or a full connection URL as an environment
69 | # variable when you boot the app. For example:
70 | #
71 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
72 | #
73 | # If the connection URL is provided in the special DATABASE_URL environment
74 | # variable, Rails will automatically merge its configuration values on top of
75 | # the values provided in this file. Alternatively, you can specify a connection
76 | # URL environment variable explicitly:
77 | #
78 | # production:
79 | # url: <%= ENV["MY_APP_DATABASE_URL"] %>
80 | #
81 | # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
82 | # for a full overview on how database connection configuration can be specified.
83 | #
84 | production:
85 | <<: *default
86 | database: nextjs_on_rails_production
87 | username: nextjs_on_rails
88 | password: <%= ENV["NEXTJS_ON_RAILS_DATABASE_PASSWORD"] %>
89 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require 'rubygems'
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV['BUNDLER_VERSION']
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update`
27 |
28 | bundler_version = nil
29 | update_index = nil
30 | ARGV.each_with_index do |a, i|
31 | bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
32 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
33 |
34 | bundler_version = Regexp.last_match(1)
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV['BUNDLE_GEMFILE']
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path('../Gemfile', __dir__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile)
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 |
59 | lockfile_contents = File.read(lockfile)
60 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
61 |
62 | Regexp.last_match(1)
63 | end
64 |
65 | def bundler_version
66 | @bundler_version ||=
67 | env_var_version || cli_arg_version ||
68 | lockfile_version
69 | end
70 |
71 | def bundler_requirement
72 | return "#{Gem::Requirement.default}.a" unless bundler_version
73 |
74 | bundler_gem_version = Gem::Version.new(bundler_version)
75 |
76 | requirement = bundler_gem_version.approximate_recommendation
77 |
78 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0')
79 |
80 | requirement += '.a' if bundler_gem_version.prerelease?
81 |
82 | requirement
83 | end
84 |
85 | def load_bundler!
86 | ENV['BUNDLE_GEMFILE'] ||= gemfile
87 |
88 | activate_bundler
89 | end
90 |
91 | def activate_bundler
92 | gem_error = activation_error_handling do
93 | gem 'bundler', bundler_requirement
94 | end
95 | return if gem_error.nil?
96 |
97 | require_error = activation_error_handling do
98 | require 'bundler/version'
99 | end
100 | if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
101 | return
102 | end
103 |
104 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
105 | exit 42
106 | end
107 |
108 | def activation_error_handling
109 | yield
110 | nil
111 | rescue StandardError, LoadError => e
112 | e
113 | end
114 | end
115 |
116 | m.load_bundler!
117 |
118 | load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script?
119 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.0].define(version: 2022_03_15_160413) do
14 | # These are extensions that must be enabled in order to support this database
15 | enable_extension "pgcrypto"
16 | enable_extension "plpgsql"
17 |
18 | create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
19 | t.datetime "created_at", null: false
20 | t.datetime "updated_at", null: false
21 | t.jsonb "state"
22 | end
23 |
24 | create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
25 | t.text "queue_name"
26 | t.integer "priority"
27 | t.jsonb "serialized_params"
28 | t.datetime "scheduled_at", precision: nil
29 | t.datetime "performed_at", precision: nil
30 | t.datetime "finished_at", precision: nil
31 | t.text "error"
32 | t.datetime "created_at", null: false
33 | t.datetime "updated_at", null: false
34 | t.uuid "active_job_id"
35 | t.text "concurrency_key"
36 | t.text "cron_key"
37 | t.uuid "retried_good_job_id"
38 | t.datetime "cron_at", precision: nil
39 | t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at"
40 | t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id"
41 | t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)"
42 | t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at"
43 | t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true
44 | t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))"
45 | t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)"
46 | t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)"
47 | end
48 |
49 | create_table "roles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
50 | t.integer "name", default: 0, null: false
51 | t.datetime "created_at", null: false
52 | t.datetime "updated_at", null: false
53 | t.index ["name"], name: "index_roles_on_name", unique: true
54 | end
55 |
56 | create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
57 | t.integer "role", default: 0, null: false
58 | t.string "email", null: false
59 | t.string "first_name", default: ""
60 | t.string "last_name", default: ""
61 | t.string "password_digest", null: false
62 | t.string "reset_password_token"
63 | t.datetime "reset_password_sent_at"
64 | t.integer "sign_in_count", default: 0, null: false
65 | t.string "provider", default: "email", null: false
66 | t.datetime "created_at", null: false
67 | t.datetime "updated_at", null: false
68 | t.index ["email"], name: "index_users_on_email", unique: true
69 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
70 | t.index ["role"], name: "index_users_on_role"
71 | end
72 |
73 | end
74 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_support/core_ext/integer/time'
4 |
5 | Rails.application.configure do
6 | # Settings specified here will take precedence over those in config/application.rb.
7 |
8 | # Code is not reloaded between requests.
9 | config.cache_classes = true
10 |
11 | # Eager load code on boot. This eager loads most of Rails and
12 | # your application in memory, allowing both threaded web servers
13 | # and those relying on copy on write to perform better.
14 | # Rake tasks automatically ignore this option for performance.
15 | config.eager_load = true
16 |
17 | # Full error reports are disabled and caching is turned on.
18 | config.consider_all_requests_local = false
19 |
20 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
21 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
22 | # config.require_master_key = true
23 |
24 | # Disable serving static files from the `/public` folder by default since
25 | # Apache or NGINX already handles this.
26 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
27 |
28 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
29 | # config.asset_host = "http://assets.example.com"
30 |
31 | # Specifies the header that your server uses for sending files.
32 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
33 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
34 |
35 | # Store uploaded files on the local file system (see config/storage.yml for options).
36 | config.active_storage.service = :local
37 |
38 | # Mount Action Cable outside main process or domain.
39 | # config.action_cable.mount_path = nil
40 | # config.action_cable.url = "wss://example.com/cable"
41 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
42 |
43 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
44 | # config.force_ssl = true
45 |
46 | # Include generic and useful information about system operation, but avoid logging too much
47 | # information to avoid inadvertent exposure of personally identifiable information (PII).
48 | config.log_level = :info
49 |
50 | # Prepend all log lines with the following tags.
51 | config.log_tags = [:request_id]
52 |
53 | # Use a different cache store in production.
54 | # config.cache_store = :mem_cache_store
55 |
56 | # Use a real queuing backend for Active Job (and separate queues per environment).
57 | # config.active_job.queue_adapter = :resque
58 | # config.active_job.queue_name_prefix = "nextjs_on_rails_production"
59 |
60 | config.action_mailer.perform_caching = false
61 |
62 | # Ignore bad email addresses and do not raise email delivery errors.
63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
64 | # config.action_mailer.raise_delivery_errors = false
65 |
66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
67 | # the I18n.default_locale when a translation cannot be found).
68 | config.i18n.fallbacks = true
69 |
70 | # Don't log any deprecations.
71 | config.active_support.report_deprecations = false
72 |
73 | # Use default logging formatter so that PID and timestamp are not suppressed.
74 | config.log_formatter = ::Logger::Formatter.new
75 |
76 | # Use a different logger for distributed setups.
77 | # require "syslog/logger"
78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
79 | config.active_job.queue_adapter = :good_job
80 | config.good_job.execution_mode = :external
81 |
82 | if ENV['RAILS_LOG_TO_STDOUT'].present?
83 | logger = ActiveSupport::Logger.new($stdout)
84 | logger.formatter = config.log_formatter
85 | config.logger = ActiveSupport::TaggedLogging.new(logger)
86 | end
87 |
88 | # Do not dump schema after migrations.
89 | config.active_record.dump_schema_after_migration = false
90 | end
91 |
--------------------------------------------------------------------------------
/test/controllers/api/v1/users_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'test_helper'
4 |
5 | module Api
6 | module V1
7 | class UsersControllerTest < ActionDispatch::IntegrationTest
8 | def test_create_user_failure
9 | post api_v1_users_path, params: {
10 | email: 'akhil@example.com'
11 | }
12 | assert_equal 422, status
13 | assert response.parsed_body.key?('errors')
14 | assert_enqueued_jobs 0
15 | end
16 |
17 | def test_create_user_success
18 | assert_enqueued_jobs 0
19 | post api_v1_users_path, params: {
20 | email: 'akhil@example.com',
21 | password: 'password'
22 | }
23 | assert_equal 201, status
24 | assert_enqueued_jobs 1
25 | assert_enqueued_with job: UserMailerJob
26 | end
27 |
28 | def test_update_user_authentication_failure_when_unauthenticated
29 | user = create(:user)
30 | patch api_v1_user_path(user), params: {
31 | email: 'some@example.com'
32 | }
33 | assert response.parsed_body.key?('errors')
34 | assert_equal 'Authentication token not provided', response.parsed_body['errors']
35 | end
36 |
37 | def test_update_user_success_when_authenticated
38 | user = create(:user)
39 | patch api_v1_user_path(user), params: {
40 | email: 'some@example.com'
41 | }, headers: get_auth_headers(user)
42 | assert response.parsed_body.key?('message')
43 | assert_equal 'some@example.com', user.reload.email
44 | end
45 |
46 | def test_update_user_failure_when_unauthorized
47 | user1 = create(:user)
48 | user2 = create(:user)
49 |
50 | patch api_v1_user_path(user1), params: {
51 | email: 'some@example.com'
52 | }, headers: get_auth_headers(user2)
53 |
54 | assert response.parsed_body.key?('errors')
55 | assert_equal I18n.t('authorization.error'), response.parsed_body['errors']
56 | end
57 |
58 | def test_update_user_failure_when_email_already_exists
59 | user1 = create(:user)
60 | user2 = create(:user)
61 |
62 | patch api_v1_user_path(user1), params: {
63 | email: user2.email
64 | }, headers: get_auth_headers(user1)
65 |
66 | assert response.parsed_body.key?('errors')
67 | assert response.parsed_body['errors'].include? 'Email has already been taken'
68 | end
69 |
70 | def test_verify_failure_when_token_not_found
71 | user = create(:user, :unverified)
72 | get users_verify_path(token: 'some-token')
73 | assert response.parsed_body.key?('errors')
74 | assert_equal I18n.t('user.errors.verify'), response.parsed_body['errors']
75 | end
76 |
77 | def test_verify_success
78 | user = create(:user, :unverified)
79 | get users_verify_path(token: user.reset_password_token)
80 | assert user.reload.reset_password_token.nil?
81 | end
82 |
83 | def test_login_failure_when_unverified
84 | user = create(:user, :unverified, password: 'password')
85 |
86 | post login_api_v1_users_path, params: {
87 | email: user.email,
88 | password: 'password'
89 | }
90 | assert_equal I18n.t('user.errors.login'), response.parsed_body['errors']
91 | end
92 |
93 | def test_login_failure_when_wrong_credentials
94 | user = create(:user, password: 'examplepassword')
95 |
96 | post login_api_v1_users_path, params: {
97 | email: user.email,
98 | password: 'password'
99 | }
100 | assert_equal I18n.t('user.errors.login'), response.parsed_body['errors']
101 | end
102 |
103 | def test_login_success
104 | user = create(:user, password: 'password')
105 |
106 | post login_api_v1_users_path, params: {
107 | email: user.email,
108 | password: 'password'
109 | }
110 | assert response.parsed_body.key?('token')
111 | assert_equal user.email, response.parsed_body['email']
112 | end
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (7.0.3.1)
5 | actionpack (= 7.0.3.1)
6 | activesupport (= 7.0.3.1)
7 | nio4r (~> 2.0)
8 | websocket-driver (>= 0.6.1)
9 | actionmailbox (7.0.3.1)
10 | actionpack (= 7.0.3.1)
11 | activejob (= 7.0.3.1)
12 | activerecord (= 7.0.3.1)
13 | activestorage (= 7.0.3.1)
14 | activesupport (= 7.0.3.1)
15 | mail (>= 2.7.1)
16 | net-imap
17 | net-pop
18 | net-smtp
19 | actionmailer (7.0.3.1)
20 | actionpack (= 7.0.3.1)
21 | actionview (= 7.0.3.1)
22 | activejob (= 7.0.3.1)
23 | activesupport (= 7.0.3.1)
24 | mail (~> 2.5, >= 2.5.4)
25 | net-imap
26 | net-pop
27 | net-smtp
28 | rails-dom-testing (~> 2.0)
29 | actionpack (7.0.3.1)
30 | actionview (= 7.0.3.1)
31 | activesupport (= 7.0.3.1)
32 | rack (~> 2.0, >= 2.2.0)
33 | rack-test (>= 0.6.3)
34 | rails-dom-testing (~> 2.0)
35 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
36 | actiontext (7.0.3.1)
37 | actionpack (= 7.0.3.1)
38 | activerecord (= 7.0.3.1)
39 | activestorage (= 7.0.3.1)
40 | activesupport (= 7.0.3.1)
41 | globalid (>= 0.6.0)
42 | nokogiri (>= 1.8.5)
43 | actionview (7.0.3.1)
44 | activesupport (= 7.0.3.1)
45 | builder (~> 3.1)
46 | erubi (~> 1.4)
47 | rails-dom-testing (~> 2.0)
48 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
49 | activejob (7.0.3.1)
50 | activesupport (= 7.0.3.1)
51 | globalid (>= 0.3.6)
52 | activemodel (7.0.3.1)
53 | activesupport (= 7.0.3.1)
54 | activerecord (7.0.3.1)
55 | activemodel (= 7.0.3.1)
56 | activesupport (= 7.0.3.1)
57 | activestorage (7.0.3.1)
58 | actionpack (= 7.0.3.1)
59 | activejob (= 7.0.3.1)
60 | activerecord (= 7.0.3.1)
61 | activesupport (= 7.0.3.1)
62 | marcel (~> 1.0)
63 | mini_mime (>= 1.1.0)
64 | activesupport (7.0.3.1)
65 | concurrent-ruby (~> 1.0, >= 1.0.2)
66 | i18n (>= 1.6, < 2)
67 | minitest (>= 5.1)
68 | tzinfo (~> 2.0)
69 | addressable (2.8.0)
70 | public_suffix (>= 2.0.2, < 5.0)
71 | annotate (3.2.0)
72 | activerecord (>= 3.2, < 8.0)
73 | rake (>= 10.4, < 14.0)
74 | ast (2.4.2)
75 | bcrypt (3.1.16)
76 | bootsnap (1.11.1)
77 | msgpack (~> 1.2)
78 | builder (3.2.4)
79 | bullet (7.0.1)
80 | activesupport (>= 3.0.0)
81 | uniform_notifier (~> 1.11)
82 | coderay (1.1.3)
83 | concurrent-ruby (1.1.10)
84 | crack (0.4.5)
85 | rexml
86 | crass (1.0.6)
87 | css_parser (1.11.0)
88 | addressable
89 | digest (3.1.0)
90 | docile (1.4.0)
91 | dotenv (2.7.6)
92 | dotenv-rails (2.7.6)
93 | dotenv (= 2.7.6)
94 | railties (>= 3.2)
95 | erubi (1.11.0)
96 | et-orbi (1.2.7)
97 | tzinfo
98 | factory_bot (6.2.0)
99 | activesupport (>= 5.0.0)
100 | factory_bot_rails (6.2.0)
101 | factory_bot (~> 6.2.0)
102 | railties (>= 5.0.0)
103 | faker (2.20.0)
104 | i18n (>= 1.8.11, < 2)
105 | fugit (1.5.2)
106 | et-orbi (~> 1.1, >= 1.1.8)
107 | raabro (~> 1.4)
108 | globalid (1.0.0)
109 | activesupport (>= 5.0)
110 | good_job (2.10.0)
111 | activejob (>= 5.2.0)
112 | activerecord (>= 5.2.0)
113 | concurrent-ruby (>= 1.0.2)
114 | fugit (>= 1.1)
115 | railties (>= 5.2.0)
116 | thor (>= 0.14.1)
117 | webrick (>= 1.3)
118 | zeitwerk (>= 2.0)
119 | hashdiff (1.0.1)
120 | htmlentities (4.3.4)
121 | i18n (1.12.0)
122 | concurrent-ruby (~> 1.0)
123 | interactor (3.1.2)
124 | interactor-rails (2.2.1)
125 | interactor (~> 3.0)
126 | rails (>= 4.2)
127 | jb (0.8.0)
128 | jwt (2.3.0)
129 | launchy (2.5.0)
130 | addressable (~> 2.7)
131 | letter_opener (1.8.0)
132 | launchy (>= 2.2, < 3)
133 | loofah (2.18.0)
134 | crass (~> 1.0.2)
135 | nokogiri (>= 1.5.9)
136 | mail (2.7.1)
137 | mini_mime (>= 0.1.1)
138 | marcel (1.0.2)
139 | method_source (1.0.0)
140 | mini_mime (1.1.2)
141 | minitest (5.16.3)
142 | mocha (1.13.0)
143 | msgpack (1.4.5)
144 | net-imap (0.2.3)
145 | digest
146 | net-protocol
147 | strscan
148 | net-pop (0.1.1)
149 | digest
150 | net-protocol
151 | timeout
152 | net-protocol (0.1.3)
153 | timeout
154 | net-smtp (0.3.1)
155 | digest
156 | net-protocol
157 | timeout
158 | nio4r (2.5.8)
159 | nokogiri (1.13.8-aarch64-linux)
160 | racc (~> 1.4)
161 | nokogiri (1.13.8-x86_64-darwin)
162 | racc (~> 1.4)
163 | pagy (5.10.1)
164 | activesupport
165 | parallel (1.21.0)
166 | parser (3.1.1.0)
167 | ast (~> 2.4.1)
168 | pg (1.4.3)
169 | premailer (1.15.0)
170 | addressable
171 | css_parser (>= 1.6.0)
172 | htmlentities (>= 4.0.0)
173 | premailer-rails (1.11.1)
174 | actionmailer (>= 3)
175 | premailer (~> 1.7, >= 1.7.9)
176 | pry (0.14.1)
177 | coderay (~> 1.1)
178 | method_source (~> 1.0)
179 | pry-rails (0.3.9)
180 | pry (>= 0.10.4)
181 | public_suffix (4.0.6)
182 | puma (5.6.4)
183 | nio4r (~> 2.0)
184 | pundit (2.2.0)
185 | activesupport (>= 3.0.0)
186 | raabro (1.4.0)
187 | racc (1.6.0)
188 | rack (2.2.4)
189 | rack-cors (1.1.1)
190 | rack (>= 2.0.0)
191 | rack-test (2.0.2)
192 | rack (>= 1.3)
193 | rails (7.0.3.1)
194 | actioncable (= 7.0.3.1)
195 | actionmailbox (= 7.0.3.1)
196 | actionmailer (= 7.0.3.1)
197 | actionpack (= 7.0.3.1)
198 | actiontext (= 7.0.3.1)
199 | actionview (= 7.0.3.1)
200 | activejob (= 7.0.3.1)
201 | activemodel (= 7.0.3.1)
202 | activerecord (= 7.0.3.1)
203 | activestorage (= 7.0.3.1)
204 | activesupport (= 7.0.3.1)
205 | bundler (>= 1.15.0)
206 | railties (= 7.0.3.1)
207 | rails-dom-testing (2.0.3)
208 | activesupport (>= 4.2.0)
209 | nokogiri (>= 1.6)
210 | rails-html-sanitizer (1.4.3)
211 | loofah (~> 2.3)
212 | railties (7.0.3.1)
213 | actionpack (= 7.0.3.1)
214 | activesupport (= 7.0.3.1)
215 | method_source
216 | rake (>= 12.2)
217 | thor (~> 1.0)
218 | zeitwerk (~> 2.5)
219 | rainbow (3.1.1)
220 | rake (13.0.6)
221 | regexp_parser (2.2.1)
222 | rexml (3.2.5)
223 | rubocop (1.26.0)
224 | parallel (~> 1.10)
225 | parser (>= 3.1.0.0)
226 | rainbow (>= 2.2.2, < 4.0)
227 | regexp_parser (>= 1.8, < 3.0)
228 | rexml
229 | rubocop-ast (>= 1.16.0, < 2.0)
230 | ruby-progressbar (~> 1.7)
231 | unicode-display_width (>= 1.4.0, < 3.0)
232 | rubocop-ast (1.16.0)
233 | parser (>= 3.1.1.0)
234 | rubocop-github (0.17.0)
235 | rubocop
236 | rubocop-performance
237 | rubocop-rails
238 | rubocop-performance (1.13.3)
239 | rubocop (>= 1.7.0, < 2.0)
240 | rubocop-ast (>= 0.4.0)
241 | rubocop-rails (2.13.2)
242 | activesupport (>= 4.2.0)
243 | rack (>= 1.1)
244 | rubocop (>= 1.7.0, < 2.0)
245 | ruby-progressbar (1.11.0)
246 | simplecov (0.21.2)
247 | docile (~> 1.1)
248 | simplecov-html (~> 0.11)
249 | simplecov_json_formatter (~> 0.1)
250 | simplecov-html (0.12.3)
251 | simplecov_json_formatter (0.1.4)
252 | strong_migrations (0.8.0)
253 | activerecord (>= 5.2)
254 | strscan (3.0.4)
255 | thor (1.2.1)
256 | timeout (0.3.0)
257 | tzinfo (2.0.5)
258 | concurrent-ruby (~> 1.0)
259 | unicode-display_width (2.1.0)
260 | uniform_notifier (1.14.2)
261 | webmock (3.14.0)
262 | addressable (>= 2.8.0)
263 | crack (>= 0.3.2)
264 | hashdiff (>= 0.4.0, < 2.0.0)
265 | webrick (1.7.0)
266 | websocket-driver (0.7.5)
267 | websocket-extensions (>= 0.1.0)
268 | websocket-extensions (0.1.5)
269 | zeitwerk (2.6.0)
270 |
271 | PLATFORMS
272 | aarch64-linux
273 | x86_64-darwin-20
274 | x86_64-darwin-21
275 |
276 | DEPENDENCIES
277 | annotate (~> 3.2)
278 | bcrypt (~> 3.1.7)
279 | bootsnap
280 | bullet (~> 7.0.1)
281 | dotenv-rails
282 | factory_bot_rails (~> 6.2.0)
283 | faker (~> 2.20.0)
284 | good_job
285 | interactor (~> 3.0)
286 | interactor-rails (~> 2.0)
287 | jb
288 | jwt
289 | letter_opener (~> 1.8)
290 | mocha
291 | pagy (~> 5.10)
292 | pg (= 1.4.3)
293 | premailer-rails
294 | pry-rails
295 | puma
296 | pundit
297 | rack-cors
298 | rails (= 7.0.3.1)
299 | rubocop-github
300 | rubocop-performance
301 | rubocop-rails
302 | simplecov (~> 0.21.2)
303 | strong_migrations
304 | tzinfo-data
305 | webmock
306 |
307 | RUBY VERSION
308 | ruby 3.1.2p20
309 |
310 | BUNDLED WITH
311 | 2.3.5
312 |
--------------------------------------------------------------------------------
/frontend/components/themes/default.js:
--------------------------------------------------------------------------------
1 | export default {
2 | button: {
3 | default: {
4 | base: 'p-3 flex items-center justify-between space-x-4 text-base uppercase font-medium tracking-wider rounded-lg shadow-md focus:outline-none transition hover:shadow-lg focus:ring-2 ring-offset-2 ring-gray-600',
5 | primary: 'bg-purple-700 hover:bg-purple-500 text-white',
6 | success: 'bg-green-500 hover:bg-green-600 text-white',
7 | danger: 'bg-red-500 hover:bg-red-600 text-white',
8 | neutral: 'bg-gray-800 hover:bg-gray-700 text-white',
9 | link: 'bg-transparent shadow-none hover:shadow-none hover:bg-gray-200 hover:text-black',
10 | },
11 | outline: {
12 | base: 'p-3 border flex items-center justify-between space-x-4 text-base uppercase font-medium tracking-wider rounded-lg shadow-md focus:outline-none transition hover:shadow-lg focus:ring-2 ring-offset-2 ring-gray-600',
13 | primary:
14 | 'border-purple-600 text-black hover:bg-purple-800 hover:text-white',
15 | success:
16 | 'border-green-500 text-black hover:bg-green-800 hover:text-white',
17 | danger: 'border-red-500 text-black hover:bg-red-800 hover:text-white',
18 | neutral: 'border-gray-500 text-black hover:bg-gray-800 hover:text-white',
19 | },
20 | },
21 | card: {
22 | default: 'flex flex-col bg-white transition hover:shadow-2xl hover:ring-2',
23 | head: 'text-left font-bold tracking-wider text-xl p-2',
24 | body: 'text-gray-800',
25 | foot: 'flex justify-end space-x-4',
26 | },
27 | pill: {
28 | base: 'flex items-center space-x-1 px-3 py-1 rounded-3xl font-semibold text-sm',
29 | success:
30 | 'text-green-700 bg-green-50 dark:bg-green-700 dark:text-green-50 hover:bg-green-100 dark:hover:bg-green-600',
31 | danger:
32 | 'text-red-700 bg-red-50 dark:text-red-50 dark:bg-red-700 hover:bg-red-100 dark:hover:bg-red-600',
33 | warning:
34 | 'text-yellow-700 bg-yellow-50 dark:text-white dark:bg-yellow-600 hover:bg-yellow-100 dark:hover:bg-yellow-600',
35 | primary:
36 | 'text-purple-700 bg-purple-50 dark:text-white dark:bg-purple-600 hover:bg-purple-100 dark:hover:bg-purple-600',
37 | default: 'text-gray-800 bg-gray-200 hover:bg-gray-800 hover:text-white',
38 | },
39 | backdrop: {
40 | base: 'fixed inset-0 z-40 flex items-end sm:items-center sm:justify-center',
41 | blurred: 'bg-gray-500 bg-opacity-20 backdrop-filter backdrop-blur-xs',
42 | },
43 | dropdown: {
44 | align: {
45 | left: 'left-0',
46 | right: 'right-0',
47 | },
48 | list: {
49 | base: 'absolute w-64 p-2 mt-2 text-gray-800 font-semibold bg-white bg-opacity-60 text-sm ring-2 ring-gray-800 rounded-xl shadow-md min-w-max-content backdrop-filter backdrop-blur-lg',
50 | },
51 | item: {
52 | base: 'flex uppercase w-full justify-between cursor-pointer hover:text-white rounded-md hover:bg-gray-800 px-3 py-2',
53 | },
54 | },
55 | input: {
56 | base: 'block w-full px-2 py-3 font-medium transition duration-200 focus:shadow-lg focus:outline-none ring-offset-2',
57 | active: 'focus:ring-2 focus:ring-purple-300',
58 | valid: 'focus:ring-2 focus:ring-green-300',
59 | invalid: 'focus:ring-2 focus:ring-red-400',
60 | disabled: 'cursor-not-allowed opacity-50 bg-gray-300',
61 | bordered: 'border border-gray-400 rounded-lg',
62 | },
63 | modal: {
64 | base: 'my-auto p-6 overflow-hidden bg-white rounded-3xl shadow-xl',
65 | xButton:
66 | 'inline-flex items-center justify-center w-6 h-6 text-gray-400 transition-colors duration-150 rounded hover:text-gray-900 hover:bg-gray-200',
67 | },
68 | table: {
69 | base: 'w-full whitespace-no-wrap',
70 | scroller: 'w-full overflow-x-auto',
71 | container: 'w-full overflow-hidden rounded-xl shadow-md',
72 | },
73 | thead: {
74 | base: 'border-b text-md font-semibold tracking-wide text-left text-gray-500 uppercase bg-purple-50',
75 | },
76 | th: {
77 | base: 'py-6 px-4',
78 | },
79 | tr: {
80 | base: {
81 | even: 'bg-blue-50 transition hover:bg-gray-50',
82 | odd: 'bg-white transition hover:bg-gray-50',
83 | },
84 | },
85 | tbody: {
86 | base: 'text-lg font-normal divide-y text-gray-700',
87 | },
88 | td: {
89 | base: 'p-4',
90 | },
91 | tfoot: {
92 | base: 'py-6 px-4 border-t text-gray-500 bg-purple-50',
93 | },
94 | list: {
95 | base: 'w-full overflow-hidden flex flex-col justify-center items-center list-none divide-y divide-gray-200 border-2 border-gray-800 rounded-xl',
96 | header:
97 | 'w-full px-6 py-4 bg-gray-500 text-white font-semibold text-xl flex items-center justify-start',
98 | item: {
99 | base: 'flex group w-full px-6 py-3 transition hover:bg-gray-900 hover:text-white',
100 | },
101 | },
102 | alert: {
103 | base: 'flex w-full px-6 py-4 my-2 rounded-xl shadow-sm font-semibold text-md',
104 | variant: {
105 | outlined: {
106 | default: 'border border-gray-300 text-gray-600',
107 | error: 'border border-red-300 text-red-600',
108 | warning: 'border border-yellow-300 text-yellow-600',
109 | success: 'border border-green-300 text-green-600',
110 | info: 'border border-purple-300 text-purple-600',
111 | },
112 | default: {
113 | default: 'bg-gray-50 text-gray-800',
114 | error: 'bg-red-50 text-red-800',
115 | warning: 'bg-yellow-50 text-yellow-800',
116 | success: 'bg-green-50 text-green-800',
117 | info: 'bg-purple-50 text-purple-800',
118 | },
119 | filled: {
120 | default: 'bg-gray-900 text-white',
121 | error: 'bg-red-900 text-white',
122 | warning: 'bg-yellow-900 text-white',
123 | success: 'bg-green-900 text-white',
124 | info: 'bg-purple-900 text-white',
125 | },
126 | },
127 | },
128 | avatar: {
129 | base: '',
130 | size: {
131 | large: 'w-20 h-20',
132 | regular: 'w-12 h-12',
133 | small: 'w-6 h-6',
134 | },
135 | },
136 | select: {
137 | base: 'w-full border p-3 rounded-lg transition duration-200 focus:outline-none focus:ring-2 ring-purple-400',
138 | valid: 'border-green-300',
139 | invalid: 'border-red-500',
140 | active: '',
141 | disabled: 'bg-gray-100 cursor-not-allowed',
142 | },
143 | option: {
144 | base: '',
145 | },
146 | label: {
147 | base: 'block space-y-1 my-2 w-full text-gray-800 text-md font-semibold',
148 | default: '',
149 | outlined:
150 | 'p-4 rounded-xl border border-purple-600 transition hover:bg-gray-50 hover:bg-opacity-50 hover:border-purple-800',
151 | },
152 | helpertext: {
153 | base: 'text-sm font-semibold',
154 | info: 'text-gray-500',
155 | error: 'text-red-500',
156 | success: 'text-green-500',
157 | warn: 'text-yellow-500',
158 | },
159 | toggleSwitch: {
160 | base: {
161 | rect: 'cursor-pointer transition-all duration-400 ease-in-out fill-current',
162 | circle: 'transition-all duration-400 ease-in-out fill-current text-white',
163 | toggledOff: 'text-gray-300',
164 | },
165 | disabled: {
166 | rect: 'fill-current text-gray-400',
167 | circle: 'fill-current text-gray-300',
168 | },
169 | primary: 'text-blue-400',
170 | danger: 'text-red-400',
171 | success: 'text-green-400',
172 | neutral: 'text-gray-800',
173 | },
174 | link: {
175 | base: 'inline font-semibold transition duration-400 ease-in-out',
176 | default: 'text-purple-500 hover:text-purple-700 active:text-purple-300',
177 | info: 'text-blue-500 hover:text-blue-700 active:text-blue-300',
178 | error: 'text-red-500 hover:text-red-700 active:text-red-300',
179 | success: 'text-green-500 hover:text-green-700 active:text-green-300',
180 | warn: 'text-yellow-500 hover:text-yellow-700 active:yellow-purple-300',
181 | },
182 |
183 | breadcrumbs: {
184 | base: 'flex px-6 py-2',
185 | default: 'text-blue-600 font-semibold text-sm border-b',
186 | error: 'border border-red-300 bg-red-50 text-red-500',
187 | warn: 'border border-yellow-300 bg-yellow-50 text-yellow-500',
188 | success: 'border border-green-300 bg-green-50 text-green-500',
189 | info: 'border border-blue-300 bg-blue-50 text-blue-500',
190 | },
191 | transition: {
192 | fade: {
193 | appear: 'opacity-0',
194 | appearActive: 'opacity-100 transition-opacity ease-in-out opacity-100',
195 | appearDone: 'opacity-100',
196 | enter: 'opacity-0',
197 | enterActive: 'opacity-100 transition-opacity ease-in-out',
198 | enterDone: 'opacity-100',
199 | exit: 'opacity-100',
200 | exitActive: 'opacity-0',
201 | exitDone: 'transition-opacity ease-in-out opacity-0',
202 | },
203 | grow: {
204 | appear: 'transform scale-0',
205 | appearActive: 'transform scale-100 transition-transform ease-in-out',
206 | appearDone: 'transform scale-100',
207 | enter: 'transform scale-0',
208 | enterActive: 'transform scale-100 transition-transform ease-in-out',
209 | enterDone: 'transform scale-100',
210 | exit: 'transform scale-100',
211 | exitActive: 'transform scale-0',
212 | exitDone: 'transition-transform ease-in-out transform scale-0',
213 | },
214 | },
215 | };
216 |
--------------------------------------------------------------------------------
/coverage/.resultset.json:
--------------------------------------------------------------------------------
1 | {
2 | "Minitest": {
3 | "coverage": {
4 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/environment.rb": {
5 | "lines": [
6 | null,
7 | null,
8 | null,
9 | 1,
10 | null,
11 | null,
12 | 1
13 | ]
14 | },
15 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/application.rb": {
16 | "lines": [
17 | null,
18 | null,
19 | 1,
20 | null,
21 | 1,
22 | null,
23 | null,
24 | null,
25 | 1,
26 | null,
27 | 1,
28 | 1,
29 | null,
30 | 1,
31 | null,
32 | null,
33 | null,
34 | null,
35 | null,
36 | null,
37 | null,
38 | null,
39 | null,
40 | null,
41 | null,
42 | null,
43 | 1,
44 | 1,
45 | null,
46 | null
47 | ]
48 | },
49 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/environments/test.rb": {
50 | "lines": [
51 | null,
52 | null,
53 | 1,
54 | null,
55 | null,
56 | null,
57 | null,
58 | null,
59 | null,
60 | 1,
61 | null,
62 | null,
63 | null,
64 | 1,
65 | null,
66 | null,
67 | null,
68 | null,
69 | 1,
70 | null,
71 | null,
72 | 1,
73 | 1,
74 | null,
75 | null,
76 | null,
77 | null,
78 | 1,
79 | 1,
80 | 1,
81 | null,
82 | 1,
83 | 1,
84 | null,
85 | null,
86 | 1,
87 | null,
88 | null,
89 | 1,
90 | null,
91 | null,
92 | 1,
93 | null,
94 | 1,
95 | null,
96 | null,
97 | null,
98 | null,
99 | 1,
100 | null,
101 | null,
102 | 1,
103 | null,
104 | null,
105 | 1,
106 | null,
107 | null,
108 | 1,
109 | null,
110 | null,
111 | null,
112 | null,
113 | null,
114 | null,
115 | null
116 | ]
117 | },
118 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/cors.rb": {
119 | "lines": [
120 | null,
121 | null,
122 | null,
123 | 1,
124 | 1,
125 | 1,
126 | null,
127 | 1,
128 | null,
129 | null,
130 | null,
131 | null
132 | ]
133 | },
134 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/filter_parameter_logging.rb": {
135 | "lines": [
136 | null,
137 | null,
138 | null,
139 | null,
140 | null,
141 | null,
142 | null,
143 | 1,
144 | null,
145 | null
146 | ]
147 | },
148 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/generators.rb": {
149 | "lines": [
150 | null,
151 | null,
152 | 1,
153 | 1,
154 | null
155 | ]
156 | },
157 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/inflections.rb": {
158 | "lines": [
159 | null,
160 | null,
161 | null,
162 | null,
163 | null,
164 | null,
165 | null,
166 | null,
167 | null,
168 | null,
169 | null,
170 | null,
171 | null,
172 | null,
173 | null,
174 | null,
175 | null
176 | ]
177 | },
178 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/premailer_rails.rb": {
179 | "lines": [
180 | null,
181 | null,
182 | 1
183 | ]
184 | },
185 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/test/factories/users.rb": {
186 | "lines": [
187 | null,
188 | null,
189 | null,
190 | null,
191 | null,
192 | null,
193 | null,
194 | null,
195 | null,
196 | null,
197 | null,
198 | null,
199 | null,
200 | null,
201 | null,
202 | null,
203 | null,
204 | null,
205 | null,
206 | null,
207 | null,
208 | null,
209 | null,
210 | null,
211 | null,
212 | null,
213 | 1,
214 | 1,
215 | 19,
216 | 19,
217 | 19,
218 | 17,
219 | 16,
220 | 19,
221 | 14,
222 | null,
223 | 1,
224 | 4,
225 | null,
226 | null,
227 | null
228 | ]
229 | },
230 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/routes.rb": {
231 | "lines": [
232 | null,
233 | null,
234 | null,
235 | null,
236 | null,
237 | 1,
238 | 1,
239 | 1,
240 | 1,
241 | 1,
242 | null,
243 | null,
244 | null,
245 | 1,
246 | null
247 | ]
248 | },
249 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/test/controllers/api/v1/users_controller_test.rb": {
250 | "lines": [
251 | null,
252 | null,
253 | 1,
254 | null,
255 | 1,
256 | 1,
257 | 1,
258 | 1,
259 | 1,
260 | null,
261 | null,
262 | 1,
263 | 1,
264 | 1,
265 | null,
266 | null,
267 | 1,
268 | 1,
269 | 1,
270 | null,
271 | null,
272 | null,
273 | 1,
274 | 1,
275 | 1,
276 | null,
277 | null,
278 | 1,
279 | 1,
280 | 1,
281 | null,
282 | null,
283 | 1,
284 | 1,
285 | null,
286 | null,
287 | 1,
288 | 1,
289 | 1,
290 | null,
291 | null,
292 | 1,
293 | 1,
294 | null,
295 | null,
296 | 1,
297 | 1,
298 | 1,
299 | null,
300 | 1,
301 | null,
302 | null,
303 | null,
304 | 1,
305 | 1,
306 | null,
307 | null,
308 | 1,
309 | 1,
310 | 1,
311 | 1,
312 | 1,
313 | null,
314 | null,
315 | 1,
316 | 1,
317 | 1,
318 | 1,
319 | null,
320 | null,
321 | 1,
322 | 1,
323 | null,
324 | 1,
325 | null,
326 | null,
327 | null,
328 | 1,
329 | null,
330 | null,
331 | 1,
332 | 1,
333 | null,
334 | 1,
335 | null,
336 | null,
337 | null,
338 | 1,
339 | null,
340 | null,
341 | 1,
342 | 1,
343 | null,
344 | 1,
345 | null,
346 | null,
347 | null,
348 | 1,
349 | 1,
350 | null,
351 | null,
352 | null,
353 | null
354 | ]
355 | },
356 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/test/mailers/user_verification_mailer_test.rb": {
357 | "lines": [
358 | null,
359 | null,
360 | 1,
361 | null,
362 | 1,
363 | null,
364 | null,
365 | null,
366 | null
367 | ]
368 | },
369 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/test/models/user_test.rb": {
370 | "lines": [
371 | null,
372 | null,
373 | null,
374 | null,
375 | null,
376 | null,
377 | null,
378 | null,
379 | null,
380 | null,
381 | null,
382 | null,
383 | null,
384 | null,
385 | null,
386 | null,
387 | null,
388 | null,
389 | null,
390 | null,
391 | null,
392 | null,
393 | null,
394 | null,
395 | null,
396 | null,
397 | 1,
398 | null,
399 | 1,
400 | 1,
401 | 5,
402 | null,
403 | null,
404 | 1,
405 | 1,
406 | null,
407 | null,
408 | 1,
409 | 1,
410 | 1,
411 | 1,
412 | null,
413 | 1,
414 | null,
415 | null,
416 | 1,
417 | 1,
418 | 1,
419 | 1,
420 | null,
421 | 1,
422 | null,
423 | null,
424 | 1,
425 | 1,
426 | 1,
427 | 1,
428 | null,
429 | 1,
430 | null,
431 | 1,
432 | 1,
433 | null,
434 | null,
435 | 1,
436 | 1,
437 | 1,
438 | 1,
439 | null,
440 | null
441 | ]
442 | },
443 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/models/user.rb": {
444 | "lines": [
445 | null,
446 | null,
447 | null,
448 | null,
449 | null,
450 | null,
451 | null,
452 | null,
453 | null,
454 | null,
455 | null,
456 | null,
457 | null,
458 | null,
459 | null,
460 | null,
461 | null,
462 | null,
463 | null,
464 | null,
465 | null,
466 | null,
467 | null,
468 | null,
469 | null,
470 | null,
471 | 1,
472 | 1,
473 | 1,
474 | null,
475 | 1,
476 | 1,
477 | null,
478 | 4,
479 | null,
480 | 1,
481 | 0,
482 | null,
483 | null
484 | ]
485 | },
486 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/models/application_record.rb": {
487 | "lines": [
488 | null,
489 | null,
490 | 1,
491 | 1,
492 | null,
493 | 1,
494 | 1,
495 | null,
496 | null
497 | ]
498 | },
499 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/models/json_web_token.rb": {
500 | "lines": [
501 | null,
502 | null,
503 | 1,
504 | 1,
505 | null,
506 | 1,
507 | 3,
508 | 3,
509 | null,
510 | null,
511 | 1,
512 | 2,
513 | 2,
514 | null,
515 | null
516 | ]
517 | },
518 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/controllers/api/v1/users_controller.rb": {
519 | "lines": [
520 | null,
521 | null,
522 | 1,
523 | 1,
524 | 1,
525 | 1,
526 | 1,
527 | null,
528 | null,
529 | 1,
530 | 2,
531 | 2,
532 | 1,
533 | null,
534 | 1,
535 | null,
536 | null,
537 | null,
538 | 1,
539 | 2,
540 | 1,
541 | 1,
542 | null,
543 | 0,
544 | null,
545 | null,
546 | null,
547 | 1,
548 | 3,
549 | 3,
550 | 1,
551 | 1,
552 | 1,
553 | null,
554 | null,
555 | null,
556 | null,
557 | null,
558 | 2,
559 | null,
560 | null,
561 | null,
562 | 1,
563 | 2,
564 | 2,
565 | null,
566 | 1,
567 | 1,
568 | null,
569 | null,
570 | 1,
571 | null,
572 | 1,
573 | 2,
574 | 2,
575 | null,
576 | null,
577 | 1,
578 | 3,
579 | null,
580 | null,
581 | 1,
582 | 5,
583 | null,
584 | null,
585 | null,
586 | null
587 | ]
588 | },
589 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/controllers/application_controller.rb": {
590 | "lines": [
591 | null,
592 | null,
593 | 1,
594 | 1,
595 | null,
596 | 1,
597 | 1,
598 | 1,
599 | null,
600 | null,
601 | 1,
602 | 3,
603 | 3,
604 | 3,
605 | null,
606 | 2,
607 | 2,
608 | 0,
609 | 0,
610 | null,
611 | 0,
612 | null,
613 | null,
614 | 1,
615 | null,
616 | null,
617 | null,
618 | 1,
619 | 2,
620 | null,
621 | null,
622 | 1,
623 | null,
624 | 1,
625 | 0,
626 | null,
627 | null,
628 | 1,
629 | 1,
630 | null,
631 | null
632 | ]
633 | },
634 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/policies/user_policy.rb": {
635 | "lines": [
636 | 1,
637 | 1,
638 | 2,
639 | null,
640 | null
641 | ]
642 | },
643 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/policies/application_policy.rb": {
644 | "lines": [
645 | null,
646 | null,
647 | 1,
648 | 1,
649 | null,
650 | 1,
651 | 2,
652 | 2,
653 | null,
654 | null,
655 | 1,
656 | 0,
657 | null,
658 | null,
659 | 1,
660 | 0,
661 | null,
662 | null,
663 | 1,
664 | 0,
665 | null,
666 | null,
667 | 1,
668 | 0,
669 | null,
670 | null,
671 | 1,
672 | 0,
673 | null,
674 | null,
675 | 1,
676 | 0,
677 | null,
678 | null,
679 | 1,
680 | 0,
681 | null,
682 | null,
683 | 1,
684 | 1,
685 | 0,
686 | 0,
687 | null,
688 | null,
689 | 1,
690 | 0,
691 | null,
692 | null,
693 | 1,
694 | null,
695 | 1,
696 | null,
697 | null
698 | ]
699 | },
700 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/interactors/user/add_to_system.rb": {
701 | "lines": [
702 | null,
703 | null,
704 | 1,
705 | 1,
706 | null,
707 | 1,
708 | null
709 | ]
710 | },
711 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/interactors/user/create.rb": {
712 | "lines": [
713 | null,
714 | null,
715 | 1,
716 | 1,
717 | null,
718 | 1,
719 | 2,
720 | null,
721 | null,
722 | null,
723 | 2,
724 | 2,
725 | 1,
726 | null,
727 | 1,
728 | null,
729 | null,
730 | null
731 | ]
732 | },
733 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/interactors/mail/verify_user.rb": {
734 | "lines": [
735 | null,
736 | null,
737 | 1,
738 | 1,
739 | 1,
740 | null,
741 | 1,
742 | 1,
743 | null,
744 | null,
745 | null
746 | ]
747 | },
748 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/jobs/user_mailer_job.rb": {
749 | "lines": [
750 | null,
751 | null,
752 | 1,
753 | 1,
754 | null,
755 | 1,
756 | 0,
757 | null,
758 | null
759 | ]
760 | },
761 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/jobs/application_job.rb": {
762 | "lines": [
763 | null,
764 | null,
765 | 1,
766 | null,
767 | null,
768 | null,
769 | null,
770 | null,
771 | null
772 | ]
773 | }
774 | },
775 | "timestamp": 1647704054
776 | },
777 | "Unit Tests": {
778 | "coverage": {
779 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/environment.rb": {
780 | "lines": [
781 | null,
782 | null,
783 | null,
784 | 1,
785 | null,
786 | null,
787 | 1
788 | ]
789 | },
790 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/application.rb": {
791 | "lines": [
792 | null,
793 | null,
794 | 1,
795 | null,
796 | 1,
797 | null,
798 | null,
799 | null,
800 | 1,
801 | null,
802 | 1,
803 | 1,
804 | null,
805 | 1,
806 | null,
807 | null,
808 | null,
809 | null,
810 | null,
811 | null,
812 | null,
813 | null,
814 | null,
815 | null,
816 | null,
817 | null,
818 | 1,
819 | 1,
820 | null,
821 | null
822 | ]
823 | },
824 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/environments/test.rb": {
825 | "lines": [
826 | null,
827 | null,
828 | 1,
829 | null,
830 | null,
831 | null,
832 | null,
833 | null,
834 | null,
835 | 1,
836 | null,
837 | null,
838 | null,
839 | 1,
840 | null,
841 | null,
842 | null,
843 | null,
844 | 1,
845 | null,
846 | null,
847 | 1,
848 | 1,
849 | null,
850 | null,
851 | null,
852 | null,
853 | 1,
854 | 1,
855 | 1,
856 | null,
857 | 1,
858 | 1,
859 | null,
860 | null,
861 | 1,
862 | null,
863 | null,
864 | 1,
865 | null,
866 | null,
867 | 1,
868 | null,
869 | 1,
870 | null,
871 | null,
872 | null,
873 | null,
874 | 1,
875 | null,
876 | null,
877 | 1,
878 | null,
879 | null,
880 | 1,
881 | null,
882 | null,
883 | 1,
884 | null,
885 | null,
886 | null,
887 | null,
888 | null,
889 | null,
890 | null
891 | ]
892 | },
893 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/cors.rb": {
894 | "lines": [
895 | null,
896 | null,
897 | null,
898 | 1,
899 | 1,
900 | 1,
901 | null,
902 | 1,
903 | null,
904 | null,
905 | null,
906 | null
907 | ]
908 | },
909 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/filter_parameter_logging.rb": {
910 | "lines": [
911 | null,
912 | null,
913 | null,
914 | null,
915 | null,
916 | null,
917 | null,
918 | 1,
919 | null,
920 | null
921 | ]
922 | },
923 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/generators.rb": {
924 | "lines": [
925 | null,
926 | null,
927 | 1,
928 | 1,
929 | null
930 | ]
931 | },
932 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/inflections.rb": {
933 | "lines": [
934 | null,
935 | null,
936 | null,
937 | null,
938 | null,
939 | null,
940 | null,
941 | null,
942 | null,
943 | null,
944 | null,
945 | null,
946 | null,
947 | null,
948 | null,
949 | null,
950 | null
951 | ]
952 | },
953 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/initializers/premailer_rails.rb": {
954 | "lines": [
955 | null,
956 | null,
957 | 1
958 | ]
959 | },
960 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/test/factories/users.rb": {
961 | "lines": [
962 | null,
963 | null,
964 | null,
965 | null,
966 | null,
967 | null,
968 | null,
969 | null,
970 | null,
971 | null,
972 | null,
973 | null,
974 | null,
975 | null,
976 | null,
977 | null,
978 | null,
979 | null,
980 | null,
981 | null,
982 | null,
983 | null,
984 | null,
985 | null,
986 | null,
987 | null,
988 | 1,
989 | 1,
990 | 12,
991 | 12,
992 | 12,
993 | 12,
994 | 9,
995 | 12,
996 | 9,
997 | null,
998 | 1,
999 | 4,
1000 | null,
1001 | null,
1002 | null
1003 | ]
1004 | },
1005 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/config/routes.rb": {
1006 | "lines": [
1007 | null,
1008 | null,
1009 | null,
1010 | null,
1011 | null,
1012 | 1,
1013 | 1,
1014 | 1,
1015 | 1,
1016 | 1,
1017 | null,
1018 | null,
1019 | null,
1020 | 1,
1021 | null
1022 | ]
1023 | },
1024 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/models/user.rb": {
1025 | "lines": [
1026 | null,
1027 | null,
1028 | null,
1029 | null,
1030 | null,
1031 | null,
1032 | null,
1033 | null,
1034 | null,
1035 | null,
1036 | null,
1037 | null,
1038 | null,
1039 | null,
1040 | null,
1041 | null,
1042 | null,
1043 | null,
1044 | null,
1045 | null,
1046 | null,
1047 | null,
1048 | null,
1049 | null,
1050 | null,
1051 | null,
1052 | 1,
1053 | 1,
1054 | 1,
1055 | null,
1056 | 1,
1057 | 1,
1058 | null,
1059 | 4,
1060 | null,
1061 | 1,
1062 | 0,
1063 | null,
1064 | null
1065 | ]
1066 | },
1067 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/models/application_record.rb": {
1068 | "lines": [
1069 | null,
1070 | null,
1071 | 1,
1072 | 1,
1073 | null,
1074 | 1,
1075 | 2,
1076 | null,
1077 | null
1078 | ]
1079 | },
1080 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/models/json_web_token.rb": {
1081 | "lines": [
1082 | null,
1083 | null,
1084 | 1,
1085 | 1,
1086 | null,
1087 | 1,
1088 | 4,
1089 | 4,
1090 | null,
1091 | null,
1092 | 1,
1093 | 3,
1094 | 3,
1095 | null,
1096 | null
1097 | ]
1098 | },
1099 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/controllers/api/v1/users_controller.rb": {
1100 | "lines": [
1101 | null,
1102 | null,
1103 | 1,
1104 | 1,
1105 | 1,
1106 | 1,
1107 | 1,
1108 | null,
1109 | null,
1110 | 1,
1111 | 2,
1112 | 2,
1113 | 1,
1114 | null,
1115 | 1,
1116 | null,
1117 | null,
1118 | null,
1119 | 1,
1120 | 3,
1121 | 2,
1122 | 1,
1123 | null,
1124 | 1,
1125 | null,
1126 | null,
1127 | null,
1128 | 1,
1129 | 3,
1130 | 3,
1131 | 1,
1132 | 1,
1133 | 1,
1134 | null,
1135 | null,
1136 | null,
1137 | null,
1138 | null,
1139 | 2,
1140 | null,
1141 | null,
1142 | null,
1143 | 1,
1144 | 2,
1145 | 2,
1146 | null,
1147 | 1,
1148 | 1,
1149 | null,
1150 | null,
1151 | 1,
1152 | null,
1153 | 1,
1154 | 3,
1155 | 3,
1156 | null,
1157 | null,
1158 | 1,
1159 | 4,
1160 | null,
1161 | null,
1162 | 1,
1163 | 5,
1164 | null,
1165 | null,
1166 | null,
1167 | null
1168 | ]
1169 | },
1170 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/controllers/application_controller.rb": {
1171 | "lines": [
1172 | null,
1173 | null,
1174 | 1,
1175 | 1,
1176 | null,
1177 | 1,
1178 | 1,
1179 | 1,
1180 | null,
1181 | null,
1182 | 1,
1183 | 4,
1184 | 4,
1185 | 4,
1186 | null,
1187 | 3,
1188 | 3,
1189 | 0,
1190 | 0,
1191 | null,
1192 | 0,
1193 | null,
1194 | null,
1195 | 1,
1196 | null,
1197 | null,
1198 | null,
1199 | 1,
1200 | 3,
1201 | null,
1202 | null,
1203 | 1,
1204 | null,
1205 | 1,
1206 | 0,
1207 | null,
1208 | null,
1209 | 1,
1210 | 1,
1211 | null,
1212 | null
1213 | ]
1214 | },
1215 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/policies/user_policy.rb": {
1216 | "lines": [
1217 | 1,
1218 | 1,
1219 | 3,
1220 | null,
1221 | null
1222 | ]
1223 | },
1224 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/policies/application_policy.rb": {
1225 | "lines": [
1226 | null,
1227 | null,
1228 | 1,
1229 | 1,
1230 | null,
1231 | 1,
1232 | 3,
1233 | 3,
1234 | null,
1235 | null,
1236 | 1,
1237 | 0,
1238 | null,
1239 | null,
1240 | 1,
1241 | 0,
1242 | null,
1243 | null,
1244 | 1,
1245 | 0,
1246 | null,
1247 | null,
1248 | 1,
1249 | 0,
1250 | null,
1251 | null,
1252 | 1,
1253 | 0,
1254 | null,
1255 | null,
1256 | 1,
1257 | 0,
1258 | null,
1259 | null,
1260 | 1,
1261 | 0,
1262 | null,
1263 | null,
1264 | 1,
1265 | 1,
1266 | 0,
1267 | 0,
1268 | null,
1269 | null,
1270 | 1,
1271 | 0,
1272 | null,
1273 | null,
1274 | 1,
1275 | null,
1276 | 1,
1277 | null,
1278 | null
1279 | ]
1280 | },
1281 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/interactors/user/add_to_system.rb": {
1282 | "lines": [
1283 | null,
1284 | null,
1285 | 1,
1286 | 1,
1287 | null,
1288 | 1,
1289 | null
1290 | ]
1291 | },
1292 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/interactors/user/create.rb": {
1293 | "lines": [
1294 | null,
1295 | null,
1296 | 1,
1297 | 1,
1298 | null,
1299 | 1,
1300 | 2,
1301 | null,
1302 | null,
1303 | null,
1304 | 2,
1305 | 2,
1306 | 1,
1307 | null,
1308 | 1,
1309 | null,
1310 | null,
1311 | null
1312 | ]
1313 | },
1314 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/interactors/mail/verify_user.rb": {
1315 | "lines": [
1316 | null,
1317 | null,
1318 | 1,
1319 | 1,
1320 | 1,
1321 | null,
1322 | 1,
1323 | 1,
1324 | null,
1325 | null,
1326 | null
1327 | ]
1328 | },
1329 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/jobs/user_mailer_job.rb": {
1330 | "lines": [
1331 | null,
1332 | null,
1333 | 1,
1334 | 1,
1335 | null,
1336 | 1,
1337 | 0,
1338 | null,
1339 | null
1340 | ]
1341 | },
1342 | "/Users/akhilgautam/projects/personal/nextjs-on-rails/app/jobs/application_job.rb": {
1343 | "lines": [
1344 | null,
1345 | null,
1346 | 1,
1347 | null,
1348 | null,
1349 | null,
1350 | null,
1351 | null,
1352 | null
1353 | ]
1354 | }
1355 | },
1356 | "timestamp": 1647704264
1357 | }
1358 | }
1359 |
--------------------------------------------------------------------------------
/coverage/assets/0.12.3/application.css:
--------------------------------------------------------------------------------
1 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,code,del,dfn,em,img,q,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,dialog,figure,footer,header,hgroup,nav,section{margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline}article,aside,dialog,figure,footer,header,hgroup,nav,section{display:block}body{line-height:1.5}table{border-collapse:separate;border-spacing:0}caption,th,td{text-align:left;font-weight:normal}table,td,th{vertical-align:middle}blockquote:before,blockquote:after,q:before,q:after{content:""}blockquote,q{quotes:"" ""}a img{border:0}html{font-size:100.01%}body{font-size:82%;color:#222;background:#fff;font-family:"Helvetica Neue",Arial,Helvetica,sans-serif}h1,h2,h3,h4,h5,h6{font-weight:normal;color:#111}h1{font-size:3em;line-height:1;margin-bottom:.5em}h2{font-size:2em;margin-bottom:.75em}h3{font-size:1.5em;line-height:1;margin-bottom:1em}h4{font-size:1.2em;line-height:1.25;margin-bottom:1.25em}h5{font-size:1em;font-weight:bold;margin-bottom:1.5em}h6{font-size:1em;font-weight:bold}h1 img,h2 img,h3 img,h4 img,h5 img,h6 img{margin:0}p{margin:0 0 1.5em}p img.left{float:left;margin:1.5em 1.5em 1.5em 0;padding:0}p img.right{float:right;margin:1.5em 0 1.5em 1.5em}a:focus,a:hover{color:#000}a{color:#009;text-decoration:underline}blockquote{margin:1.5em;color:#666;font-style:italic}strong{font-weight:bold}em,dfn{font-style:italic}dfn{font-weight:bold}sup,sub{line-height:0}abbr,acronym{border-bottom:1px dotted #666}address{margin:0 0 1.5em;font-style:italic}del{color:#666}pre{margin:1.5em 0;white-space:pre}pre,code,tt{font:1em 'andale mono','lucida console',monospace;line-height:1.5}li ul,li ol{margin:0}ul,ol{margin:0 1.5em 1.5em 0;padding-left:3.333em}ul{list-style-type:disc}ol{list-style-type:decimal}dl{margin:0 0 1.5em 0}dl dt{font-weight:bold}dd{margin-left:1.5em}table{margin-bottom:1.4em;width:100%}th{font-weight:bold}thead th{background:#c3d9ff}th,td,caption{padding:4px 10px 4px 5px}tr.even td{background:#efefef}tfoot{font-style:italic}caption{background:#eee}.small{font-size:.8em;margin-bottom:1.875em;line-height:1.875em}.large{font-size:1.2em;line-height:2.5em;margin-bottom:1.25em}.hide{display:none}.quiet{color:#666}.loud{color:#000}.highlight{background:#ff0}.added{background:#060;color:#fff}.removed{background:#900;color:#fff}.first{margin-left:0;padding-left:0}.last{margin-right:0;padding-right:0}.top{margin-top:0;padding-top:0}.bottom{margin-bottom:0;padding-bottom:0}label{font-weight:bold}fieldset{padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc}legend{font-weight:bold;font-size:1.2em}input[type=text],input[type=password],input.text,input.title,textarea,select{background-color:#fff;border:1px solid #bbb}input[type=text]:focus,input[type=password]:focus,input.text:focus,input.title:focus,textarea:focus,select:focus{border-color:#666}input[type=text],input[type=password],input.text,input.title,textarea,select{margin:.5em 0}input.text,input.title{width:300px;padding:5px}input.title{font-size:1.5em}textarea{width:390px;height:250px;padding:5px}input[type=checkbox],input[type=radio],input.checkbox,input.radio{position:relative;top:.25em}form.inline{line-height:3}form.inline p{margin-bottom:0}.error,.notice,.success{padding:.8em;margin-bottom:1em;border:2px solid #ddd}.error{background:#fbe3e4;color:#8a1f11;border-color:#fbc2c4}.notice{background:#fff6bf;color:#514721;border-color:#ffd324}.success{background:#e6efc2;color:#264409;border-color:#c6d880}.error a{color:#8a1f11}.notice a{color:#514721}.success a{color:#264409}.box{padding:1.5em;margin-bottom:1.5em;background:#e5ecf9}hr{background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:0}hr.space{background:#fff;color:#fff;visibility:hidden}.clearfix:after,.container:after{content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden}.clearfix,.container{display:block}.clear{clear:both}table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:0}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{cursor:pointer;*cursor:hand;background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("DataTables-1.10.20/images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("DataTables-1.10.20/images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("DataTables-1.10.20/images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("DataTables-1.10.20/images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("DataTables-1.10.20/images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#fff}table.dataTable tbody tr.selected{background-color:#b0bed9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:0}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:0}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear,left top,left bottom,color-stop(0,white),color-stop(100%,#dcdcdc));background:-webkit-linear-gradient(top,white 0,#dcdcdc 100%);background:-moz-linear-gradient(top,white 0,#dcdcdc 100%);background:-ms-linear-gradient(top,white 0,#dcdcdc 100%);background:-o-linear-gradient(top,white 0,#dcdcdc 100%);background:linear-gradient(to bottom,white 0,#dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#585858),color-stop(100%,#111));background:-webkit-linear-gradient(top,#585858 0,#111 100%);background:-moz-linear-gradient(top,#585858 0,#111 100%);background:-ms-linear-gradient(top,#585858 0,#111 100%);background:-o-linear-gradient(top,#585858 0,#111 100%);background:linear-gradient(to bottom,#585858 0,#111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:0;background-color:#2b2b2b;background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#2b2b2b),color-stop(100%,#0c0c0c));background:-webkit-linear-gradient(top,#2b2b2b 0,#0c0c0c 100%);background:-moz-linear-gradient(top,#2b2b2b 0,#0c0c0c 100%);background:-ms-linear-gradient(top,#2b2b2b 0,#0c0c0c 100%);background:-o-linear-gradient(top,#2b2b2b 0,#0c0c0c 100%);background:linear-gradient(to bottom,#2b2b2b 0,#0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(25%,rgba(255,255,255,0.9)),color-stop(75%,rgba(255,255,255,0.9)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>thead>tr>td>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody>table>tbody>tr>td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable,.dataTables_wrapper.no-footer div.dataTables_scrollBody>table{border-bottom:0}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width:767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:.5em}}@media screen and (max-width:640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:.5em}}pre .comment,pre .template_comment,pre .diff .header,pre .javadoc{color:#998;font-style:italic}pre .keyword,pre .css .rule .keyword,pre .winutils,pre .javascript .title,pre .lisp .title{color:#000;font-weight:bold}pre .number,pre .hexcolor{color:#458}pre .string,pre .tag .value,pre .phpdoc,pre .tex .formula{color:#d14}pre .subst{color:#712}pre .constant,pre .title,pre .id{color:#900;font-weight:bold}pre .javascript .title,pre .lisp .title,pre .subst{font-weight:normal}pre .class .title,pre .haskell .label,pre .tex .command{color:#458;font-weight:bold}pre .tag,pre .tag .title,pre .rules .property,pre .django .tag .keyword{color:navy;font-weight:normal}pre .attribute,pre .variable,pre .instancevar,pre .lisp .body{color:teal}pre .regexp{color:#009926}pre .class{color:#458;font-weight:bold}pre .symbol,pre .ruby .symbol .string,pre .ruby .symbol .keyword,pre .ruby .symbol .keymethods,pre .lisp .keyword,pre .tex .special,pre .input_number{color:#990073}pre .builtin,pre .built_in,pre .lisp .title{color:#0086b3}pre .preprocessor,pre .pi,pre .doctype,pre .shebang,pre .cdata{color:#999;font-weight:bold}pre .deletion{background:#fdd}pre .addition{background:#dfd}pre .diff .change{background:#0086b3}pre .chunk{color:#aaa}pre .tex .formula{opacity:.5}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{position:absolute;left:-99999999px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden}.ui-helper-clearfix{display:inline-block}/*\*/* html .ui-helper-clearfix{height:1%}.ui-helper-clearfix{display:block}/**/.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-state-disabled{cursor:default !important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:absolute;top:0;left:0;width:100%;height:100%}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-widget :active{outline:0}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-icon{width:16px;height:16px;background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-off{background-position:-96px -144px}.ui-icon-radio-on{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-tl{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px}.ui-corner-tr{-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;border-top-right-radius:4px}.ui-corner-bl{-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.ui-corner-br{-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-corner-top{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;border-top-right-radius:4px}.ui-corner-bottom{-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-corner-right{-moz-border-radius-topright:4px;-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-bottomright:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px}.ui-corner-left{-moz-border-radius-topleft:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.ui-corner-all{-moz-border-radius:4px;-webkit-border-radius:4px;border-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.30;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.30;filter:Alpha(Opacity=30);-moz-border-radius:8px;-webkit-border-radius:8px;border-radius:8px}#colorbox,#cboxOverlay,#cboxWrapper{position:absolute;top:0;left:0;z-index:9999;overflow:hidden}#cboxOverlay{position:fixed;width:100%;height:100%}#cboxMiddleLeft,#cboxBottomLeft{clear:left}#cboxContent{position:relative}#cboxLoadedContent{overflow:auto}#cboxTitle{margin:0}#cboxLoadingOverlay,#cboxLoadingGraphic{position:absolute;top:0;left:0;width:100%;height:100%}#cboxPrevious,#cboxNext,#cboxClose,#cboxSlideshow{cursor:pointer}.cboxPhoto{float:left;margin:auto;border:0;display:block;max-width:none}.cboxIframe{width:100%;height:100%;display:block;border:0}#colorbox,#cboxContent,#cboxLoadedContent{box-sizing:content-box}#cboxOverlay{background:#000}#cboxTopLeft{width:14px;height:14px;background:url(colorbox/controls.png) no-repeat 0 0}#cboxTopCenter{height:14px;background:url(colorbox/border.png) repeat-x top left}#cboxTopRight{width:14px;height:14px;background:url(colorbox/controls.png) no-repeat -36px 0}#cboxBottomLeft{width:14px;height:43px;background:url(colorbox/controls.png) no-repeat 0 -32px}#cboxBottomCenter{height:43px;background:url(colorbox/border.png) repeat-x bottom left}#cboxBottomRight{width:14px;height:43px;background:url(colorbox/controls.png) no-repeat -36px -32px}#cboxMiddleLeft{width:14px;background:url(colorbox/controls.png) repeat-y -175px 0}#cboxMiddleRight{width:14px;background:url(colorbox/controls.png) repeat-y -211px 0}#cboxContent{background:#fff;overflow:visible}.cboxIframe{background:#fff}#cboxError{padding:50px;border:1px solid #ccc}#cboxLoadedContent{margin-bottom:5px}#cboxLoadingOverlay{background:url(colorbox/loading_background.png) no-repeat center center}#cboxLoadingGraphic{background:url(colorbox/loading.gif) no-repeat center center}#cboxTitle{position:absolute;bottom:-25px;left:0;text-align:center;width:100%;font-weight:bold;color:#7c7c7c}#cboxCurrent{position:absolute;bottom:-25px;left:58px;font-weight:bold;color:#7c7c7c}#cboxPrevious,#cboxNext,#cboxClose,#cboxSlideshow{position:absolute;bottom:-29px;background:url(colorbox/controls.png) no-repeat 0 0;width:23px;height:23px;text-indent:-9999px}#cboxPrevious{left:0;background-position:-51px -25px}#cboxPrevious:hover{background-position:-51px 0}#cboxNext{left:27px;background-position:-75px -25px}#cboxNext:hover{background-position:-75px 0}#cboxClose{right:0;background-position:-100px -25px}#cboxClose:hover{background-position:-100px 0}.cboxSlideshow_on #cboxSlideshow{background-position:-125px 0;right:27px}.cboxSlideshow_on #cboxSlideshow:hover{background-position:-150px 0}.cboxSlideshow_off #cboxSlideshow{background-position:-150px -25px;right:27px}.cboxSlideshow_off #cboxSlideshow:hover{background-position:-125px 0}#loading{position:fixed;left:40%;top:50%}a{color:#333;text-decoration:none}a:hover{color:#000;text-decoration:underline}body{font-family:"Lucida Grande",Helvetica,"Helvetica Neue",Arial,sans-serif;padding:12px;background-color:#333}h1,h2,h3,h4{color:#1c2324;margin:0;padding:0;margin-bottom:12px}table{width:100%}#content{clear:left;background-color:white;border:2px solid #ddd;border-top:8px solid #ddd;padding:18px;-webkit-border-bottom-left-radius:5px;-webkit-border-bottom-right-radius:5px;-webkit-border-top-right-radius:5px;-moz-border-radius-bottomleft:5px;-moz-border-radius-bottomright:5px;-moz-border-radius-topright:5px;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top-right-radius:5px}.dataTables_filter,.dataTables_info{padding:2px 6px}abbr.timeago{text-decoration:none;border:0;font-weight:bold}.timestamp{float:right;color:#ddd}.group_tabs{list-style:none;float:left;margin:0;padding:0}.group_tabs li{display:inline;float:left}.group_tabs li a{font-family:Helvetica,Arial,sans-serif;display:block;float:left;text-decoration:none;padding:4px 8px;background-color:#aaa;background:-webkit-gradient(linear,0 0,0 bottom,from(#ddd),to(#aaa));background:-moz-linear-gradient(#ddd,#aaa);background:linear-gradient(#ddd,#aaa);text-shadow:#e5e5e5 1px 1px 0;border-bottom:0;color:#333;font-weight:bold;margin-right:8px;border-top:1px solid #efefef;-webkit-border-top-left-radius:2px;-webkit-border-top-right-radius:2px;-moz-border-radius-topleft:2px;-moz-border-radius-topright:2px;border-top-left-radius:2px;border-top-right-radius:2px}.group_tabs li a:hover{background-color:#ccc;background:-webkit-gradient(linear,0 0,0 bottom,from(#eee),to(#aaa));background:-moz-linear-gradient(#eee,#aaa);background:linear-gradient(#eee,#aaa)}.group_tabs li a:active{padding-top:5px;padding-bottom:3px}.group_tabs li.active a{color:black;text-shadow:#fff 1px 1px 0;background-color:#ddd;background:-webkit-gradient(linear,0 0,0 bottom,from(white),to(#ddd));background:-moz-linear-gradient(white,#ddd);background:linear-gradient(white,#ddd)}.file_list{margin-bottom:18px}.file_list--responsive{overflow-x:auto;overflow-y:hidden}a.src_link{background:url("./magnify.png") no-repeat left 50%;padding-left:18px}tr,td{margin:0;padding:0}th{white-space:nowrap}th.ui-state-default{cursor:pointer}th span.ui-icon{float:left}td{padding:4px 8px}td.strong{font-weight:bold}.cell--number{text-align:right}.source_table h3,.source_table h4{padding:0;margin:0;margin-bottom:4px}.source_table .header{padding:10px}.source_table pre{margin:0;padding:0;white-space:normal;color:#000;font-family:"Monaco","Inconsolata","Consolas",monospace}.source_table code{color:#000;font-family:"Monaco","Inconsolata","Consolas",monospace}.source_table pre{background-color:#333}.source_table pre ol{margin:0;padding:0;margin-left:45px;font-size:12px;color:white}.source_table pre li{margin:0;padding:2px 6px;border-left:5px solid white}.source_table pre li code{white-space:pre;white-space:pre-wrap}.source_table pre .hits{float:right;margin-left:10px;padding:2px 4px;background-color:#444;background:-webkit-gradient(linear,0 0,0 bottom,from(#222),to(#666));background:-moz-linear-gradient(#222,#666);background:linear-gradient(#222,#666);color:white;font-family:Helvetica,"Helvetica Neue",Arial,sans-serif;font-size:10px;font-weight:bold;text-align:center;border-radius:6px}#footer{color:#ddd;font-size:12px;font-weight:bold;margin-top:12px;text-align:right}#footer a{color:#eee;text-decoration:underline}#footer a:hover{color:#fff;text-decoration:none}.green{color:#090}.red{color:#900}.yellow{color:#da0}.blue{color:blue}thead th{background:white}.source_table .covered{border-color:#090}.source_table .missed{border-color:#900}.source_table .never{border-color:black}.source_table .skipped{border-color:#fc0}.source_table .missed-branch{border-color:#bf0000}.source_table .covered:nth-child(odd){background-color:#cdf2cd}.source_table .covered:nth-child(even){background-color:#dbf2db}.source_table .missed:nth-child(odd){background-color:#f7c0c0}.source_table .missed:nth-child(even){background-color:#f7cfcf}.source_table .never:nth-child(odd){background-color:#efefef}.source_table .never:nth-child(even){background-color:#f4f4f4}.source_table .skipped:nth-child(odd){background-color:#fbf0c0}.source_table .skipped:nth-child(even){background-color:#fbffcf}.source_table .missed-branch:nth-child(odd){background-color:#cc8e8e}.source_table .missed-branch:nth-child(even){background-color:#cc6e6e}
--------------------------------------------------------------------------------