├── .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 |
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 | 10 | 16 | 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 | 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 | 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 |
22 |

23 | 24 | NXR 25 | 26 |

27 | 28 | 46 |
47 | {user?.token?.length ? ( 48 | 49 | ) : ( 50 | 51 | 52 | 53 | 54 | 55 | )} 56 |
57 |
58 | {showMenu && ( 59 | 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 |
41 |
45 |
Sign up
46 | 53 | 60 | 70 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
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 |
54 |
58 |
Sign in
59 | 69 | 78 | 79 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Next.js + Ruby on Rails API 2 | 3 | ![image](https://user-images.githubusercontent.com/28865023/159129958-04d9bf87-5b5c-4ad7-a8e8-628ff418c3e9.png) 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 | Screenshot 2022-03-30 at 11 52 33 PM 71 | 72 |

73 | 74 | 75 |

76 | Screenshot 2022-03-30 at 11 52 46 PM 77 | 78 |

79 | 80 |

81 | Screenshot 2022-03-30 at 11 52 58 PM 82 | 83 |

84 | 85 |

86 | Screenshot 2022-03-30 at 11 53 46 PM 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 |
17 |

18 | Welcome to Next.js on{' '} 19 | Rails 20 |

21 |

22 | UI components available for use in your Next.js application 23 |

24 |
25 |
26 |

27 | Buttons 28 |

29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 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 | 57 |
58 |
59 |

Input

60 |
61 | 62 | 63 | 68 | 69 |
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} --------------------------------------------------------------------------------