├── log
└── .keep
├── tmp
└── .keep
├── vendor
└── .keep
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── public
├── favicon.ico
├── apple-touch-icon.png
├── apple-touch-icon-precomposed.png
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── test
├── helpers
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ └── todo_test.rb
├── system
│ ├── .keep
│ └── todos_test.rb
├── controllers
│ ├── .keep
│ └── todos_controller_test.rb
├── fixtures
│ ├── .keep
│ ├── files
│ │ └── .keep
│ └── todos.yml
├── integration
│ └── .keep
├── application_system_test_case.rb
└── test_helper.rb
├── app
├── assets
│ ├── images
│ │ ├── .keep
│ │ ├── square-o.svg
│ │ ├── check-square-o.svg
│ │ ├── rails.svg
│ │ └── react.svg
│ ├── packs
│ │ ├── todos.ts
│ │ └── app.ts
│ ├── javascripts
│ │ ├── todos
│ │ │ ├── components
│ │ │ │ ├── App
│ │ │ │ │ ├── styles.module.scss
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── render.tsx
│ │ │ │ ├── TodoList
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── render.tsx
│ │ │ │ ├── EditButton
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── render.tsx
│ │ │ │ ├── __test__
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ ├── TodoAddForm.spec.tsx
│ │ │ │ │ └── TodoItem.spec.tsx
│ │ │ │ ├── TodoItem
│ │ │ │ │ ├── styles.module.scss
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── render.tsx
│ │ │ │ ├── __stories__
│ │ │ │ │ ├── EditButton.stories.tsx
│ │ │ │ │ ├── TodoConditions.stories.tsx
│ │ │ │ │ ├── TodoAddForm.stories.tsx
│ │ │ │ │ └── TodoItem.stories.tsx
│ │ │ │ ├── TodoAddForm
│ │ │ │ │ ├── styles.module.scss
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── render.tsx
│ │ │ │ └── TodoConditions
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── render.tsx
│ │ │ ├── reducers
│ │ │ │ ├── index.ts
│ │ │ │ ├── __test__
│ │ │ │ │ ├── app.spec.ts
│ │ │ │ │ └── todo.spec.ts
│ │ │ │ ├── todos.ts
│ │ │ │ └── app.ts
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ ├── store
│ │ │ │ └── index.ts
│ │ │ ├── index.tsx
│ │ │ ├── webApi
│ │ │ │ └── index.ts
│ │ │ ├── sagas
│ │ │ │ └── index.ts
│ │ │ └── actions
│ │ │ │ └── index.ts
│ │ ├── application.ts
│ │ ├── types
│ │ │ ├── rails-ujs.d.ts
│ │ │ ├── ignore-styles.d.ts
│ │ │ └── assets.d.ts
│ │ ├── lib
│ │ │ └── case-util.ts
│ │ └── change-query.ts
│ ├── config
│ │ └── manifest.js
│ └── stylesheets
│ │ ├── todos.scss
│ │ ├── application.scss
│ │ ├── scaffold.scss
│ │ └── react-datetime.scss
├── models
│ ├── concerns
│ │ └── .keep
│ ├── todo.rb
│ └── application_record.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── pages_controller.rb
│ ├── application_controller.rb
│ ├── react_todos_controller.rb
│ └── todos_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ ├── mailer.html.erb
│ │ └── application.html.erb
│ ├── todos
│ │ ├── show.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ ├── _todo.json.jbuilder
│ │ ├── new.html.erb
│ │ ├── edit.html.erb
│ │ ├── show.html.erb
│ │ ├── _form.html.erb
│ │ └── index.html.erb
│ ├── pages
│ │ └── home.html.erb
│ └── react_todos
│ │ └── index.html.erb
├── helpers
│ ├── todos_helper.rb
│ └── application_helper.rb
├── jobs
│ └── application_job.rb
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
└── mailers
│ └── application_mailer.rb
├── .postcssrc.yml
├── .rubocop.yml
├── .editorconfig
├── Procfile
├── bin
├── rake
├── bundle
├── rails
├── yarn
├── webpack
├── webpack-dev-server
├── spring
├── update
└── setup
├── config
├── webpack
│ ├── test.js
│ ├── production.js
│ ├── development.js
│ └── environment.js
├── spring.rb
├── routes.rb
├── environment.rb
├── initializers
│ ├── mime_types.rb
│ ├── filter_parameter_logging.rb
│ ├── application_controller_renderer.rb
│ ├── cookies_serializer.rb
│ ├── backtrace_silencers.rb
│ ├── wrap_parameters.rb
│ ├── inflections.rb
│ ├── content_security_policy.rb
│ └── new_framework_defaults_5_2.rb
├── boot.rb
├── cable.yml
├── database.yml
├── locales
│ └── en.yml
├── application.rb
├── storage.yml
├── espower-typescript.js
├── secrets.yml
├── puma.rb
├── webpacker.yml
└── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
├── tsconfig.test.json
├── config.ru
├── Rakefile
├── .storybook
├── config.js
└── webpack.config.js
├── db
├── migrate
│ └── 20170617154903_create_todos.rb
├── seeds.rb
└── schema.rb
├── tsconfig.json
├── .babelrc
├── tslint.json
├── .gitignore
├── Gemfile
├── README.md
├── package.json
└── Gemfile.lock
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.postcssrc.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | autoprefixer: {}
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | Documentation:
2 | Enabled: false
3 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/assets/packs/todos.ts:
--------------------------------------------------------------------------------
1 | import 'javascripts/todos'
2 |
--------------------------------------------------------------------------------
/app/helpers/todos_helper.rb:
--------------------------------------------------------------------------------
1 | module TodosHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/todo.rb:
--------------------------------------------------------------------------------
1 | class Todo < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | indent_size = 2
4 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/views/todos/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'todos/todo', todo: @todo
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: PORT=3000 bundle exec rails s
2 | webpacker: ./bin/webpack-dev-server
3 |
--------------------------------------------------------------------------------
/app/views/todos/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.array! @todos, partial: 'todos/todo', as: :todo
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/App/styles.module.scss:
--------------------------------------------------------------------------------
1 | .logo {
2 | width: 100px;
3 | }
4 |
--------------------------------------------------------------------------------
/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 | def home; end
3 | end
4 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoList/index.ts:
--------------------------------------------------------------------------------
1 | import TodoList from './render'
2 | export default TodoList
3 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/EditButton/index.ts:
--------------------------------------------------------------------------------
1 | import EditButton from './render'
2 | export default EditButton
3 |
--------------------------------------------------------------------------------
/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | const environment = require('./environment')
2 |
3 | module.exports = environment.toWebpackConfig()
4 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | const environment = require('./environment')
2 |
3 | module.exports = environment.toWebpackConfig()
4 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | const environment = require('./environment')
2 |
3 | module.exports = environment.toWebpackConfig()
4 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | // = link_tree ../images
2 | // = link_directory ../javascripts .js
3 | // = link_directory ../stylesheets .css
4 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 | end
4 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w[
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ].each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/app/assets/packs/app.ts:
--------------------------------------------------------------------------------
1 | import 'javascripts/application'
2 | import 'stylesheets/application'
3 | require.context('images', true, /\.(png|jpg|jpeg|svg)$/)
4 |
--------------------------------------------------------------------------------
/app/controllers/react_todos_controller.rb:
--------------------------------------------------------------------------------
1 | class ReactTodosController < ApplicationController
2 | def index
3 | @todos = Todo.all
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/todos/_todo.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! todo, :id, :content, :due_date, :done, :created_at, :updated_at
2 | json.url todo_url(todo, format: :json)
3 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root to: 'pages#home'
3 | resources :todos
4 | get 'react/todos', to: 'react_todos#index'
5 | end
6 |
--------------------------------------------------------------------------------
/app/assets/javascripts/application.ts:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import 'javascripts/change-query'
3 | import * as Rails from 'rails-ujs'
4 |
5 | Rails.start()
6 |
--------------------------------------------------------------------------------
/app/views/todos/new.html.erb:
--------------------------------------------------------------------------------
1 |
New Todo
2 |
3 | <%= render 'form', todo: @todo %>
4 |
5 | <%= link_to 'Back', todos_path, class: 'btn btn-secondary' %>
6 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../config/application', __dir__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/models/todo_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TodoTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/todos.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Place all the styles related to the matching controller here.
3 | They will automatically be included in application.css.
4 | */
5 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__test__/setup.ts:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme'
2 | import * as Adapter from 'enzyme-adapter-react-16'
3 |
4 | configure({ adapter: new Adapter() })
5 |
--------------------------------------------------------------------------------
/app/assets/javascripts/types/rails-ujs.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'rails-ujs' {
2 | const Rails: {
3 | start(): void
4 | csrfToken(): string | undefined
5 | }
6 |
7 | export = Rails
8 | }
9 |
--------------------------------------------------------------------------------
/app/views/pages/home.html.erb:
--------------------------------------------------------------------------------
1 |
2 | - <%= link_to 'Modern Todo App(React Redux)', react_todos_path %>
3 | - <%= link_to 'Classical Todo App(Rails MVC)', todos_path %>
4 |
5 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/todos/edit.html.erb:
--------------------------------------------------------------------------------
1 | Editing Todo
2 |
3 | <%= render 'form', todo: @todo %>
4 |
5 | <%= link_to 'Show', @todo, class: 'btn btn-secondary' %> |
6 | <%= link_to 'Back', todos_path, class: 'btn btn-secondary' %>
7 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/system/todos_test.rb:
--------------------------------------------------------------------------------
1 | require 'application_system_test_case'
2 |
3 | class TodosTest < ApplicationSystemTestCase
4 | # test "visiting the index" do
5 | # visit todos_url
6 | #
7 | # assert_selector "h1", text: "Todo"
8 | # end
9 | end
10 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: webpacker_react_example_production
11 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/test/fixtures/todos.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | content: MyString
5 | due_date: 2017-06-18 00:49:03
6 | done: false
7 |
8 | two:
9 | content: MyString
10 | due_date: 2017-06-18 00:49:03
11 | done: false
12 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react'
2 | import 'stylesheets/application.scss'
3 |
4 | const req = require.context('javascripts', true, /\.stories\.tsx$/)
5 |
6 | function loadStories() {
7 | req.keys().forEach((filename) => req(filename))
8 | }
9 |
10 | configure(loadStories, module)
11 |
--------------------------------------------------------------------------------
/app/views/react_todos/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :head do %>
2 | <%= javascript_pack_tag 'todos' %>
3 | <%= stylesheet_pack_tag 'todos' %>
4 | <% end %>
5 |
6 | <%= content_tag :div,
7 | id: 'todos-data',
8 | data: {
9 | todos: @todos
10 | }.to_json do %>
11 | <% end %>
12 |
13 |
--------------------------------------------------------------------------------
/db/migrate/20170617154903_create_todos.rb:
--------------------------------------------------------------------------------
1 | class CreateTodos < ActiveRecord::Migration[5.1]
2 | def change
3 | create_table :todos do |t|
4 | t.string :content, null: false
5 | t.datetime :due_date, null: false
6 | t.boolean :done, null: false
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoItem/styles.module.scss:
--------------------------------------------------------------------------------
1 | .contentCol {
2 | width: 20em;
3 | }
4 |
5 | .contentLabel {
6 | display: inline;
7 | font-weight: normal;
8 | }
9 |
10 | .editButton {
11 | margin-left: 1rem;
12 | }
13 |
14 | .dueDateCol {
15 | width: 25em;
16 | }
17 |
18 | .error {
19 | color: red;
20 | }
21 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_ROOT = File.expand_path('..', __dir__)
3 | Dir.chdir(APP_ROOT) do
4 | begin
5 | exec "yarnpkg", *ARGV
6 | rescue Errno::ENOENT
7 | $stderr.puts "Yarn executable was not detected in the system."
8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
9 | exit 1
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | @import '~bootstrap/scss/bootstrap';
2 | @import '~@fortawesome/fontawesome-free/scss/fontawesome';
3 | @import '~@fortawesome/fontawesome-free/scss/regular';
4 | @import '~stylesheets/scaffold';
5 | @import '~stylesheets/react-datetime';
6 |
7 | .check {
8 | width: 1em;
9 | }
10 |
11 | .logo {
12 | width: 100px;
13 | }
14 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../config/environment', __dir__)
2 | require 'rails/test_help'
3 |
4 | module ActiveSupport
5 | class TestCase
6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical
7 | # order.
8 | fixtures :all
9 |
10 | # Add more helper methods to be used by all tests here...
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebpackerReactExample
5 | <%= csrf_meta_tags %>
6 |
7 | <%= javascript_pack_tag 'app' %>
8 | <%= stylesheet_pack_tag 'app' %>
9 | <%= yield :head %>
10 |
11 |
12 |
13 |
14 | <%= yield %>
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/assets/images/square-o.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers, Reducer } from 'redux'
2 | import app, { AppState } from './app'
3 | import todos, { TodosState } from './todos'
4 |
5 | export interface StoreState {
6 | readonly todos: TodosState
7 | readonly app: AppState
8 | }
9 |
10 | export * from './todos'
11 | export * from './app'
12 | export default combineReducers({ todos, app })
13 |
--------------------------------------------------------------------------------
/bin/webpack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "rubygems"
11 | require "bundler/setup"
12 |
13 | require "webpacker"
14 | require "webpacker/webpack_runner"
15 | Webpacker::WebpackRunner.run(ARGV)
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__stories__/EditButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { action } from '@storybook/addon-actions'
2 | import { storiesOf } from '@storybook/react'
3 | import * as React from 'react'
4 | import EditButton from '../EditButton/render'
5 |
6 | storiesOf('EditButton', module)
7 | .add('typical', () => )
8 | .add('disabled', () => )
9 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database
2 | # with its default values. The data can then be loaded with the rails db:seed
3 | # command (or created alongside the database with db:setup).
4 | #
5 | # Examples:
6 | #
7 | # movies = Movie.create([
8 | # { name: 'Star Wars' },
9 | # { name: 'Lord of the Rings' }
10 | # ])
11 | # Character.create(name: 'Luke', movie: movies.first)
12 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoAddForm/styles.module.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | font-weight: normal;
3 | }
4 |
5 | .dueDate {
6 | display: inline-block;
7 | font-weight: normal;
8 |
9 | :global(.form-control) {
10 | line-height: inherit;
11 | height: inherit;
12 | padding: 2px;
13 | border-radius: 0;
14 | }
15 | }
16 |
17 | .item {
18 | margin: 0 0.5rem;
19 | }
20 |
21 | .error {
22 | color: red;
23 | }
24 |
--------------------------------------------------------------------------------
/bin/webpack-dev-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "rubygems"
11 | require "bundler/setup"
12 |
13 | require "webpacker"
14 | require "webpacker/dev_server_runner"
15 | Webpacker::DevServerRunner.run(ARGV)
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "noImplicitAny": true,
5 | "strictNullChecks": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "jsx": "preserve"
9 | },
10 | "include": [
11 | "./app/assets/javascripts/**/*",
12 | "./app/assets/packs/**/*"
13 | ],
14 | "awesomeTypescriptLoaderOptions": {
15 | "useBabel": true,
16 | "useCache": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/App/index.ts:
--------------------------------------------------------------------------------
1 | import { flowRight } from 'lodash'
2 | import { hot } from 'react-hot-loader'
3 | import { connect } from 'react-redux'
4 | import { StoreState, visibleTodos } from '../../reducers'
5 | import App, { Props } from './render'
6 |
7 | export default flowRight([
8 | hot(module),
9 | connect(({ todos, app }: StoreState) => ({
10 | todos: visibleTodos(todos, app.sortBy, app.sortOrder, app.doneFilter),
11 | })),
12 | ])(App)
13 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface Todo {
2 | id: number
3 | content: string
4 | done: boolean
5 | dueDate: string
6 | createdAt: string
7 | updatedAt: string
8 | }
9 |
10 | export class IdentifiableError extends Error {
11 | constructor(readonly targetId: number, message?: string) {
12 | super(message)
13 | Object.setPrototypeOf(this, this.constructor.prototype) // restore prototype chain
14 | this.name = 'IdentifiableError'
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/assets/javascripts/types/ignore-styles.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module 'ignore-styles' {
4 | type Handler = (m: NodeModule, filename: string) => any
5 |
6 | export const DEFAULT_EXTENSIONS: string[]
7 |
8 | export let oldHandlers: {
9 | [ext: string]: Handler
10 | }
11 |
12 | export function noOp(): void
13 |
14 | export function restore(): void
15 |
16 | export default function register(
17 | extensions?: string[],
18 | handler?: Handler
19 | ): void
20 | }
21 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "modules": false,
7 | "targets": {
8 | "browsers": "> 1%",
9 | "uglify": true
10 | },
11 | "useBuiltIns": true
12 | }
13 | ],
14 | "react"
15 | ],
16 | "plugins": [
17 | "syntax-dynamic-import",
18 | ["transform-class-properties", { "spec": true }],
19 | ["transform-object-rest-spread", {"useBuiltIns": true}],
20 | "lodash",
21 | "react-hot-loader/babel"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__stories__/TodoConditions.stories.tsx:
--------------------------------------------------------------------------------
1 | import { action } from '@storybook/addon-actions'
2 | import { storiesOf } from '@storybook/react'
3 | import * as React from 'react'
4 | import TodoConditions from '../TodoConditions/render'
5 |
6 | storiesOf('TodoConditions', module).add('typical', () => (
7 |
14 | ))
15 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoList/render.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import TodoItem from '../TodoItem'
3 |
4 | export interface Props {
5 | todos: number[]
6 | }
7 |
8 | export default function TodoList({ todos }: Props) {
9 | return (
10 |
11 |
12 |
13 | | Content |
14 | Due date |
15 | |
16 |
17 |
18 | {todos.map(id => )}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | if spring
13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
14 | gem 'spring', spring.version
15 | require 'spring/binstub'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/assets/images/check-square-o.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": [
4 | "tslint:latest",
5 | "tslint-react",
6 | "tslint-eslint-rules",
7 | "tslint-config-prettier"
8 | ],
9 | "rules": {
10 | "quotemark": [true, "single", "jsx-double"],
11 | "semicolon": [true, "never"],
12 | "interface-name": [true, "never-prefix"],
13 | "object-literal-sort-keys": false,
14 | "no-submodule-imports": false,
15 | "member-access": [true, "no-public"],
16 | "jsx-boolean-value": [true, "never"],
17 | "jsx-no-lambda": false,
18 | "no-implicit-dependencies": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/views/todos/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= notice %>
2 |
3 |
4 | Content:
5 | <%= @todo.content %>
6 |
7 |
8 |
9 | Due date:
10 | <%= @todo.due_date %>
11 |
12 |
13 |
14 | Done:
15 | <% if @todo.done %>
16 |
17 | <% else %>
18 |
19 | <% end %>
20 |
21 |
22 | <%= link_to 'Edit', edit_todo_path(@todo), class: 'btn btn-secondary' %> |
23 | <%= link_to 'Back', todos_path, class: 'btn btn-secondary' %>
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/lib/case-util.ts:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash'
2 |
3 | type DictToDict = (obj: _.Dictionary) => _.Dictionary
4 |
5 | export function snakeCaseKeys(obj: object): object {
6 | const tr: DictToDict = o =>
7 | _.transform(o, (result, value, key) => {
8 | result[_.snakeCase(key)] = _.isPlainObject(value) ? tr(value) : value
9 | })
10 | return tr(obj)
11 | }
12 |
13 | export function camelCaseKeys(obj: object): object {
14 | const tr: DictToDict = o =>
15 | _.transform(o, (result, value, key) => {
16 | result[_.camelCase(key)] = _.isPlainObject(value) ? tr(value) : value
17 | })
18 | return tr(obj)
19 | }
20 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/App/render.tsx:
--------------------------------------------------------------------------------
1 | import * as reactIcon from 'images/react.svg'
2 | import * as React from 'react'
3 | import TodoAddForm from '../TodoAddForm'
4 | import TodoConditions from '../TodoConditions'
5 | import TodoList from '../TodoList'
6 | import * as styles from './styles.module.scss'
7 |
8 | export interface Props {
9 | todos: number[]
10 | }
11 |
12 | export default function App({ todos }: Props) {
13 | return (
14 |
15 |

16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/store/index.ts:
--------------------------------------------------------------------------------
1 | import { applyMiddleware, createStore, Store } from 'redux'
2 | import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction'
3 | import createSagaMiddleware from 'redux-saga'
4 | import reducers, { StoreState } from '../reducers'
5 | import rootSaga from '../sagas'
6 |
7 | export default function createAppStore(
8 | preloadedState: Partial
9 | ): Store {
10 | const sagaMiddleware = createSagaMiddleware()
11 | const enhancer = composeWithDevTools(applyMiddleware(sagaMiddleware))
12 |
13 | const store = createStore(reducers, preloadedState as StoreState, enhancer)
14 |
15 | sagaMiddleware.run(rootSaga)
16 |
17 | return store
18 | }
19 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/EditButton/render.tsx:
--------------------------------------------------------------------------------
1 | import * as classNames from 'classnames'
2 | import * as React from 'react'
3 |
4 | export interface Props {
5 | className?: string
6 | disabled?: boolean
7 | onClick: (e: React.MouseEvent) => void
8 | }
9 |
10 | export default function EditButton({
11 | className = '',
12 | disabled = false,
13 | onClick,
14 | }: Props) {
15 | return (
16 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/scaffold.scss:
--------------------------------------------------------------------------------
1 | div.field,
2 | div.actions {
3 | margin-bottom: 10px;
4 | }
5 |
6 | #notice {
7 | color: green;
8 | }
9 |
10 | .field_with_errors {
11 | padding: 2px;
12 | background-color: red;
13 | display: table;
14 | }
15 |
16 | #error_explanation {
17 | width: 450px;
18 | border: 2px solid red;
19 | padding: 7px 7px 0;
20 | margin-bottom: 20px;
21 | background-color: #f0f0f0;
22 | }
23 |
24 | #error_explanation h2 {
25 | text-align: left;
26 | font-weight: bold;
27 | padding: 5px 5px 5px 15px;
28 | font-size: 12px;
29 | margin: -7px -7px 0;
30 | background-color: #c00;
31 | color: #fff;
32 | }
33 |
34 | #error_explanation ul li {
35 | font-size: 12px;
36 | list-style: square;
37 | }
38 |
39 | label {
40 | display: block;
41 | }
42 |
--------------------------------------------------------------------------------
/app/assets/javascripts/types/assets.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: string
3 | export = content
4 | }
5 |
6 | declare module '*.png' {
7 | const content: string
8 | export = content
9 | }
10 |
11 | declare module '*.gif' {
12 | const content: string
13 | export = content
14 | }
15 |
16 | declare module '*.jpeg' {
17 | const content: string
18 | export = content
19 | }
20 |
21 | declare module '*.jpg' {
22 | const content: string
23 | export = content
24 | }
25 |
26 | interface StringMap {
27 | [key: string]: string
28 | }
29 |
30 | declare module '*.css' {
31 | const content: StringMap
32 | export = content
33 | }
34 |
35 | declare module '*.scss' {
36 | const content: StringMap
37 | export = content
38 | }
39 |
40 | declare module '*.sass' {
41 | const content: StringMap
42 | export default content
43 | }
44 |
--------------------------------------------------------------------------------
/.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 the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | /yarn-error.log
21 |
22 | .byebug_history
23 | /public/packs
24 | /public/packs-test
25 | /node_modules
26 |
27 | # Ignore text editor artifacts
28 | *.swp
29 | *~
30 |
31 | # Ignore macOS artificats
32 | .DS_Store
33 |
34 | # Ignore package-lock.json because this project uses yarn
35 | package-lock.json
36 |
37 | # Ignore awesome-typescript-loader cache
38 | .awcache
39 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a way to update your development environment automatically.
14 | # Add necessary update steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies if using Yarn
21 | # system('bin/yarn')
22 |
23 | puts "\n== Updating database =="
24 | system! 'bin/rails db:migrate'
25 |
26 | puts "\n== Removing old logs and tempfiles =="
27 | system! 'bin/rails log:clear tmp:clear'
28 |
29 | puts "\n== Restarting application server =="
30 | system! 'bin/rails restart'
31 | end
32 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at http://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/app/views/todos/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with(model: todo, local: true) do |form| %>
2 | <% if todo.errors.any? %>
3 |
4 |
<%= pluralize(todo.errors.count, "error") %> prohibited this todo from being saved:
5 |
6 |
7 | <% todo.errors.full_messages.each do |message| %>
8 | - <%= message %>
9 | <% end %>
10 |
11 |
12 | <% end %>
13 |
14 |
15 | <%= form.label :content, for: :todo_content %>
16 | <%= form.text_field :content, id: :todo_content, class: 'form-control' %>
17 |
18 |
19 |
20 | <%= form.label :due_date, for: :todo_due_date_1i %>
21 | <%= form.datetime_select :due_date, id: :todo_due_date, class: 'form-control' %>
22 |
23 |
24 |
25 | <%= form.label :done, for: :todo_done %>
26 | <%= form.check_box :done, id: :todo_done %>
27 |
28 |
29 |
30 | <%= form.submit class: 'btn btn-secondary' %>
31 |
32 | <% end %>
33 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 | include FileUtils
4 |
5 | # path to your application root.
6 | APP_ROOT = File.expand_path('..', __dir__)
7 |
8 | def system!(*args)
9 | system(*args) || abort("\n== Command #{args} failed ==")
10 | end
11 |
12 | chdir APP_ROOT do
13 | # This script is a starting point to setup your application.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies if using Yarn
21 | # system('bin/yarn')
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:setup'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | puts "\n== Restarting application server =="
35 | system! 'bin/rails restart'
36 | end
37 |
--------------------------------------------------------------------------------
/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 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20170617154903) do
14 |
15 | create_table "todos", force: :cascade do |t|
16 | t.string "content", null: false
17 | t.datetime "due_date", null: false
18 | t.boolean "done", null: false
19 | t.datetime "created_at", null: false
20 | t.datetime "updated_at", null: false
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoConditions/index.ts:
--------------------------------------------------------------------------------
1 | import { connect, Dispatch } from 'react-redux'
2 | import { Action, selectOrder, toggleDoneFilter } from '../../actions'
3 | import { SortBy, SortOrder, StoreState } from '../../reducers'
4 | import TodoConditions, { Props } from './render'
5 |
6 | type StateProps = Pick
7 | type DispatchProps = Pick
8 |
9 | export default connect(
10 | (state: StoreState) => ({
11 | sortBy: state.app.sortBy,
12 | sortOrder: state.app.sortOrder,
13 | doneFilter: state.app.doneFilter,
14 | }),
15 | (dispatch: Dispatch) => ({
16 | onOrderChange(
17 | ev:
18 | | React.ChangeEvent
19 | | React.FocusEvent
20 | ) {
21 | const [prop, order] = ev.currentTarget.value.split('-')
22 | dispatch(selectOrder(prop as SortBy, order as SortOrder))
23 | },
24 | onDoneFilterChange() {
25 | dispatch(toggleDoneFilter())
26 | },
27 | })
28 | )(TodoConditions)
29 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | require "active_storage/engine"
9 | require "action_controller/railtie"
10 | require "action_mailer/railtie"
11 | require "action_view/railtie"
12 | require "action_cable/engine"
13 | # require "sprockets/railtie"
14 | require "rails/test_unit/railtie"
15 |
16 | # Require the gems listed in Gemfile, including any gems
17 | # you've limited to :test, :development, or :production.
18 | Bundler.require(*Rails.groups)
19 |
20 | module WebpackerReactExample
21 | class Application < Rails::Application
22 | # Initialize configuration defaults for originally generated Rails version.
23 | config.load_defaults 5.1
24 |
25 | # Settings in config/environments/* take precedence over those specified here.
26 | # Application configuration can go into files in config/initializers
27 | # -- all .rb files in that directory are automatically loaded after loading
28 | # the framework and any gems in your application.
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 |
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 |
19 | # If you are using UJS then enable automatic nonce generation
20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
21 |
22 | # Report CSP violations to a specified URI
23 | # For further information see the following documentation:
24 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
25 | # Rails.application.config.content_security_policy_report_only = true
26 |
--------------------------------------------------------------------------------
/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 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
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
23 |
24 | # Use 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
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/assets/javascripts/change-query.ts:
--------------------------------------------------------------------------------
1 | import * as queryString from 'query-string'
2 |
3 | function prepereSelectElems(): void {
4 | const doms = document.querySelectorAll(
5 | 'select[data-change-query]'
6 | ) as NodeListOf
7 | const query = queryString.parse(location.search)
8 |
9 | for (const select of doms) {
10 | if (query.sort_by) {
11 | select.value = query.sort_by
12 | }
13 | select.addEventListener('change', () => {
14 | query.sort_by = select.value
15 | location.search = `?${queryString.stringify(query)}`
16 | })
17 | }
18 | }
19 |
20 | function prepereCheckboxElems(): void {
21 | const doms = document.querySelectorAll(
22 | 'input[type=checkbox][data-change-query]'
23 | ) as NodeListOf
24 | const query = queryString.parse(location.search)
25 |
26 | for (const checkbox of doms) {
27 | checkbox.checked = query.done && query.done !== 'false'
28 | checkbox.addEventListener('change', () => {
29 | query.done = checkbox.checked
30 | location.search = `?${queryString.stringify(query)}`
31 | })
32 | }
33 | }
34 |
35 | document.addEventListener('DOMContentLoaded', () => {
36 | prepereSelectElems()
37 | prepereCheckboxElems()
38 | })
39 |
--------------------------------------------------------------------------------
/config/webpack/environment.js:
--------------------------------------------------------------------------------
1 | const { environment } = require('@rails/webpacker')
2 | const merge = require('webpack-merge')
3 | const { resolve } = require('path')
4 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
5 |
6 | // Replace ts-loader with awesome-typescript-loader
7 | environment.loaders.append('typescript', {
8 | test: /\.tsx?$/,
9 | exclude: /node_modules/,
10 | loader: 'awesome-typescript-loader'
11 | })
12 |
13 | // Add resolve-url-loader
14 | // see: https://github.com/rails/webpacker/blob/master/docs/css.md#resolve-url-loader
15 | const resolveUrlLoader = {
16 | loader: 'resolve-url-loader',
17 | options: {
18 | attempts: 1
19 | }
20 | }
21 | environment.loaders.get('sass').use.splice(-1, 0, resolveUrlLoader)
22 | environment.loaders.get('moduleSass').use.splice(-1, 0, resolveUrlLoader)
23 |
24 | // Clearer generated classnames
25 | for (const ruleName of ['moduleCss', 'moduleSass']) {
26 | environment.loaders.get(ruleName).use
27 | .find(rule => rule.loader === 'css-loader')
28 | .options.localIdentName = '[folder]-[local]--[hash:base64:5]'
29 | }
30 |
31 | // Add bundle analyzer
32 | environment.plugins.append(
33 | 'Analyzer',
34 | new BundleAnalyzerPlugin({
35 | analyzerMode: 'static',
36 | openAnalyzer: false,
37 | })
38 | )
39 |
40 | module.exports = environment
41 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__stories__/TodoAddForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import { action } from '@storybook/addon-actions'
2 | import { storiesOf } from '@storybook/react'
3 | import * as React from 'react'
4 | import { Request } from '../../reducers'
5 | import TodoAddForm from '../TodoAddForm/render'
6 |
7 | function todoAddFormHelper({ addTodoRequest }: { addTodoRequest: Request }) {
8 | return (
9 |
17 | )
18 | }
19 |
20 | const succeededRequest: Request = {
21 | requesting: false,
22 | error: null,
23 | }
24 |
25 | const errorRequest: Request = {
26 | requesting: false,
27 | error: new Error('error'),
28 | }
29 |
30 | const loadingRequest: Request = {
31 | requesting: true,
32 | error: null,
33 | }
34 |
35 | storiesOf('TodoAddForm', module)
36 | .add('typical', () =>
37 | todoAddFormHelper({
38 | addTodoRequest: succeededRequest,
39 | })
40 | )
41 | .add('while adding', () =>
42 | todoAddFormHelper({
43 | addTodoRequest: loadingRequest,
44 | })
45 | )
46 | .add('adding error', () =>
47 | todoAddFormHelper({
48 | addTodoRequest: errorRequest,
49 | })
50 | )
51 |
--------------------------------------------------------------------------------
/config/espower-typescript.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 |
4 | var ts = require('typescript');
5 |
6 | var pattern = 'test/**/*.@(ts|tsx)';
7 | var cwd = process.cwd();
8 | var packageData = require(path.join(cwd, 'package.json'));
9 |
10 | if (packageData &&
11 | typeof packageData.directories === 'object' &&
12 | typeof packageData.directories.test === 'string') {
13 | var testDir = packageData.directories.test;
14 | pattern = testDir + ((testDir.lastIndexOf('/', 0) === 0) ? '' : '/') + '**/*.@(ts|tsx)';
15 | }
16 |
17 | var tsconfigPath = ts.findConfigFile(cwd, fs.existsSync, 'tsconfig.test.json');
18 | var tsconfigBasepath = null;
19 | var compilerOptions = null;
20 | if (tsconfigPath) {
21 | compilerOptions = parseTsConfig(tsconfigPath);
22 | tsconfigBasepath = path.dirname(tsconfigPath);
23 | }
24 |
25 | require('espower-typescript')({
26 | cwd: cwd,
27 | pattern: pattern,
28 | compilerOptions: compilerOptions,
29 | basepath: tsconfigBasepath
30 | });
31 |
32 | function parseTsConfig(tsconfigPath) {
33 | var parsed = ts.parseConfigFileTextToJson(tsconfigPath, fs.readFileSync(tsconfigPath, 'utf8'));
34 | if (parsed.error) {
35 | throw new Error(parsed.error.messageText);
36 | }
37 |
38 | if (!parsed.config || !parsed.config.compilerOptions) {
39 | return null;
40 | }
41 |
42 | return parsed.config.compilerOptions;
43 | }
44 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | # Shared secrets are available across all environments.
14 |
15 | # shared:
16 | # api_key: a1B2c3D4e5F6
17 |
18 | # Environmental secrets are only available for that specific environment.
19 |
20 | development:
21 | secret_key_base: e5bd6ee9b46adca3aa9f342b11214ed7589a797eaeb3ce6ccb683d02ec6c5cd595fbece5bdbf587cd982af2d86ed210b819d6b171fea2a111a62d2e48bf1ab7d
22 |
23 | test:
24 | secret_key_base: 57a9e5af41414c556026f7b68f8eac8af1d6aaabf36e9c79aa3c98bb91e54d19d5b7481f8fc58b045719d2119915f79023343e82284bc74f1f426565f05e36ab
25 |
26 | # Do not keep production secrets in the unencrypted secrets file.
27 | # Instead, either read values from the environment.
28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets
29 | # and move the `production:` environment over there.
30 |
31 | production:
32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
33 |
--------------------------------------------------------------------------------
/test/controllers/todos_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TodosControllerTest < ActionDispatch::IntegrationTest
4 | setup do
5 | @todo = todos(:one)
6 | end
7 |
8 | test 'should get index' do
9 | get todos_url
10 | assert_response :success
11 | end
12 |
13 | test 'should get new' do
14 | get new_todo_url
15 | assert_response :success
16 | end
17 |
18 | test 'should create todo' do
19 | assert_difference('Todo.count') do
20 | post todos_url, params: {
21 | todo: {
22 | content: @todo.content,
23 | done: @todo.done,
24 | due_date: @todo.due_date
25 | }
26 | }
27 | end
28 |
29 | assert_redirected_to todo_url(Todo.last)
30 | end
31 |
32 | test 'should show todo' do
33 | get todo_url(@todo)
34 | assert_response :success
35 | end
36 |
37 | test 'should get edit' do
38 | get edit_todo_url(@todo)
39 | assert_response :success
40 | end
41 |
42 | test 'should update todo' do
43 | patch todo_url(@todo), params: {
44 | todo: {
45 | content: @todo.content,
46 | done: @todo.done,
47 | due_date: @todo.due_date
48 | }
49 | }
50 | assert_redirected_to todo_url(@todo)
51 | end
52 |
53 | test 'should destroy todo' do
54 | assert_difference('Todo.count', -1) do
55 | delete todo_url(@todo)
56 | end
57 |
58 | assert_redirected_to todos_url
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | // you can use this file to add your custom webpack plugins, loaders and anything you like.
2 | // This is just the basic way to add additional webpack configurations.
3 | // For more information refer the docs: https://storybook.js.org/configurations/custom-webpack-config
4 |
5 | // IMPORTANT
6 | // When you add this file, we won't add the default configurations which is similar
7 | // to "React Create App". This only has babel loader to load JavaScript.
8 | const { resolve } = require('path');
9 |
10 | const config = require('@rails/webpacker/package/config');
11 | const environment = require('../config/webpack/environment')
12 |
13 | module.exports = (storybookBaseConfig, configType) => {
14 | // Here reusing webpacker's style rules. It needs setting hmr config true in
15 | // webpacker.yml. Otherwise ExtractTextPlugin will be used and result in runtime
16 | // errors in storybook.
17 | const rules = [
18 | environment.loaders.get('css'),
19 | environment.loaders.get('sass'),
20 | environment.loaders.get('moduleCss'),
21 | environment.loaders.get('moduleSass'),
22 | environment.loaders.get('typescript'),
23 | environment.loaders.get('file'),
24 | ]
25 | storybookBaseConfig.module.rules.push(...rules)
26 | storybookBaseConfig.resolve.extensions.push('.ts', '.tsx', 'scss');
27 | storybookBaseConfig.resolve.modules.unshift(resolve(config.source_path))
28 | return storybookBaseConfig
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import { Store } from 'redux'
5 | import { camelCaseKeys, snakeCaseKeys } from '../lib/case-util'
6 | import App from './components/App'
7 | import { StoreState } from './reducers'
8 | import createAppStore from './store'
9 | import { Todo } from './types'
10 |
11 | interface ServerState {
12 | todos: Array<{
13 | id: number
14 | content: string
15 | done: boolean
16 | due_date: string
17 | created_at: string
18 | updated_at: string
19 | }>
20 | }
21 |
22 | function convert(state: ServerState): Partial {
23 | const accumulator: { [id: number]: Todo } = {}
24 | const byId = state.todos.reduce((prev, curr) => {
25 | prev[curr.id] = camelCaseKeys(curr) as Todo
26 | return prev
27 | }, accumulator)
28 | const ids = state.todos.map(todo => todo.id)
29 |
30 | return {
31 | todos: {
32 | byId,
33 | ids,
34 | },
35 | }
36 | }
37 |
38 | function getPreloadedState() {
39 | const node = document.getElementById('todos-data')!
40 | return convert(JSON.parse(node.getAttribute('data')!) as ServerState)
41 | }
42 |
43 | document.addEventListener('DOMContentLoaded', () => {
44 | const store = createAppStore(getPreloadedState())
45 | ReactDOM.render(
46 |
47 |
48 | ,
49 | document.getElementById('todo-app')
50 | )
51 | })
52 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory.
30 | #
31 | # preload_app!
32 |
33 | # Allow puma to be restarted by `rails restart` command.
34 | plugin :tmp_restart
35 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoConditions/render.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { SortBy, SortOrder } from '../../reducers'
3 |
4 | export interface Props {
5 | sortBy: SortBy
6 | sortOrder: SortOrder
7 | doneFilter: boolean
8 | onOrderChange: (
9 | ev:
10 | | React.ChangeEvent
11 | | React.FocusEvent
12 | ) => void
13 | onDoneFilterChange: () => void
14 | }
15 |
16 | export default function TodoConditions({
17 | sortBy,
18 | sortOrder,
19 | doneFilter,
20 | onOrderChange,
21 | onDoneFilterChange,
22 | }: Props) {
23 | return (
24 |
25 |
26 |
40 |
41 |
42 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/config/webpacker.yml:
--------------------------------------------------------------------------------
1 | # Note: You must restart bin/webpack-dev-server for changes to take effect
2 |
3 | default: &default
4 | source_path: app/assets
5 | source_entry_path: packs
6 | public_output_path: packs
7 | cache_path: tmp/cache/webpacker
8 |
9 | # Additional paths webpack should lookup modules
10 | # ['app/assets', 'engine/foo/app/assets']
11 | resolved_paths: []
12 |
13 | # Reload manifest.json on all requests so we reload latest compiled packs
14 | cache_manifest: false
15 |
16 | extensions:
17 | - .coffee
18 | - .erb
19 | - .js
20 | - .jsx
21 | - .ts
22 | - .tsx
23 | - .vue
24 | - .sass
25 | - .scss
26 | - .css
27 | - .module.sass
28 | - .module.scss
29 | - .module.css
30 | - .png
31 | - .svg
32 | - .gif
33 | - .jpeg
34 | - .jpg
35 |
36 | development:
37 | <<: *default
38 | compile: true
39 |
40 | # Reference: https://webpack.js.org/configuration/dev-server/
41 | dev_server:
42 | https: false
43 | host: localhost
44 | port: 3035
45 | public: localhost:3035
46 | hmr: true # hmr should be set to true for storybook
47 | # Inline should be set to true if using HMR
48 | inline: true
49 | overlay: true
50 | disable_host_check: true
51 | use_local_ip: false
52 |
53 | test:
54 | <<: *default
55 | compile: true
56 |
57 | # Compile test packs to a separate directory
58 | public_output_path: packs-test
59 |
60 | production:
61 | <<: *default
62 |
63 | # Production depends on precompilation of packs prior to booting for performance.
64 | compile: false
65 |
66 | # Cache manifest.json for performance
67 | cache_manifest: true
68 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__test__/TodoAddForm.spec.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:ordered-imports */
2 | import register from 'ignore-styles'
3 | register(undefined, module => {
4 | const styles = ['item', 'content', 'dueDate', 'error']
5 | module.exports = _.zipObject(styles, styles)
6 | })
7 | /* tslint:enable:ordered-imports */
8 |
9 | import * as assert from 'assert'
10 | import * as enzyme from 'enzyme'
11 | import * as _ from 'lodash'
12 | import 'mocha'
13 | import * as React from 'react'
14 | import { Request } from '../../reducers'
15 | import TodoAddForm from '../TodoAddForm/render'
16 | import './setup'
17 |
18 | describe('', () => {
19 | describe('display errors', () => {
20 | function item(request: Request) {
21 | return (
22 |
30 | )
31 | }
32 |
33 | context('when request failed', () => {
34 | it('should render error message', () => {
35 | const request = { requesting: false, error: new Error('error') }
36 | const wrapper = enzyme.shallow(item(request))
37 | assert(wrapper.find('.error').exists())
38 | })
39 | })
40 |
41 | context('when request succeeded', () => {
42 | it('should not render error message', () => {
43 | const request = { requesting: false, error: null }
44 | const wrapper = enzyme.shallow(item(request))
45 | assert(!wrapper.find('.error').exists())
46 | })
47 | })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoAddForm/index.ts:
--------------------------------------------------------------------------------
1 | import * as moment from 'moment'
2 | import { connect, Dispatch } from 'react-redux'
3 | import { compose, withHandlers, withState } from 'recompose'
4 | import { Action, addTodoRequested } from '../../actions'
5 | import { SINGLETON_ID, StoreState } from '../../reducers'
6 | import TodoAddForm, { Props } from './render'
7 |
8 | type StateProps = Pick
9 | type DispatchProps = Pick
10 |
11 | interface StateUpdaters {
12 | setContent: (value: string) => void
13 | setDueDate: (value: Date) => void
14 | }
15 |
16 | const enhancer = compose(
17 | connect(
18 | ({ app: { requests } }: StoreState) => ({
19 | addTodoRequest: requests.addTodo[SINGLETON_ID] || {
20 | requesting: false,
21 | error: null,
22 | },
23 | }),
24 | (dispatch: Dispatch) => ({
25 | onAddTodo(content: string, dueDate: Date) {
26 | dispatch(addTodoRequested(content, dueDate.toISOString()))
27 | },
28 | })
29 | ),
30 | withState('content', 'setContent', ''),
31 | withState('dueDate', 'setDueDate', new Date()),
32 | withHandlers<
33 | StateUpdaters & DispatchProps,
34 | Pick
35 | >({
36 | onAddTodo: props => (content, dueDate) => {
37 | props.setContent('')
38 | props.setDueDate(new Date())
39 | props.onAddTodo(content, dueDate)
40 | },
41 | onChangeContent: props => ev => {
42 | props.setContent(ev.currentTarget.value)
43 | },
44 | onChangeDueDate: props => ev => {
45 | if (moment.isMoment(ev)) {
46 | props.setDueDate(ev.toDate())
47 | }
48 | },
49 | })
50 | )
51 |
52 | export default enhancer(TodoAddForm)
53 |
--------------------------------------------------------------------------------
/app/views/todos/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= notice %>
2 |
3 |
4 |
5 |
21 |
22 |
23 |
24 |
25 | | Content |
26 | Due date |
27 | |
28 |
29 |
30 |
31 |
32 | <% @todos.each do |todo| %>
33 |
34 |
35 | <% if todo.done %>
36 |
37 | <% else %>
38 |
39 | <% end %>
40 | <%= todo.content %>
41 | |
42 | <%= todo.due_date %> |
43 | <%= link_to 'Show', todo, class: 'btn btn-secondary' %> |
44 | <%= link_to 'Edit', edit_todo_path(todo), class: 'btn btn-secondary' %> |
45 | <%= link_to 'Destroy', todo, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-secondary' %> |
46 |
47 | <% end %>
48 |
49 |
50 |
51 |
52 |
53 | <%= link_to 'New Todo', new_todo_path, class: 'btn btn-secondary' %>
54 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults_5_2.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 5.2 upgrade.
4 | #
5 | # Once upgraded flip defaults one by one to migrate to the new default.
6 | #
7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option.
8 |
9 | # Make Active Record use stable #cache_key alongside new #cache_version method.
10 | # This is needed for recyclable cache keys.
11 | # Rails.application.config.active_record.cache_versioning = true
12 |
13 | # Use AES-256-GCM authenticated encryption for encrypted cookies.
14 | # Also, embed cookie expiry in signed or encrypted cookies for increased security.
15 | #
16 | # This option is not backwards compatible with earlier Rails versions.
17 | # It's best enabled when your entire app is migrated and stable on 5.2.
18 | #
19 | # Existing cookies will be converted on read then written with the new scheme.
20 | # Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true
21 |
22 | # Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
23 | # instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
24 | # Rails.application.config.active_support.use_authenticated_message_encryption = true
25 |
26 | # Add default protection from forgery to ActionController::Base instead of in
27 | # ApplicationController.
28 | # Rails.application.config.action_controller.default_protect_from_forgery = true
29 |
30 | # Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
31 | # 'f' after migrating old data.
32 | # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
33 |
34 | # Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
35 | # Rails.application.config.active_support.use_sha1_digests = true
36 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | git_source(:github) do |repo_name|
4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
5 | "https://github.com/#{repo_name}.git"
6 | end
7 |
8 |
9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
10 | gem 'rails', '~> 5.2.0'
11 | # Boot large Ruby/Rails apps faster
12 | gem 'bootsnap', require: false
13 | # Use sqlite3 as the database for Active Record
14 | gem 'sqlite3'
15 | # Use Puma as the app server
16 | gem 'puma', '~> 3.7'
17 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
18 | gem 'webpacker'
19 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
20 | gem 'jbuilder', '~> 2.5'
21 | # Use Redis adapter to run Action Cable in production
22 | # gem 'redis', '~> 3.0'
23 | # Use ActiveModel has_secure_password
24 | # gem 'bcrypt', '~> 3.1.7'
25 |
26 | # Use Capistrano for deployment
27 | # gem 'capistrano-rails', group: :development
28 |
29 | group :development, :test do
30 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
31 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
32 | # Adds support for Capybara system testing and selenium driver
33 | gem 'capybara', '~> 2.13'
34 | gem 'selenium-webdriver'
35 | # Add lint tool
36 | gem 'rubocop', require: false
37 | end
38 |
39 | group :development do
40 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
41 | gem 'web-console', '>= 3.3.0'
42 | gem 'listen', '>= 3.0.5', '< 3.2'
43 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
44 | gem 'spring'
45 | gem 'spring-watcher-listen', '~> 2.0.0'
46 | end
47 |
48 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
49 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
50 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoAddForm/render.tsx:
--------------------------------------------------------------------------------
1 | import * as classNames from 'classnames'
2 | import * as moment from 'moment'
3 | import * as React from 'react'
4 | import * as DateTime from 'react-datetime'
5 | import { Request } from '../../reducers'
6 | import * as styles from './styles.module.scss'
7 |
8 | export interface Props {
9 | content: string
10 | dueDate: Date
11 | addTodoRequest: Request
12 | onAddTodo: (content: string, dueDate: Date) => void
13 | onChangeContent: (ev: React.ChangeEvent) => void
14 | onChangeDueDate: (ev: React.ChangeEvent | moment.Moment | string) => void
15 | }
16 |
17 | export default function TodoAddForm({
18 | content,
19 | dueDate,
20 | addTodoRequest,
21 | onAddTodo,
22 | onChangeContent,
23 | onChangeDueDate,
24 | }: Props) {
25 | return (
26 |
27 |
28 |
37 |
38 |
39 |
47 |
48 |
55 | {addTodoRequest.error && (
56 |
Create Todo Failed
57 | )}
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 |
31 | # Store uploaded files on the local file system in a temporary directory
32 | config.active_storage.service = :test
33 |
34 | config.action_mailer.perform_caching = false
35 |
36 | # Tell Action Mailer not to deliver emails to the real world.
37 | # The :test delivery method accumulates sent emails in the
38 | # ActionMailer::Base.deliveries array.
39 | config.action_mailer.delivery_method = :test
40 |
41 | # Print deprecation notices to the stderr.
42 | config.active_support.deprecation = :stderr
43 |
44 | # Raises error for missing translations
45 | # config.action_view.raise_on_missing_translations = true
46 | end
47 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | # Run rails dev:cache to toggle caching.
17 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
18 | config.action_controller.perform_caching = true
19 |
20 | config.cache_store = :memory_store
21 | config.public_file_server.headers = {
22 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
23 | }
24 | else
25 | config.action_controller.perform_caching = false
26 |
27 | config.cache_store = :null_store
28 | end
29 |
30 | # Store uploaded files on the local file system (see config/storage.yml for options)
31 | config.active_storage.service = :local
32 |
33 | # Don't care if the mailer can't send.
34 | config.action_mailer.raise_delivery_errors = false
35 |
36 | config.action_mailer.perform_caching = false
37 |
38 | # Print deprecation notices to the Rails logger.
39 | config.active_support.deprecation = :log
40 |
41 | # Raise an error on page load if there are pending migrations.
42 | config.active_record.migration_error = :page_load
43 |
44 | # Highlight code that triggered database queries in logs.
45 | config.active_record.verbose_query_logs = true
46 |
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 | end
55 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/webApi/index.ts:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash'
2 | import { csrfToken } from 'rails-ujs'
3 | import * as request from 'superagent'
4 | import { camelCaseKeys, snakeCaseKeys } from '../../lib/case-util'
5 | import { Todo } from '../types'
6 |
7 | function error(res: request.Response): Error {
8 | return new Error(
9 | (res && res.body && res.body.data && res.body.data.message) ||
10 | 'unexpected error'
11 | )
12 | }
13 |
14 | export function addTodo(
15 | content: string,
16 | dueDate: string,
17 | done: boolean
18 | ): Promise {
19 | return new Promise((resolve, reject) => {
20 | const data = {
21 | content,
22 | dueDate,
23 | done,
24 | }
25 | request
26 | .post('/todos.json')
27 | .send(snakeCaseKeys(data))
28 | .set('X-CSRF-Token', csrfToken()!)
29 | .end((err, res) => {
30 | if (err) {
31 | reject(error(res))
32 | } else {
33 | resolve(camelCaseKeys(res.body) as Todo)
34 | }
35 | })
36 | })
37 | }
38 |
39 | export function updateTodo(
40 | id: number,
41 | content?: string,
42 | dueDate?: string,
43 | done?: boolean
44 | ): Promise {
45 | return new Promise((resolve, reject) => {
46 | const data = _.omitBy(
47 | {
48 | content,
49 | dueDate,
50 | done,
51 | },
52 | _.isUndefined
53 | )
54 | request
55 | .put(`/todos/${id}.json`)
56 | .send(snakeCaseKeys(data))
57 | .set('X-CSRF-Token', csrfToken()!)
58 | .end((err, res) => {
59 | if (err) {
60 | reject(error(res))
61 | } else {
62 | resolve(camelCaseKeys(res.body) as Todo)
63 | }
64 | })
65 | })
66 | }
67 |
68 | export function deleteTodo(id: number): Promise {
69 | return new Promise((resolve, reject) => {
70 | request
71 | .delete(`/todos/${id}.json`)
72 | .set('X-CSRF-Token', csrfToken()!)
73 | .end((err, res) => {
74 | if (err) {
75 | reject(error(res))
76 | } else {
77 | resolve()
78 | }
79 | })
80 | })
81 | }
82 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/reducers/__test__/app.spec.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert'
2 | import * as _ from 'lodash'
3 | import 'mocha'
4 | import * as actions from '../../actions'
5 | import appReducer, { AppState, initialAppState, SINGLETON_ID } from '../app'
6 |
7 | function todoItem() {
8 | return {
9 | id: 1,
10 | content: 'todo',
11 | done: false,
12 | dueDate: '2017-09-30T07:32:08.591Z',
13 | createdAt: '2017-09-30T07:32:08.591Z',
14 | updatedAt: '2017-09-30T07:32:08.591Z',
15 | }
16 | }
17 |
18 | describe('App reducer', () => {
19 | describe('ToggleDoneFilter Action', () => {
20 | it('should toggles doneFilter state', () => {
21 | const newState = appReducer(initialAppState, actions.toggleDoneFilter())
22 | assert.notEqual(newState.doneFilter, initialAppState.doneFilter)
23 | })
24 | })
25 |
26 | describe('SelectOrder Action', () => {
27 | it('should set sortBy and sortOrder state', () => {
28 | const state: AppState = {
29 | ...initialAppState,
30 | sortBy: 'createdAt',
31 | sortOrder: 'asc',
32 | }
33 | const newState = appReducer(state, actions.selectOrder('dueDate', 'desc'))
34 | assert.equal(newState.sortBy, 'dueDate')
35 | assert.equal(newState.sortOrder, 'desc')
36 | })
37 | })
38 |
39 | describe('AddTodoRequested Action', () => {
40 | it('should set requesting to true and error to null', () => {
41 | const newState = appReducer(
42 | initialAppState,
43 | actions.addTodoRequested('todo', '2017-09-30T07:32:08.591Z')
44 | )
45 | assert.deepEqual(newState.requests.addTodo[SINGLETON_ID]!, {
46 | requesting: true,
47 | error: null,
48 | })
49 | })
50 | })
51 |
52 | describe('AddTodoReceived Action', () => {
53 | it('should remove requests entry', () => {
54 | const state: AppState = _.set(initialAppState, 'requests.addTodo', {
55 | [SINGLETON_ID]: {
56 | requesting: true,
57 | error: null,
58 | },
59 | })
60 | const newState = appReducer(
61 | state,
62 | actions.addTodoReceived({
63 | requestId: SINGLETON_ID,
64 | item: todoItem(),
65 | })
66 | )
67 | assert.deepEqual(newState.requests.addTodo, {})
68 | })
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/sagas/index.ts:
--------------------------------------------------------------------------------
1 | import { call, put, select, takeEvery, takeLatest } from 'redux-saga/effects'
2 | import * as actions from '../actions'
3 | import { SINGLETON_ID, StoreState } from '../reducers'
4 | import { IdentifiableError } from '../types'
5 | import * as webApi from '../webApi'
6 |
7 | function* addTodoRequested(action: actions.AddTodoRequested) {
8 | try {
9 | const { requestId, item: { content, dueDate } } = action.payload
10 | const item = yield call(webApi.addTodo, content, dueDate, false)
11 | yield put(actions.addTodoReceived({ requestId, item }))
12 | } catch (error) {
13 | yield put(
14 | actions.addTodoReceived(
15 | new IdentifiableError(SINGLETON_ID, error.message)
16 | )
17 | )
18 | }
19 | }
20 |
21 | function* updateTodoRequested(action: actions.UpdateTodoRequested) {
22 | const { requestId, item: { id, content, dueDate } } = action.payload
23 | try {
24 | const item = yield call(webApi.updateTodo, id, content, dueDate)
25 | yield put(actions.updateTodoReceived({ requestId, item }))
26 | } catch (error) {
27 | yield put(
28 | actions.updateTodoReceived(
29 | new IdentifiableError(requestId, error.message)
30 | )
31 | )
32 | }
33 | }
34 |
35 | function* toggleTodoDoneRequested(action: actions.ToggleTodoDoneRequested) {
36 | const { requestId, item: { id } } = action.payload
37 | try {
38 | const done = yield select((state: StoreState) => state.todos.byId[id].done)
39 | const item = yield call(webApi.updateTodo, id, undefined, undefined, !done)
40 | yield put(actions.toggleTodoDoneReceived({ requestId, item }))
41 | } catch (error) {
42 | yield put(
43 | actions.toggleTodoDoneReceived(
44 | new IdentifiableError(requestId, error.message)
45 | )
46 | )
47 | }
48 | }
49 |
50 | function* deleteTodoRequested(action: actions.DeleteTodoRequested) {
51 | const { requestId, item: { id } } = action.payload
52 | try {
53 | yield call(webApi.deleteTodo, id)
54 | yield put(actions.deleteTodoReceived(action.payload))
55 | } catch (error) {
56 | yield put(
57 | actions.deleteTodoReceived(
58 | new IdentifiableError(requestId, error.message)
59 | )
60 | )
61 | }
62 | }
63 |
64 | export default function* rootSaga() {
65 | yield takeLatest('ADD_TODO:REQUESTED', addTodoRequested)
66 | yield takeEvery('UPDATE_TODO:REQUESTED', updateTodoRequested)
67 | yield takeEvery('TOGGLE_TODO_DONE:REQUESTED', toggleTodoDoneRequested)
68 | yield takeEvery('DELETE_TODO:REQUESTED', deleteTodoRequested)
69 | }
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Webpacker 3 modern front-end example todo app
2 |
3 | ## Dependency
4 |
5 | * [Rails 5.2](http://guides.rubyonrails.org/5_2_release_notes.html)
6 | * [Webpacker 3.5.5](http://weblog.rubyonrails.org/2017/8/30/webpacker-3-0/)
7 |
8 | ## Features
9 |
10 | * **No Sprockets**. Using webpacker for overall asset management.
11 | * Both [React](https://reactjs.org/) client app and normal Rails MVC.
12 | * Full npm support for [sprinkles](http://guides.rubyonrails.org/working_with_javascript_in_rails.html) including `import` statement.
13 | * CSRF protection for API call.
14 | * Add hash suffix to static asset filenames for production(cache-buster).
15 | * Apply [bootstrap](http://getbootstrap.com/) CSS both server and client side.
16 | * [TypeScript](https://www.typescriptlang.org/index.html) support for sprinkles and client app.
17 | * Flux for client architecture using React [Redux](http://redux.js.org/) with static type checking.
18 | * Using [CSS Modules](https://glenmaddern.com/articles/css-modules) for client compoents styling.
19 | * Faster UI development using [Storybook](https://storybook.js.org/).
20 | * Simple API and comprehensive message for assertion by [power-assert](https://github.com/power-assert-js/power-assert).
21 | * Component testing written by TypeScript using [mocha](https://mochajs.org/) and [enzyme](https://github.com/airbnb/enzyme).
22 | * Automatic code formatting using [pretteir](https://github.com/prettier/prettier)
23 | * Static code analysis using [TSLint](https://palantir.github.io/tslint/)
24 | * Code size analysis using [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer).
25 | * Using [lodash](https://lodash.com/) and just actually imported functions are bundled by webpack's tree-shaking.
26 | * Using [Babel](https://babeljs.io/) to transpile esnext.
27 | * [Hot Module Replacement](https://webpack.js.org/guides/hot-module-replacement/) for development.
28 |
29 | ## Out of the scope
30 |
31 | * Server side rendering
32 | * Client side routing
33 |
34 | ## How to run
35 |
36 | ```
37 | yarn install
38 | bundle install
39 | bin/rails db:setup
40 | bin/rails db:migrate
41 | foreman start
42 | ```
43 |
44 | Then open http://localhost:3000/
45 |
46 | ## How to deploy
47 |
48 | Run webpacker to build.
49 |
50 | ```
51 | RAILS_ENV=production bin/rails webpacker:compile
52 | ```
53 |
54 | Then compiled assets will be generated to `public/packs/`.
55 | Copy these files to public directory and run server.
56 |
57 | ## Run client side unit tests
58 |
59 | ```
60 | yarn test
61 | ```
62 |
63 | ## Show storybook client UI catalog
64 |
65 | ```
66 | yarn storybook
67 | ```
68 |
69 | ## Run client code formatting
70 |
71 | ```
72 | yarn prettier
73 | ```
74 |
75 | ## Run client code lint
76 |
77 | ```
78 | yarn lint
79 | ```
80 |
81 | ## Bundle size analysis
82 |
83 | `public/packs/report.html` is generated after build finished.
84 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__test__/TodoItem.spec.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:ordered-imports */
2 | import register from 'ignore-styles'
3 | register(undefined, module => {
4 | const styles = [
5 | 'contentLabel',
6 | 'editButton',
7 | 'contentCol',
8 | 'dueDateCol',
9 | 'error',
10 | ]
11 | module.exports = _.zipObject(styles, styles)
12 | })
13 | /* tslint:enable:ordered-imports */
14 |
15 | import * as assert from 'assert'
16 | import * as enzyme from 'enzyme'
17 | import * as _ from 'lodash'
18 | import 'mocha'
19 | import * as React from 'react'
20 | import { Request } from '../../reducers'
21 | import TodoItem from '../TodoItem/render'
22 | import './setup'
23 |
24 | describe('', () => {
25 | describe('display errors', () => {
26 | function item(updateRequest: Request, deleteRequest: Request) {
27 | const todo = {
28 | id: 1,
29 | content: 'todo',
30 | done: false,
31 | dueDate: '2017-09-30T07:32:08.591Z',
32 | createdAt: '2017-09-30T07:32:08.591Z',
33 | updatedAt: '2017-09-30T07:32:08.591Z',
34 | }
35 | return (
36 |
50 | )
51 | }
52 |
53 | context('when update request failed', () => {
54 | it('should render error message', () => {
55 | const updateRequest = { requesting: false, error: new Error('error') }
56 | const deleteRequest = { requesting: false, error: null }
57 | const wrapper = enzyme.shallow(item(updateRequest, deleteRequest))
58 | assert.equal(wrapper.find('.error').text(), 'Update todo failed')
59 | })
60 | })
61 |
62 | context('when delete request failed', () => {
63 | it('should render error message', () => {
64 | const updateRequest = { requesting: false, error: null }
65 | const deleteRequest = { requesting: false, error: new Error('error') }
66 | const wrapper = enzyme.shallow(item(updateRequest, deleteRequest))
67 | assert.equal(wrapper.find('.error').text(), 'Delete todo failed')
68 | })
69 | })
70 |
71 | context('when all requests succeeded', () => {
72 | it('should not render error message', () => {
73 | const updateRequest = { requesting: false, error: null }
74 | const deleteRequest = { requesting: false, error: null }
75 | const wrapper = enzyme.shallow(item(updateRequest, deleteRequest))
76 | assert(!wrapper.find('.error').exists())
77 | })
78 | })
79 | })
80 | })
81 |
--------------------------------------------------------------------------------
/app/controllers/todos_controller.rb:
--------------------------------------------------------------------------------
1 | class TodosController < ApplicationController
2 | before_action :set_todo, only: %i[show edit update destroy]
3 | before_action :new_todo, only: [:create]
4 |
5 | # GET /todos
6 | # GET /todos.json
7 | def index
8 | @todos = Todo.order(index_order).where(index_filter)
9 | end
10 |
11 | # GET /todos/1
12 | # GET /todos/1.json
13 | def show; end
14 |
15 | # GET /todos/new
16 | def new
17 | @todo = Todo.new
18 | end
19 |
20 | # GET /todos/1/edit
21 | def edit; end
22 |
23 | # POST /todos
24 | # POST /todos.json
25 | def create
26 | respond_to do |format|
27 | if @todo.save
28 | format.html do
29 | redirect_to @todo, notice: 'Todo was successfully created.'
30 | end
31 | format.json { render :show, status: :created, location: @todo }
32 | else
33 | render_errors :new
34 | end
35 | end
36 | end
37 |
38 | # PATCH/PUT /todos/1
39 | # PATCH/PUT /todos/1.json
40 | def update
41 | respond_to do |format|
42 | if @todo.update(todo_params)
43 | format.html do
44 | redirect_to @todo, notice: 'Todo was successfully updated.'
45 | end
46 | format.json { render :show, status: :ok, location: @todo }
47 | else
48 | render_errors :edit
49 | end
50 | end
51 | end
52 |
53 | # DELETE /todos/1
54 | # DELETE /todos/1.json
55 | def destroy
56 | @todo.destroy
57 | respond_to do |format|
58 | format.html do
59 | redirect_to todos_url, notice: 'Todo was successfully destroyed.'
60 | end
61 | format.json { head :no_content }
62 | end
63 | end
64 |
65 | private
66 |
67 | def render_errors(action)
68 | format.html { render action }
69 | format.json { render json: @todo.errors, status: :unprocessable_entity }
70 | end
71 |
72 | def new_todo
73 | @todo = Todo.new(todo_params)
74 | end
75 |
76 | # Use callbacks to share common setup or constraints between actions.
77 | def set_todo
78 | @todo = Todo.find(params[:id])
79 | end
80 |
81 | # Never trust parameters from the scary internet, only allow the white list
82 | # through.
83 | def todo_params
84 | params.require(:todo).permit(:content, :due_date, :done)
85 | end
86 |
87 | def index_order
88 | case params[:sort_by]
89 | when 'created_at_asc'
90 | { created_at: :asc }
91 | when 'created_at_desc'
92 | { created_at: :desc }
93 | when 'due_date_desc'
94 | { due_date: :desc }
95 | else
96 | { due_date: :asc }
97 | end
98 | end
99 |
100 | def index_filter
101 | cond = { done: false }
102 | done_is_truthy && cond.delete(:done)
103 | cond
104 | end
105 |
106 | def done_is_truthy
107 | params.key?(:done) && params[:done] != 'false'
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpacker-react-example",
3 | "private": true,
4 | "scripts": {
5 | "lint": "tslint -c tslint.json 'app/**/*.ts' 'app/**/*.tsx'",
6 | "lint:fix": "tslint -c tslint.json 'app/**/*.ts' '**/*.tsx' --fix",
7 | "test": "mocha --require ./config/espower-typescript.js **/*.spec.{ts,tsx}",
8 | "storybook": "RAILS_ENV=development start-storybook -p 6006 webpack-dev-server",
9 | "prettier": "prettier --single-quote --trailing-comma=es5 --no-semi --write 'app/**/*.{ts,tsx,css,scss}'",
10 | "precommit-check": "yarn prettier && yarn lint && yarn test"
11 | },
12 | "directories": {
13 | "test": "**/__test__/"
14 | },
15 | "resolutions": {
16 | "**/@types/node": "^8.0.31",
17 | "**/@types/react": "~16.4.14"
18 | },
19 | "dependencies": {
20 | "@fortawesome/fontawesome-free": "^5.1.1",
21 | "@rails/webpacker": "^3.5.5",
22 | "@types/classnames": "^2.2.3",
23 | "@types/lodash": "^4.14.74",
24 | "@types/node": "^8.0.31",
25 | "@types/query-string": "^5.0.0",
26 | "@types/react": "~16.4.14",
27 | "@types/react-dom": "~16.0.7",
28 | "@types/react-hot-loader": "^4.1.0",
29 | "@types/react-redux": "~5.0.19",
30 | "@types/recompose": "~0.26.5",
31 | "@types/superagent": "^3.5.5",
32 | "autoprefixer": "^7.1.1",
33 | "awesome-typescript-loader": "~4.0.0",
34 | "babel-plugin-lodash": "^3.2.11",
35 | "babel-polyfill": "^6.26.0",
36 | "babel-preset-react": "^6.24.1",
37 | "bootstrap": "^4.1.2",
38 | "classnames": "^2.2.5",
39 | "coffeescript": "^2.0.1",
40 | "lodash": "^4.17.4",
41 | "moment": "^2.18.1",
42 | "query-string": "^4.3.4",
43 | "rails-ujs": "^5.1.1",
44 | "react": "~16.4.2",
45 | "react-datetime": "^2.9.0",
46 | "react-dom": "~16.4.2",
47 | "react-hot-loader": "^4.3.4",
48 | "react-redux": "~5.0.7",
49 | "recompose": "~0.26.0",
50 | "redux": "~4.0.0",
51 | "redux-devtools-extension": "~2.13.5",
52 | "redux-saga": "^0.15.6",
53 | "resolve-url-loader": "^2.3.0",
54 | "superagent": "^3.5.2",
55 | "typescript": "^3.0.1",
56 | "webpack-merge": "^4.1.0"
57 | },
58 | "devDependencies": {
59 | "@storybook/addon-actions": "^3.2.12",
60 | "@storybook/addon-links": "^3.2.12",
61 | "@storybook/cli": "^3.2.12",
62 | "@storybook/react": "~3.4.10",
63 | "@types/enzyme": "~3.1.13",
64 | "@types/enzyme-adapter-react-16": "~1.0.3",
65 | "@types/mocha": "^2.2.43",
66 | "@types/storybook__addon-actions": "^3.0.1",
67 | "@types/storybook__react": "^3.0.5",
68 | "enzyme": "~3.1.1",
69 | "enzyme-adapter-react-16": "~1.5.0",
70 | "espower-typescript": "^8.1.1",
71 | "ignore-styles": "^5.0.1",
72 | "mocha": "^3.5.3",
73 | "power-assert": "^1.4.4",
74 | "prettier": "^1.7.4",
75 | "react-test-renderer": "~16.4.2",
76 | "tslint": "^5.11.0",
77 | "tslint-config-prettier": "^1.5.0",
78 | "tslint-eslint-rules": "^4.1.1",
79 | "tslint-react": "^3.2.0",
80 | "webpack-bundle-analyzer": "^2.9.0",
81 | "webpack-dev-server": "^2.11.1"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoItem/index.ts:
--------------------------------------------------------------------------------
1 | import * as moment from 'moment'
2 | import { connect, Dispatch } from 'react-redux'
3 | import { compose, withHandlers, withState } from 'recompose'
4 | import { StoreState } from '../../reducers'
5 | import { Todo } from '../../types'
6 |
7 | import {
8 | Action,
9 | deleteTodoRequested,
10 | toggleTodoDoneRequested,
11 | updateTodoRequested,
12 | } from '../../actions'
13 |
14 | import TodoItem, { Props } from './render'
15 |
16 | type OwnProps = Pick
17 | type StateProps = Pick
18 | type DispatchProps = Pick<
19 | Props,
20 | 'onCheckboxChange' | 'onContentBlur' | 'onDueDateBlur' | 'onDestroyClick'
21 | >
22 |
23 | interface StateUpdaters {
24 | setContentEditing: (value: boolean) => void
25 | setDueDateEditing: (value: boolean) => void
26 | }
27 |
28 | const enhancer = compose(
29 | connect(
30 | (state: StoreState, ownProps: OwnProps) => ({
31 | todo: state.todos.byId[ownProps.id],
32 | updateRequest: state.app.requests.updateTodo[ownProps.id] ||
33 | state.app.requests.toggleTodoDone[ownProps.id] || {
34 | requesting: false,
35 | error: null,
36 | },
37 | deleteRequest: state.app.requests.deleteTodo[ownProps.id] || {
38 | requesting: false,
39 | error: null,
40 | },
41 | }),
42 | (dispatch: Dispatch, ownProps: OwnProps) => ({
43 | onCheckboxChange() {
44 | dispatch(toggleTodoDoneRequested(ownProps.id))
45 | },
46 | onContentBlur(ev: React.FocusEvent, todo: Todo) {
47 | dispatch(
48 | updateTodoRequested(ownProps.id, ev.currentTarget.value, todo.dueDate)
49 | )
50 | },
51 | onDueDateBlur(
52 | ev: React.FocusEvent | moment.Moment | string,
53 | todo: Todo
54 | ) {
55 | if (moment.isMoment(ev)) {
56 | dispatch(
57 | updateTodoRequested(ownProps.id, todo.content, ev.toISOString())
58 | )
59 | }
60 | },
61 | onDestroyClick() {
62 | if (confirm('Are you sure?')) {
63 | dispatch(deleteTodoRequested(ownProps.id))
64 | }
65 | },
66 | })
67 | ),
68 | withState('contentEditing', 'setContentEditing', false),
69 | withState('dueDateEditing', 'setDueDateEditing', false),
70 | withHandlers<
71 | StateUpdaters & DispatchProps,
72 | Pick<
73 | Props,
74 | 'onContentClick' | 'onContentBlur' | 'onDueDateClick' | 'onDueDateBlur'
75 | >
76 | >({
77 | onContentClick: props => () => {
78 | props.setContentEditing(true)
79 | },
80 | onContentBlur: props => (event, todo) => {
81 | props.setContentEditing(false)
82 | props.onContentBlur(event, todo)
83 | },
84 | onDueDateClick: props => () => {
85 | props.setDueDateEditing(true)
86 | },
87 | onDueDateBlur: props => (event, todo) => {
88 | props.setDueDateEditing(false)
89 | props.onDueDateBlur(event, todo)
90 | },
91 | })
92 | )
93 |
94 | export default enhancer(TodoItem)
95 |
--------------------------------------------------------------------------------
/app/assets/images/rails.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/images/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/__stories__/TodoItem.stories.tsx:
--------------------------------------------------------------------------------
1 | import { action } from '@storybook/addon-actions'
2 | import { storiesOf } from '@storybook/react'
3 | import * as React from 'react'
4 | import { Request } from '../../reducers'
5 | import { Todo } from '../../types'
6 | import TodoItem from '../TodoItem/render'
7 |
8 | function todoItemHelper(
9 | todo: Todo,
10 | {
11 | updateRequest,
12 | deleteRequest,
13 | contentEditing = false,
14 | dueDateEditing = false,
15 | }: {
16 | updateRequest: Request
17 | deleteRequest: Request
18 | contentEditing?: boolean
19 | dueDateEditing?: boolean
20 | }
21 | ) {
22 | return (
23 |
37 | )
38 | }
39 |
40 | const typicalTodo: Todo = {
41 | id: 1,
42 | content: 'something to do',
43 | done: false,
44 | dueDate: '2017-09-30T07:32:08.591Z',
45 | createdAt: '2017-09-30T07:32:08.591Z',
46 | updatedAt: '2017-09-30T07:32:08.591Z',
47 | }
48 |
49 | const succeededRequest: Request = {
50 | requesting: false,
51 | error: null,
52 | }
53 |
54 | const errorRequest: Request = {
55 | requesting: false,
56 | error: new Error('error'),
57 | }
58 |
59 | const loadingRequest: Request = {
60 | requesting: true,
61 | error: null,
62 | }
63 |
64 | storiesOf('TodoItem', module)
65 | .add('typical', () =>
66 | todoItemHelper(typicalTodo, {
67 | updateRequest: succeededRequest,
68 | deleteRequest: succeededRequest,
69 | })
70 | )
71 | .add('while updating', () =>
72 | todoItemHelper(typicalTodo, {
73 | updateRequest: loadingRequest,
74 | deleteRequest: succeededRequest,
75 | })
76 | )
77 | .add('while deleting', () =>
78 | todoItemHelper(typicalTodo, {
79 | updateRequest: succeededRequest,
80 | deleteRequest: loadingRequest,
81 | })
82 | )
83 | .add('updating error', () =>
84 | todoItemHelper(typicalTodo, {
85 | updateRequest: errorRequest,
86 | deleteRequest: succeededRequest,
87 | })
88 | )
89 | .add('deleting error', () =>
90 | todoItemHelper(typicalTodo, {
91 | updateRequest: succeededRequest,
92 | deleteRequest: errorRequest,
93 | })
94 | )
95 | .add('content editing', () =>
96 | todoItemHelper(typicalTodo, {
97 | updateRequest: succeededRequest,
98 | deleteRequest: succeededRequest,
99 | contentEditing: true,
100 | })
101 | )
102 | .add('dueDate editing', () =>
103 | todoItemHelper(typicalTodo, {
104 | updateRequest: succeededRequest,
105 | deleteRequest: succeededRequest,
106 | dueDateEditing: true,
107 | })
108 | )
109 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/reducers/todos.ts:
--------------------------------------------------------------------------------
1 | import * as actions from '../actions'
2 |
3 | import * as _ from 'lodash'
4 | import { SortBy, SortOrder } from '../reducers/app'
5 | import { Todo } from '../types'
6 |
7 | export interface TodoMap {
8 | readonly [id: number]: Readonly
9 | }
10 |
11 | export interface TodosState {
12 | readonly byId: TodoMap
13 | readonly ids: number[]
14 | }
15 |
16 | export const initialTodosState: TodosState = {
17 | byId: {},
18 | ids: [],
19 | }
20 |
21 | function addTodoReceived(state: TodosState, action: actions.AddTodoReceived) {
22 | if (action.payload instanceof Error) {
23 | return state
24 | }
25 |
26 | const newTodo = action.payload.item
27 |
28 | return {
29 | ...state,
30 | byId: {
31 | ...state.byId,
32 | [newTodo.id]: newTodo,
33 | },
34 | ids: [...state.ids, newTodo.id],
35 | }
36 | }
37 |
38 | function updateTodoReceived(
39 | state: TodosState,
40 | action: actions.UpdateTodoReceived
41 | ) {
42 | if (action.payload instanceof Error) {
43 | return state
44 | }
45 |
46 | const updatedTodo = action.payload.item
47 |
48 | return {
49 | ...state,
50 | byId: {
51 | ...state.byId,
52 | [updatedTodo.id]: updatedTodo,
53 | },
54 | }
55 | }
56 |
57 | function toggleTodoDoneReceived(
58 | state: TodosState,
59 | action: actions.ToggleTodoDoneReceived
60 | ) {
61 | if (action.payload instanceof Error) {
62 | return state
63 | }
64 |
65 | const updatedTodo = action.payload.item
66 |
67 | return {
68 | ...state,
69 | byId: {
70 | ...state.byId,
71 | [updatedTodo.id]: updatedTodo,
72 | },
73 | }
74 | }
75 |
76 | function deleteTodoReceived(
77 | state: TodosState,
78 | action: actions.DeleteTodoReceived
79 | ) {
80 | if (action.payload instanceof Error) {
81 | return state
82 | }
83 |
84 | const { id } = action.payload.item
85 | const byId: TodoMap = _.omit(state.byId, [id])
86 | const ids = state.ids.filter(item => item !== id)
87 |
88 | return {
89 | ...state,
90 | byId,
91 | ids,
92 | }
93 | }
94 |
95 | export default function todosReducer(
96 | state: TodosState = initialTodosState,
97 | action: actions.Action
98 | ): TodosState {
99 | switch (action.type) {
100 | case actions.ADD_TODO_RECEIVED:
101 | return addTodoReceived(state, action)
102 | case actions.UPDATE_TODO_RECEIVED:
103 | return updateTodoReceived(state, action)
104 | case actions.TOGGLE_TODO_DONE_RECEIVED:
105 | return toggleTodoDoneReceived(state, action)
106 | case actions.DELETE_TODO_RECEIVED:
107 | return deleteTodoReceived(state, action)
108 | default:
109 | return state
110 | }
111 | }
112 |
113 | export function visibleTodos(
114 | state: TodosState,
115 | prop: SortBy,
116 | order: SortOrder,
117 | done: boolean
118 | ): number[] {
119 | // When done is required, return all todos including done.
120 | const ids = done ? state.ids : state.ids.filter(id => !state.byId[id].done)
121 |
122 | const time = (id: number) => new Date(state.byId[id][prop]).getTime()
123 | return _.orderBy(ids, time, [order])
124 | }
125 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
19 | # config.require_master_key = true
20 |
21 | # Disable serving static files from the `/public` folder by default since
22 | # Apache or NGINX already handles this.
23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
24 |
25 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
26 | # config.action_controller.asset_host = 'http://assets.example.com'
27 |
28 | # Specifies the header that your server uses for sending files.
29 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
30 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
31 |
32 | # Store uploaded files on the local file system (see config/storage.yml for options)
33 | config.active_storage.service = :local
34 |
35 | # Mount Action Cable outside main process or domain
36 | # config.action_cable.mount_path = nil
37 | # config.action_cable.url = 'wss://example.com/cable'
38 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
39 |
40 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
41 | # config.force_ssl = true
42 |
43 | # Use the lowest log level to ensure availability of diagnostic information
44 | # when problems arise.
45 | config.log_level = :debug
46 |
47 | # Prepend all log lines with the following tags.
48 | config.log_tags = [ :request_id ]
49 |
50 | # Use a different cache store in production.
51 | # config.cache_store = :mem_cache_store
52 |
53 | # Use a real queuing backend for Active Job (and separate queues per environment)
54 | # config.active_job.queue_adapter = :resque
55 | # config.active_job.queue_name_prefix = "webpacker_react_example_#{Rails.env}"
56 |
57 | config.action_mailer.perform_caching = false
58 |
59 | # Ignore bad email addresses and do not raise email delivery errors.
60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
61 | # config.action_mailer.raise_delivery_errors = false
62 |
63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
64 | # the I18n.default_locale when a translation cannot be found).
65 | config.i18n.fallbacks = true
66 |
67 | # Send deprecation notices to registered listeners.
68 | config.active_support.deprecation = :notify
69 |
70 | # Use default logging formatter so that PID and timestamp are not suppressed.
71 | config.log_formatter = ::Logger::Formatter.new
72 |
73 | # Use a different logger for distributed setups.
74 | # require 'syslog/logger'
75 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
76 |
77 | if ENV["RAILS_LOG_TO_STDOUT"].present?
78 | logger = ActiveSupport::Logger.new(STDOUT)
79 | logger.formatter = config.log_formatter
80 | config.logger = ActiveSupport::TaggedLogging.new(logger)
81 | end
82 |
83 | # Do not dump schema after migrations.
84 | config.active_record.dump_schema_after_migration = false
85 | end
86 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/reducers/app.ts:
--------------------------------------------------------------------------------
1 | import * as _ from 'lodash'
2 | import * as actions from '../actions'
3 | import { IdentifiableError } from '../types'
4 |
5 | export type SortBy = 'dueDate' | 'createdAt'
6 | export type SortOrder = 'asc' | 'desc'
7 |
8 | export interface Request {
9 | readonly requesting: boolean
10 | readonly error: Error | null
11 | }
12 |
13 | interface RequestTable {
14 | readonly [id: number]: Request | undefined
15 | }
16 |
17 | export const SINGLETON_ID = 0
18 | export interface AppState {
19 | readonly doneFilter: boolean
20 | readonly sortBy: SortBy
21 | readonly sortOrder: SortOrder
22 | readonly requests: {
23 | // If a request is for creation, which means id doesn't exist yet, the index of table should be 0.
24 | // In this case, RequestTable would be a singleton.
25 | // This convention increases code reuse between reducers below requests.
26 | readonly addTodo: RequestTable
27 | readonly updateTodo: RequestTable
28 | readonly toggleTodoDone: RequestTable
29 | readonly deleteTodo: RequestTable
30 | }
31 | }
32 |
33 | export const initialAppState: AppState = {
34 | doneFilter: false,
35 | sortBy: 'dueDate',
36 | sortOrder: 'desc',
37 | requests: {
38 | addTodo: {},
39 | updateTodo: {},
40 | toggleTodoDone: {},
41 | deleteTodo: {},
42 | },
43 | }
44 |
45 | function toggleDoneFilter(state: AppState) {
46 | return {
47 | ...state,
48 | doneFilter: !state.doneFilter,
49 | }
50 | }
51 |
52 | function selectOrder(state: AppState, action: actions.SelectOrder) {
53 | return {
54 | ...state,
55 | sortBy: action.payload.sortBy,
56 | sortOrder: action.payload.sortOrder,
57 | }
58 | }
59 |
60 | interface RequestedAction {
61 | payload: { requestId: number }
62 | }
63 | function handleRequested(
64 | target: keyof AppState['requests'],
65 | state: AppState,
66 | action: RequestedAction
67 | ) {
68 | return {
69 | ...state,
70 | requests: {
71 | ...state.requests,
72 | [target]: {
73 | ...state.requests[target],
74 | [action.payload.requestId]: {
75 | requesting: true,
76 | error: null,
77 | },
78 | },
79 | },
80 | }
81 | }
82 |
83 | interface ReceivedAction {
84 | payload: { requestId: number } | IdentifiableError
85 | }
86 | function handleReceived(
87 | target: keyof AppState['requests'],
88 | state: AppState,
89 | action: ReceivedAction
90 | ) {
91 | let table
92 | if (action.payload instanceof IdentifiableError) {
93 | table = {
94 | ...state.requests[target],
95 | [action.payload.targetId]: {
96 | requesting: false,
97 | error: action.payload,
98 | },
99 | }
100 | } else {
101 | table = _.omit(state.requests[target], [action.payload.requestId])
102 | }
103 |
104 | return {
105 | ...state,
106 | requests: {
107 | ...state.requests,
108 | [target]: table,
109 | },
110 | }
111 | }
112 |
113 | export default function appReducer(
114 | state: AppState = initialAppState,
115 | action: actions.Action
116 | ): AppState {
117 | switch (action.type) {
118 | case actions.TOGGLE_DONE_FILTER:
119 | return toggleDoneFilter(state)
120 | case actions.SELECT_ORDER:
121 | return selectOrder(state, action)
122 | case actions.ADD_TODO_REQUESTED:
123 | return handleRequested('addTodo', state, action)
124 | case actions.ADD_TODO_RECEIVED:
125 | return handleReceived('addTodo', state, action)
126 | case actions.UPDATE_TODO_REQUESTED:
127 | return handleRequested('updateTodo', state, action)
128 | case actions.UPDATE_TODO_RECEIVED:
129 | return handleReceived('updateTodo', state, action)
130 | case actions.TOGGLE_TODO_DONE_REQUESTED:
131 | return handleRequested('toggleTodoDone', state, action)
132 | case actions.TOGGLE_TODO_DONE_RECEIVED:
133 | return handleReceived('toggleTodoDone', state, action)
134 | case actions.DELETE_TODO_REQUESTED:
135 | return handleRequested('deleteTodo', state, action)
136 | case actions.DELETE_TODO_RECEIVED:
137 | return handleReceived('deleteTodo', state, action)
138 | default:
139 | return state
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/reducers/__test__/todo.spec.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert'
2 | import * as _ from 'lodash'
3 | import 'mocha'
4 | import * as actions from '../../actions'
5 | import appReducer, { SINGLETON_ID } from '../app'
6 | import todosReducer, { initialTodosState, TodosState } from '../todos'
7 |
8 | function todoItem() {
9 | return {
10 | id: 1,
11 | content: 'todo',
12 | done: false,
13 | dueDate: '2017-09-30T07:32:08.591Z',
14 | createdAt: '2017-09-30T07:32:08.591Z',
15 | updatedAt: '2017-09-30T07:32:08.591Z',
16 | }
17 | }
18 |
19 | describe('Todo reducer', () => {
20 | describe('AddTodoReceived Action', () => {
21 | it('should add an element to ids', () => {
22 | const newState = todosReducer(
23 | initialTodosState,
24 | actions.addTodoReceived({
25 | requestId: SINGLETON_ID,
26 | item: todoItem(),
27 | })
28 | )
29 | assert.deepEqual(newState.ids, [1])
30 | })
31 |
32 | it('should add an element to byId', () => {
33 | const newState = todosReducer(
34 | initialTodosState,
35 | actions.addTodoReceived({
36 | requestId: SINGLETON_ID,
37 | item: todoItem(),
38 | })
39 | )
40 | assert.deepEqual(newState.byId, { 1: todoItem() })
41 | })
42 | })
43 |
44 | describe('UpdateTodoReceived Action', () => {
45 | let state: TodosState
46 |
47 | beforeEach(() => {
48 | state = {
49 | ...initialTodosState,
50 | ids: [1],
51 | byId: {
52 | 1: todoItem(),
53 | },
54 | }
55 | })
56 |
57 | it('should keep ids', () => {
58 | const newState = todosReducer(
59 | state,
60 | actions.updateTodoReceived({
61 | requestId: 1,
62 | item: {
63 | ...todoItem(),
64 | done: true,
65 | },
66 | })
67 | )
68 | assert.deepEqual(newState.ids, [1])
69 | })
70 |
71 | it('should update item', () => {
72 | const newState = todosReducer(
73 | state,
74 | actions.updateTodoReceived({
75 | requestId: 1,
76 | item: {
77 | ...todoItem(),
78 | done: true,
79 | },
80 | })
81 | )
82 | assert.equal(newState.byId[1].done, true)
83 | })
84 | })
85 |
86 | describe('ToggleTodoDoneReceived Action', () => {
87 | let state: TodosState
88 |
89 | beforeEach(() => {
90 | state = {
91 | ...initialTodosState,
92 | ids: [1],
93 | byId: {
94 | 1: todoItem(),
95 | },
96 | }
97 | })
98 |
99 | it('should keep ids', () => {
100 | const newState = todosReducer(
101 | state,
102 | actions.toggleTodoDoneReceived({
103 | requestId: 1,
104 | item: {
105 | ...todoItem(),
106 | done: true,
107 | },
108 | })
109 | )
110 | assert.deepEqual(newState.ids, [1])
111 | })
112 |
113 | it('should update item', () => {
114 | const newState = todosReducer(
115 | state,
116 | actions.toggleTodoDoneReceived({
117 | requestId: 1,
118 | item: {
119 | ...todoItem(),
120 | done: true,
121 | },
122 | })
123 | )
124 | assert.equal(newState.byId[1].done, true)
125 | })
126 | })
127 |
128 | describe('DeleteTodoReceived Action', () => {
129 | let state: TodosState
130 |
131 | beforeEach(() => {
132 | state = {
133 | ...initialTodosState,
134 | ids: [1],
135 | byId: {
136 | 1: todoItem(),
137 | },
138 | }
139 | })
140 |
141 | it('should delete an item from ids', () => {
142 | const newState = todosReducer(
143 | state,
144 | actions.deleteTodoReceived({
145 | requestId: 1,
146 | item: { id: 1 },
147 | })
148 | )
149 | assert.deepEqual(newState.ids, [])
150 | })
151 |
152 | it('should delete an item from byId', () => {
153 | const newState = todosReducer(
154 | state,
155 | actions.deleteTodoReceived({
156 | requestId: 1,
157 | item: { id: 1 },
158 | })
159 | )
160 | assert.deepEqual(newState.byId, {})
161 | })
162 | })
163 | })
164 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/components/TodoItem/render.tsx:
--------------------------------------------------------------------------------
1 | import * as moment from 'moment'
2 | import * as React from 'react'
3 | import * as DateTime from 'react-datetime/DateTime'
4 | import { Request } from '../../reducers'
5 | import { Todo } from '../../types'
6 |
7 | import EditButton from '../EditButton'
8 | import * as styles from './styles.module.scss'
9 |
10 | interface ContentProps {
11 | todo: Todo
12 | disabled: boolean
13 | editing: boolean
14 | onCheckboxChange: () => void
15 | onEditClick: () => void
16 | onInputBlur: (ev: React.FocusEvent, todo: Todo) => void
17 | }
18 | function Content({
19 | todo,
20 | disabled,
21 | editing,
22 | onCheckboxChange,
23 | onEditClick,
24 | onInputBlur,
25 | }: ContentProps) {
26 | if (editing) {
27 | return (
28 | onInputBlur(ev, todo)}
33 | />
34 | )
35 | }
36 |
37 | return (
38 |
39 |
48 |
53 |
54 | )
55 | }
56 |
57 | interface DueDateProps {
58 | todo: Todo
59 | disabled: boolean
60 | editing: boolean
61 | onEditClick: () => void
62 | onInputBlur: (
63 | ev: React.FocusEvent | moment.Moment | string,
64 | todo: Todo
65 | ) => void
66 | }
67 | function DueDate({
68 | todo,
69 | disabled,
70 | editing,
71 | onEditClick,
72 | onInputBlur,
73 | }: DueDateProps) {
74 | if (editing) {
75 | return (
76 | onInputBlur(ev, todo)}
80 | />
81 | )
82 | }
83 |
84 | return (
85 |
86 | {moment(todo.dueDate)
87 | .local()
88 | .toString()}
89 |
94 |
95 | )
96 | }
97 |
98 | export interface Props {
99 | id: number
100 | todo: Todo
101 | contentEditing: boolean
102 | dueDateEditing: boolean
103 | updateRequest: Request
104 | deleteRequest: Request
105 | onCheckboxChange: () => void
106 | onContentClick: () => void
107 | onContentBlur: (ev: React.FocusEvent, todo: Todo) => void
108 | onDueDateClick: () => void
109 | onDueDateBlur: (
110 | ev: React.FocusEvent | moment.Moment | string,
111 | todo: Todo
112 | ) => void
113 | onDestroyClick: () => void
114 | }
115 |
116 | export default function TodoItem({
117 | todo,
118 | contentEditing,
119 | dueDateEditing,
120 | updateRequest,
121 | deleteRequest,
122 | onCheckboxChange,
123 | onContentClick,
124 | onContentBlur,
125 | onDueDateClick,
126 | onDueDateBlur,
127 | onDestroyClick,
128 | }: Props) {
129 | return (
130 |
131 | |
132 |
140 | |
141 |
142 |
149 | |
150 |
151 |
158 |
159 | {updateRequest.error && (
160 | Update todo failed
161 | )}
162 | {deleteRequest.error && (
163 | Delete todo failed
164 | )}
165 | |
166 |
167 | )
168 | }
169 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/react-datetime.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * https://github.com/YouCanBookMe/react-datetime
3 | */
4 |
5 | .rdt {
6 | position: relative;
7 | }
8 | .rdtPicker {
9 | display: none;
10 | position: absolute;
11 | width: 250px;
12 | padding: 4px;
13 | margin-top: 1px;
14 | z-index: 99999 !important;
15 | background: #fff;
16 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
17 | border: 1px solid #f9f9f9;
18 | }
19 | .rdtOpen .rdtPicker {
20 | display: block;
21 | }
22 | .rdtStatic .rdtPicker {
23 | box-shadow: none;
24 | position: static;
25 | }
26 |
27 | .rdtPicker .rdtTimeToggle {
28 | text-align: center;
29 | }
30 |
31 | .rdtPicker table {
32 | width: 100%;
33 | margin: 0;
34 | }
35 | .rdtPicker td,
36 | .rdtPicker th {
37 | text-align: center;
38 | height: 28px;
39 | }
40 | .rdtPicker td {
41 | cursor: pointer;
42 | }
43 | .rdtPicker td.rdtDay:hover,
44 | .rdtPicker td.rdtHour:hover,
45 | .rdtPicker td.rdtMinute:hover,
46 | .rdtPicker td.rdtSecond:hover,
47 | .rdtPicker .rdtTimeToggle:hover {
48 | background: #eeeeee;
49 | cursor: pointer;
50 | }
51 | .rdtPicker td.rdtOld,
52 | .rdtPicker td.rdtNew {
53 | color: #999999;
54 | }
55 | .rdtPicker td.rdtToday {
56 | position: relative;
57 | }
58 | .rdtPicker td.rdtToday:before {
59 | content: '';
60 | display: inline-block;
61 | border-left: 7px solid transparent;
62 | border-bottom: 7px solid #428bca;
63 | border-top-color: rgba(0, 0, 0, 0.2);
64 | position: absolute;
65 | bottom: 4px;
66 | right: 4px;
67 | }
68 | .rdtPicker td.rdtActive,
69 | .rdtPicker td.rdtActive:hover {
70 | background-color: #428bca;
71 | color: #fff;
72 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
73 | }
74 | .rdtPicker td.rdtActive.rdtToday:before {
75 | border-bottom-color: #fff;
76 | }
77 | .rdtPicker td.rdtDisabled,
78 | .rdtPicker td.rdtDisabled:hover {
79 | background: none;
80 | color: #999999;
81 | cursor: not-allowed;
82 | }
83 |
84 | .rdtPicker td span.rdtOld {
85 | color: #999999;
86 | }
87 | .rdtPicker td span.rdtDisabled,
88 | .rdtPicker td span.rdtDisabled:hover {
89 | background: none;
90 | color: #999999;
91 | cursor: not-allowed;
92 | }
93 | .rdtPicker th {
94 | border-bottom: 1px solid #f9f9f9;
95 | }
96 | .rdtPicker .dow {
97 | width: 14.2857%;
98 | border-bottom: none;
99 | }
100 | .rdtPicker th.rdtSwitch {
101 | width: 100px;
102 | }
103 | .rdtPicker th.rdtNext,
104 | .rdtPicker th.rdtPrev {
105 | font-size: 21px;
106 | vertical-align: top;
107 | }
108 |
109 | .rdtPrev span,
110 | .rdtNext span {
111 | display: block;
112 | -webkit-touch-callout: none; /* iOS Safari */
113 | -webkit-user-select: none; /* Chrome/Safari/Opera */
114 | -khtml-user-select: none; /* Konqueror */
115 | -moz-user-select: none; /* Firefox */
116 | -ms-user-select: none; /* Internet Explorer/Edge */
117 | user-select: none;
118 | }
119 |
120 | .rdtPicker th.rdtDisabled,
121 | .rdtPicker th.rdtDisabled:hover {
122 | background: none;
123 | color: #999999;
124 | cursor: not-allowed;
125 | }
126 | .rdtPicker thead tr:first-child th {
127 | cursor: pointer;
128 | }
129 | .rdtPicker thead tr:first-child th:hover {
130 | background: #eeeeee;
131 | }
132 |
133 | .rdtPicker tfoot {
134 | border-top: 1px solid #f9f9f9;
135 | }
136 |
137 | .rdtPicker button {
138 | border: none;
139 | background: none;
140 | cursor: pointer;
141 | }
142 | .rdtPicker button:hover {
143 | background-color: #eee;
144 | }
145 |
146 | .rdtPicker thead button {
147 | width: 100%;
148 | height: 100%;
149 | }
150 |
151 | td.rdtMonth,
152 | td.rdtYear {
153 | height: 50px;
154 | width: 25%;
155 | cursor: pointer;
156 | }
157 | td.rdtMonth:hover,
158 | td.rdtYear:hover {
159 | background: #eee;
160 | }
161 |
162 | .rdtCounters {
163 | display: inline-block;
164 | }
165 |
166 | .rdtCounters > div {
167 | float: left;
168 | }
169 |
170 | .rdtCounter {
171 | height: 100px;
172 | }
173 |
174 | .rdtCounter {
175 | width: 40px;
176 | }
177 |
178 | .rdtCounterSeparator {
179 | line-height: 100px;
180 | }
181 |
182 | .rdtCounter .rdtBtn {
183 | height: 40%;
184 | line-height: 40px;
185 | cursor: pointer;
186 | display: block;
187 |
188 | -webkit-touch-callout: none; /* iOS Safari */
189 | -webkit-user-select: none; /* Chrome/Safari/Opera */
190 | -khtml-user-select: none; /* Konqueror */
191 | -moz-user-select: none; /* Firefox */
192 | -ms-user-select: none; /* Internet Explorer/Edge */
193 | user-select: none;
194 | }
195 | .rdtCounter .rdtBtn:hover {
196 | background: #eee;
197 | }
198 | .rdtCounter .rdtCount {
199 | height: 20%;
200 | font-size: 1.2em;
201 | }
202 |
203 | .rdtMilli {
204 | vertical-align: middle;
205 | padding-left: 8px;
206 | width: 48px;
207 | }
208 |
209 | .rdtMilli input {
210 | width: 100%;
211 | font-size: 1.2em;
212 | margin-top: 37px;
213 | }
214 |
--------------------------------------------------------------------------------
/app/assets/javascripts/todos/actions/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Action definition follows Martin Hochel's method.
3 | * see details on https://medium.com/@martin_hotell/redux-typescript-typed-actions-with-less-keystrokes-d984063901d
4 | */
5 |
6 | import { SINGLETON_ID, SortBy, SortOrder } from '../reducers/app'
7 | import { IdentifiableError, Todo } from '../types'
8 |
9 | interface Request {
10 | requestId: number
11 | item: T
12 | }
13 |
14 | export const ADD_TODO_REQUESTED = 'ADD_TODO:REQUESTED'
15 | export const ADD_TODO_RECEIVED = 'ADD_TODO:RECEIVED'
16 | export const UPDATE_TODO_REQUESTED = 'UPDATE_TODO:REQUESTED'
17 | export const UPDATE_TODO_RECEIVED = 'UPDATE_TODO:RECEIVED'
18 | export const TOGGLE_TODO_DONE_REQUESTED = 'TOGGLE_TODO_DONE:REQUESTED'
19 | export const TOGGLE_TODO_DONE_RECEIVED = 'TOGGLE_TODO_DONE:RECEIVED'
20 | export const DELETE_TODO_REQUESTED = 'DELETE_TODO:REQUESTED'
21 | export const DELETE_TODO_RECEIVED = 'DELETE_TODO:RECEIVED'
22 | export const TOGGLE_DONE_FILTER = 'TOGGLE_DONE_FILTER'
23 | export const SELECT_ORDER = 'SELECT_ORDER'
24 |
25 | export function addTodoRequested(content: string, dueDate: string) {
26 | return {
27 | type: ADD_TODO_REQUESTED as typeof ADD_TODO_REQUESTED,
28 | payload: {
29 | requestId: SINGLETON_ID,
30 | item: { content, dueDate },
31 | },
32 | }
33 | }
34 |
35 | export function addTodoReceived(payload: Request | IdentifiableError) {
36 | return {
37 | type: ADD_TODO_RECEIVED as typeof ADD_TODO_RECEIVED,
38 | payload,
39 | }
40 | }
41 |
42 | export function updateTodoRequested(
43 | id: number,
44 | content: string,
45 | dueDate: string
46 | ) {
47 | return {
48 | type: UPDATE_TODO_REQUESTED as typeof UPDATE_TODO_REQUESTED,
49 | payload: {
50 | requestId: id,
51 | item: { id, content, dueDate },
52 | },
53 | }
54 | }
55 |
56 | export function updateTodoReceived(payload: Request | IdentifiableError) {
57 | return {
58 | type: UPDATE_TODO_RECEIVED as typeof UPDATE_TODO_RECEIVED,
59 | payload,
60 | }
61 | }
62 |
63 | export function toggleTodoDoneRequested(id: number) {
64 | return {
65 | type: TOGGLE_TODO_DONE_REQUESTED as typeof TOGGLE_TODO_DONE_REQUESTED,
66 | payload: {
67 | requestId: id,
68 | item: { id },
69 | },
70 | }
71 | }
72 |
73 | export function toggleTodoDoneReceived(
74 | payload: Request | IdentifiableError
75 | ) {
76 | return {
77 | type: TOGGLE_TODO_DONE_RECEIVED as typeof TOGGLE_TODO_DONE_RECEIVED,
78 | payload,
79 | }
80 | }
81 |
82 | export function deleteTodoRequested(id: number) {
83 | return {
84 | type: DELETE_TODO_REQUESTED as typeof DELETE_TODO_REQUESTED,
85 | payload: {
86 | requestId: id,
87 | item: { id },
88 | },
89 | }
90 | }
91 |
92 | export function deleteTodoReceived(
93 | payload: Request<{ id: number }> | IdentifiableError
94 | ) {
95 | return {
96 | type: DELETE_TODO_RECEIVED as typeof DELETE_TODO_RECEIVED,
97 | payload,
98 | }
99 | }
100 |
101 | export function toggleDoneFilter() {
102 | return {
103 | type: TOGGLE_DONE_FILTER as typeof TOGGLE_DONE_FILTER,
104 | }
105 | }
106 |
107 | export function selectOrder(sortBy: SortBy, sortOrder: SortOrder) {
108 | return {
109 | type: SELECT_ORDER as typeof SELECT_ORDER,
110 | payload: { sortBy, sortOrder },
111 | }
112 | }
113 |
114 | function getReturnType(f: (...args: any[]) => R): R {
115 | return null!
116 | }
117 |
118 | const pseudoAddTodoRequested = getReturnType(addTodoRequested)
119 | const pseudoAddTodoReceived = getReturnType(addTodoReceived)
120 | const pseudoUpdateTodoRequested = getReturnType(updateTodoRequested)
121 | const pseudoUpdateTodoReceived = getReturnType(updateTodoReceived)
122 | const pseudoToggleTodoDoneRequested = getReturnType(toggleTodoDoneRequested)
123 | const pseudoToggleTodoDoneReceived = getReturnType(toggleTodoDoneReceived)
124 | const pseudoDeleteTodoRequested = getReturnType(deleteTodoRequested)
125 | const pseudoDeleteTodoReceived = getReturnType(deleteTodoReceived)
126 | const pseudoToggleDoneFilter = getReturnType(toggleDoneFilter)
127 | const pseudoSelectOrder = getReturnType(selectOrder)
128 | export type AddTodoRequested = typeof pseudoAddTodoRequested
129 | export type AddTodoReceived = typeof pseudoAddTodoReceived
130 | export type UpdateTodoRequested = typeof pseudoUpdateTodoRequested
131 | export type UpdateTodoReceived = typeof pseudoUpdateTodoReceived
132 | export type ToggleTodoDoneRequested = typeof pseudoToggleTodoDoneRequested
133 | export type ToggleTodoDoneReceived = typeof pseudoToggleTodoDoneReceived
134 | export type DeleteTodoRequested = typeof pseudoDeleteTodoRequested
135 | export type DeleteTodoReceived = typeof pseudoDeleteTodoReceived
136 | export type ToggleDoneFilter = typeof pseudoToggleDoneFilter
137 | export type SelectOrder = typeof pseudoSelectOrder
138 |
139 | export type Action =
140 | | AddTodoRequested
141 | | AddTodoReceived
142 | | UpdateTodoRequested
143 | | UpdateTodoReceived
144 | | ToggleTodoDoneRequested
145 | | ToggleTodoDoneReceived
146 | | DeleteTodoRequested
147 | | DeleteTodoReceived
148 | | ToggleDoneFilter
149 | | SelectOrder
150 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.2.0)
5 | actionpack (= 5.2.0)
6 | nio4r (~> 2.0)
7 | websocket-driver (>= 0.6.1)
8 | actionmailer (5.2.0)
9 | actionpack (= 5.2.0)
10 | actionview (= 5.2.0)
11 | activejob (= 5.2.0)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 2.0)
14 | actionpack (5.2.0)
15 | actionview (= 5.2.0)
16 | activesupport (= 5.2.0)
17 | rack (~> 2.0)
18 | rack-test (>= 0.6.3)
19 | rails-dom-testing (~> 2.0)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.2.0)
22 | activesupport (= 5.2.0)
23 | builder (~> 3.1)
24 | erubi (~> 1.4)
25 | rails-dom-testing (~> 2.0)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
27 | activejob (5.2.0)
28 | activesupport (= 5.2.0)
29 | globalid (>= 0.3.6)
30 | activemodel (5.2.0)
31 | activesupport (= 5.2.0)
32 | activerecord (5.2.0)
33 | activemodel (= 5.2.0)
34 | activesupport (= 5.2.0)
35 | arel (>= 9.0)
36 | activestorage (5.2.0)
37 | actionpack (= 5.2.0)
38 | activerecord (= 5.2.0)
39 | marcel (~> 0.3.1)
40 | activesupport (5.2.0)
41 | concurrent-ruby (~> 1.0, >= 1.0.2)
42 | i18n (>= 0.7, < 2)
43 | minitest (~> 5.1)
44 | tzinfo (~> 1.1)
45 | addressable (2.5.1)
46 | public_suffix (~> 2.0, >= 2.0.2)
47 | arel (9.0.0)
48 | ast (2.4.0)
49 | bindex (0.5.0)
50 | bootsnap (1.3.1)
51 | msgpack (~> 1.0)
52 | builder (3.2.3)
53 | byebug (9.0.6)
54 | capybara (2.14.3)
55 | addressable
56 | mime-types (>= 1.16)
57 | nokogiri (>= 1.3.3)
58 | rack (>= 1.0.0)
59 | rack-test (>= 0.5.4)
60 | xpath (~> 2.0)
61 | childprocess (0.7.0)
62 | ffi (~> 1.0, >= 1.0.11)
63 | concurrent-ruby (1.0.5)
64 | crass (1.0.4)
65 | erubi (1.7.1)
66 | ffi (1.9.18)
67 | globalid (0.4.1)
68 | activesupport (>= 4.2.0)
69 | i18n (1.0.1)
70 | concurrent-ruby (~> 1.0)
71 | jaro_winkler (1.5.1)
72 | jbuilder (2.7.0)
73 | activesupport (>= 4.2.0)
74 | multi_json (>= 1.2)
75 | listen (3.1.5)
76 | rb-fsevent (~> 0.9, >= 0.9.4)
77 | rb-inotify (~> 0.9, >= 0.9.7)
78 | ruby_dep (~> 1.2)
79 | loofah (2.2.2)
80 | crass (~> 1.0.2)
81 | nokogiri (>= 1.5.9)
82 | mail (2.7.0)
83 | mini_mime (>= 0.1.1)
84 | marcel (0.3.2)
85 | mimemagic (~> 0.3.2)
86 | method_source (0.9.0)
87 | mime-types (3.1)
88 | mime-types-data (~> 3.2015)
89 | mime-types-data (3.2016.0521)
90 | mimemagic (0.3.2)
91 | mini_mime (1.0.0)
92 | mini_portile2 (2.3.0)
93 | minitest (5.11.3)
94 | msgpack (1.2.4)
95 | multi_json (1.12.2)
96 | nio4r (2.3.1)
97 | nokogiri (1.8.4)
98 | mini_portile2 (~> 2.3.0)
99 | parallel (1.12.1)
100 | parser (2.5.1.2)
101 | ast (~> 2.4.0)
102 | powerpack (0.1.2)
103 | public_suffix (2.0.5)
104 | puma (3.9.1)
105 | rack (2.0.5)
106 | rack-proxy (0.6.4)
107 | rack
108 | rack-test (1.1.0)
109 | rack (>= 1.0, < 3)
110 | rails (5.2.0)
111 | actioncable (= 5.2.0)
112 | actionmailer (= 5.2.0)
113 | actionpack (= 5.2.0)
114 | actionview (= 5.2.0)
115 | activejob (= 5.2.0)
116 | activemodel (= 5.2.0)
117 | activerecord (= 5.2.0)
118 | activestorage (= 5.2.0)
119 | activesupport (= 5.2.0)
120 | bundler (>= 1.3.0)
121 | railties (= 5.2.0)
122 | sprockets-rails (>= 2.0.0)
123 | rails-dom-testing (2.0.3)
124 | activesupport (>= 4.2.0)
125 | nokogiri (>= 1.6)
126 | rails-html-sanitizer (1.0.4)
127 | loofah (~> 2.2, >= 2.2.2)
128 | railties (5.2.0)
129 | actionpack (= 5.2.0)
130 | activesupport (= 5.2.0)
131 | method_source
132 | rake (>= 0.8.7)
133 | thor (>= 0.18.1, < 2.0)
134 | rainbow (3.0.0)
135 | rake (12.3.1)
136 | rb-fsevent (0.9.8)
137 | rb-inotify (0.9.9)
138 | ffi (~> 1.0)
139 | rubocop (0.58.1)
140 | jaro_winkler (~> 1.5.1)
141 | parallel (~> 1.10)
142 | parser (>= 2.5, != 2.5.1.1)
143 | powerpack (~> 0.1)
144 | rainbow (>= 2.2.2, < 4.0)
145 | ruby-progressbar (~> 1.7)
146 | unicode-display_width (~> 1.0, >= 1.0.1)
147 | ruby-progressbar (1.9.0)
148 | ruby_dep (1.5.0)
149 | rubyzip (1.2.1)
150 | selenium-webdriver (3.4.3)
151 | childprocess (~> 0.5)
152 | rubyzip (~> 1.0)
153 | spring (2.0.2)
154 | activesupport (>= 4.2)
155 | spring-watcher-listen (2.0.1)
156 | listen (>= 2.7, < 4.0)
157 | spring (>= 1.2, < 3.0)
158 | sprockets (3.7.2)
159 | concurrent-ruby (~> 1.0)
160 | rack (> 1, < 3)
161 | sprockets-rails (3.2.1)
162 | actionpack (>= 4.0)
163 | activesupport (>= 4.0)
164 | sprockets (>= 3.0.0)
165 | sqlite3 (1.3.13)
166 | thor (0.20.0)
167 | thread_safe (0.3.6)
168 | tzinfo (1.2.5)
169 | thread_safe (~> 0.1)
170 | unicode-display_width (1.4.0)
171 | web-console (3.5.1)
172 | actionview (>= 5.0)
173 | activemodel (>= 5.0)
174 | bindex (>= 0.4.0)
175 | railties (>= 5.0)
176 | webpacker (3.5.5)
177 | activesupport (>= 4.2)
178 | rack-proxy (>= 0.6.1)
179 | railties (>= 4.2)
180 | websocket-driver (0.7.0)
181 | websocket-extensions (>= 0.1.0)
182 | websocket-extensions (0.1.3)
183 | xpath (2.1.0)
184 | nokogiri (~> 1.3)
185 |
186 | PLATFORMS
187 | ruby
188 |
189 | DEPENDENCIES
190 | bootsnap
191 | byebug
192 | capybara (~> 2.13)
193 | jbuilder (~> 2.5)
194 | listen (>= 3.0.5, < 3.2)
195 | puma (~> 3.7)
196 | rails (~> 5.2.0)
197 | rubocop
198 | selenium-webdriver
199 | spring
200 | spring-watcher-listen (~> 2.0.0)
201 | sqlite3
202 | tzinfo-data
203 | web-console (>= 3.3.0)
204 | webpacker
205 |
206 | BUNDLED WITH
207 | 1.16.1
208 |
--------------------------------------------------------------------------------