├── index.js ├── 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 │ └── user_test.rb ├── controllers │ ├── .keep │ └── api │ │ └── users_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ └── users.yml ├── integration │ └── .keep └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── trello.png │ │ └── trello_vector.svg │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── api │ │ │ └── users.coffee │ │ ├── cable.js │ │ └── application.js │ ├── stylesheets │ │ ├── board_index.scss │ │ ├── api │ │ │ └── users.scss │ │ ├── index.scss │ │ ├── board_show.scss │ │ ├── application.css.scss │ │ └── header.scss │ └── config │ │ └── manifest.js ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── list.rb │ ├── card.rb │ ├── comment.rb │ ├── board_share.rb │ ├── board.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── users_controller.rb │ │ ├── board_shares_controller.rb │ │ ├── cards_controller.rb │ │ ├── lists_controller.rb │ │ ├── sessions_controller.rb │ │ ├── boards_controller.rb │ │ └── moves_controller.rb │ └── application_controller.rb ├── views │ ├── api │ │ ├── board_shares │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ │ ├── users │ │ │ ├── error.json.jbuilder │ │ │ ├── _user.json.jbuilder │ │ │ ├── show.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── lists │ │ │ └── show.json.jbuilder │ │ ├── cards │ │ │ └── show.json.jbuilder │ │ └── boards │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── api │ │ └── users_helper.rb │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── .babelrc ├── frontend ├── trello.png ├── actions │ ├── hover_actions.js │ ├── user_actions.js │ ├── board_share_actions.js │ ├── list_actions.js │ ├── session_actions.js │ ├── board_actions.js │ └── card_actions.js ├── store │ └── store.js ├── reducers │ ├── user_reducer.js │ ├── shared_board_reducer.js │ ├── root_reducer.js │ ├── async_status_sample_reducer.js │ ├── session_reducer.js │ ├── board_reducer.js │ ├── hover_reducer.js │ ├── card_reducer.js │ └── list_reducer.js ├── components │ ├── root.jsx │ ├── app.jsx │ ├── greeting.jsx │ ├── head │ │ ├── board_menu_dropdown.jsx │ │ ├── user_menu.jsx │ │ ├── create_board_dropdown.jsx │ │ ├── header.jsx │ │ └── board_sharing_dropdown.jsx │ ├── list_edit_modal.jsx │ ├── board_index.jsx │ ├── card_edit_modal.jsx │ ├── session_form.jsx │ ├── board_show.jsx │ ├── card.jsx │ └── list.jsx ├── entry.jsx └── util │ ├── route_util.jsx │ └── session_api_util.js ├── docs ├── wireframes │ ├── BoardIndex.png │ ├── BoardShow.png │ ├── CardModal.png │ └── QuickBoardLanding.png ├── images │ ├── mello_screenshot.png │ ├── trello_screenshot.png │ ├── board_index_screenshot.png │ └── board_show_screenshot.png ├── sample-state-2.md ├── api-endpoints.md ├── component-hierarchy.md ├── README.md └── schema.md ├── db ├── migrate │ ├── 20170809042234_add_ord_index.rb │ ├── 20170618202748_change_to_title.rb │ ├── 20170809043134_remove_index_test.rb │ ├── 20170808034343_changecol.rb │ ├── 20170808034700_floatify.rb │ ├── 20170619174535_add_name_to_users.rb │ ├── 20170616153144_make_username_unique.rb │ ├── 20170627234511_rename_order.rb │ ├── 20170616153349_make_username_unique_on_index.rb │ ├── 20170618160843_create_list.rb │ ├── 20170616151458_create_users.rb │ ├── 20170618160517_create_board.rb │ ├── 20170618183453_create_board_share.rb │ ├── 20170618161818_create_card.rb │ └── 20170618161744_create_comment.rb ├── seeds.rb └── schema.rb ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── initializers │ ├── session_store.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── new_framework_defaults.rb ├── application.rb ├── routes.rb ├── locales │ └── en.yml ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── puma.rb └── database.yml ├── config.ru ├── Rakefile ├── .gitignore ├── webpack.config.js ├── package.json ├── Gemfile ├── README.md └── Gemfile.lock /index.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/board_index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/api/board_shares/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/api/board_shares/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | 4 | } 5 | -------------------------------------------------------------------------------- /app/views/api/users/error.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.errors @errors 2 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /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/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.extract! user, :id, :username 3 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.partial! 'api/users/user', user: @user 3 | -------------------------------------------------------------------------------- /frontend/trello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/frontend/trello.png -------------------------------------------------------------------------------- /app/assets/images/trello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/app/assets/images/trello.png -------------------------------------------------------------------------------- /docs/wireframes/BoardIndex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/wireframes/BoardIndex.png -------------------------------------------------------------------------------- /docs/wireframes/BoardShow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/wireframes/BoardShow.png -------------------------------------------------------------------------------- /docs/wireframes/CardModal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/wireframes/CardModal.png -------------------------------------------------------------------------------- /docs/images/mello_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/images/mello_screenshot.png -------------------------------------------------------------------------------- /docs/images/trello_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/images/trello_screenshot.png -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /docs/images/board_index_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/images/board_index_screenshot.png -------------------------------------------------------------------------------- /docs/images/board_show_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/images/board_show_screenshot.png -------------------------------------------------------------------------------- /docs/wireframes/QuickBoardLanding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZackingIt/Mello/HEAD/docs/wireframes/QuickBoardLanding.png -------------------------------------------------------------------------------- /db/migrate/20170809042234_add_ord_index.rb: -------------------------------------------------------------------------------- 1 | class AddOrdIndex < ActiveRecord::Migration[5.0] 2 | add_index :cards, :ord 3 | end 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /db/migrate/20170618202748_change_to_title.rb: -------------------------------------------------------------------------------- 1 | class ChangeToTitle < ActiveRecord::Migration[5.0] 2 | def change 3 | 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170809043134_remove_index_test.rb: -------------------------------------------------------------------------------- 1 | class RemoveIndexTest < ActiveRecord::Migration[5.0] 2 | remove_index :cards, :ord 3 | end 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170808034343_changecol.rb: -------------------------------------------------------------------------------- 1 | class Changecol < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column(:cards, :ord, :float) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /db/migrate/20170808034700_floatify.rb: -------------------------------------------------------------------------------- 1 | class Floatify < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column(:cards, :ord, :float) 4 | end 5 | 6 | end 7 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /db/migrate/20170619174535_add_name_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddNameToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | @current_user = current_user 4 | render :root 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_QuickBoard_session' 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170616153144_make_username_unique.rb: -------------------------------------------------------------------------------- 1 | class MakeUsernameUnique < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :users, :username, :string, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/lists/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! :list do 2 | 3 | json.id @list.id 4 | json.board_id @list.board_id 5 | json.title @list.title 6 | json.ord @list.ord 7 | json.cardIds @list.cards.map(&:id) 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20170627234511_rename_order.rb: -------------------------------------------------------------------------------- 1 | class RenameOrder < ActiveRecord::Migration[5.0] 2 | def change 3 | 4 | rename_column :cards, :order, :ord 5 | rename_column :lists, :order, :ord 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /db/migrate/20170616153349_make_username_unique_on_index.rb: -------------------------------------------------------------------------------- 1 | class MakeUsernameUniqueOnIndex < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_index :users, :username 4 | add_index :users, :username, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/views/api/cards/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.set! :card do 3 | json.id @card.id 4 | json.list_id @card.list_id 5 | json.ord @card.ord 6 | json.body @card.body 7 | json.due_date @card.due_date 8 | json.completed @card.completed 9 | end 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.users {} 2 | json.set! :users do 3 | @users.each do |user| 4 | json.set! user.id do 5 | json.id user.id 6 | json.username user.username 7 | json.boardIds Board.where(author_id: user.id).map{|board| board.id} 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20170618160843_create_list.rb: -------------------------------------------------------------------------------- 1 | class CreateList < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :lists do |t| 4 | t.integer :board_id, null: false 5 | t.string :name, null: false 6 | t.integer :ord, null: false 7 | t.timestamps 8 | end 9 | add_index :lists, :board_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170616151458_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :password_digest, null: false 6 | t.string :session_token 7 | t.timestamps 8 | end 9 | add_index :users, :username 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170618160517_create_board.rb: -------------------------------------------------------------------------------- 1 | class CreateBoard < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :boards do |t| 4 | t.integer :author_id, null: false 5 | t.string :name, null: false 6 | t.boolean :privacy_status, default: true 7 | t.timestamps 8 | end 9 | add_index :boards, :author_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical ord. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170618183453_create_board_share.rb: -------------------------------------------------------------------------------- 1 | class CreateBoardShare < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :board_shares do |t| 4 | t.integer :board_id, null: false 5 | t.integer :user_id, null: false 6 | t.timestamps 7 | end 8 | add_index :board_shares, :board_id 9 | add_index :board_shares, :user_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /frontend/actions/hover_actions.js: -------------------------------------------------------------------------------- 1 | export const CREATE_DROPZONE = "CREATE_DROPZONE"; 2 | 3 | export const createDropZone = (dropParams) => { 4 | return { 5 | type: CREATE_DROPZONE, 6 | response: dropParams, 7 | }; 8 | }; 9 | 10 | 11 | export const generateDropZone = (dropParams) => { 12 | return (dispatch) => { 13 | dispatch(createDropZone(dropParams)); 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /db/migrate/20170618161818_create_card.rb: -------------------------------------------------------------------------------- 1 | class CreateCard < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :cards do |t| 4 | t.integer :list_id, null: false 5 | t.integer :ord, null: false 6 | t.string :body, null: false 7 | t.date :due_date 8 | t.boolean :completed, default: false 9 | end 10 | add_index :cards, :list_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170618161744_create_comment.rb: -------------------------------------------------------------------------------- 1 | class CreateComment < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :comments do |t| 4 | t.integer :author_id, null: false 5 | t.integer :card_id, null: false 6 | t.string :body, null: false 7 | t.timestamps 8 | end 9 | add_index :comments, :author_id 10 | add_index :comments, :card_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | // import logger from 'redux-logger'; 3 | import thunk from 'redux-thunk'; 4 | import rootReducer from '../reducers/root_reducer'; 5 | 6 | const configureStore = (preloadedState = {}) => ( 7 | createStore( 8 | rootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk) 11 | ) 12 | ); 13 | 14 | export default configureStore; 15 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mello 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | <%= javascript_include_tag 'application' %> 9 | 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the rails generate channel command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | export const RECEIVE_USERS = 'RECEIVE_USERS'; 3 | 4 | export const receiveUsers = (response) => { 5 | return { 6 | type: RECEIVE_USERS, 7 | response: response 8 | }; 9 | }; 10 | 11 | export const requestUsers = () => { 12 | return (dispatch) => { 13 | return APIUtil.fetchUsers() 14 | .then(data => { 15 | return dispatch(receiveUsers(data)); 16 | } 17 | ); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def show 4 | @users = User.all 5 | 6 | render :index 7 | end 8 | 9 | def create 10 | @user = User.new(user_params) 11 | if @user.save 12 | login(@user) 13 | render :show 14 | else 15 | render json: @user.errors.full_messages, status: 422 16 | end 17 | end 18 | 19 | def user_params 20 | params.require(:user).permit(:username, :password) 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /frontend/reducers/user_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_USERS } from '../actions/user_actions'; 2 | import { RECEIVE_BOARD } from '../actions/board_actions'; 3 | 4 | const UserReducer = (state = {}, action) => { 5 | let newState; 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_BOARD: 9 | return action.response.user_sharing; 10 | case RECEIVE_USERS: 11 | return action.response; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default UserReducer; 18 | -------------------------------------------------------------------------------- /app/controllers/api/board_shares_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BoardSharesController < ApplicationController 2 | 3 | def index 4 | render :index 5 | end 6 | 7 | 8 | def create 9 | @board_share = BoardShare.new(board_share_params) 10 | if @board_share.save 11 | render :show 12 | else 13 | render json: @board_show.errors.full_messages, status: 422 14 | end 15 | end 16 | 17 | def board_share_params 18 | params.require(:board_share).permit(:user_id, :board_id) 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /app/views/api/boards/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! :boards do 2 | @boards.each do |board| 3 | json.set! board.id do 4 | json.author_id board.author_id 5 | json.title board.title 6 | json.listIds board.lists.map{|el| el.id} 7 | end 8 | end 9 | end 10 | json.set! :shared_boards do 11 | @shared_boards.each do |board| 12 | json.set! board.id do 13 | json.author_id board.author_id 14 | json.title board.title 15 | json.listIds board.lists.map{|el| el.id} 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /frontend/actions/board_share_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | export const ADD_USER_TO_BOARD = 'ADD_USER_TO_BOARD'; 3 | 4 | 5 | export const boardShare = (response) => { 6 | return { 7 | type: ADD_USER_TO_BOARD, 8 | response: response 9 | }; 10 | }; 11 | 12 | export const addUserToBoard = (boardShareParams) => { 13 | return (dispatch) => { 14 | APIUtil.addUserToBoard(boardShareParams).then( response =>{ 15 | dispatch(boardShare(response)); 16 | }); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class UserTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module QuickBoard 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /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 ord 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 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { HashRouter } from 'react-router-dom'; 5 | import App from './app'; 6 | 7 | import { DragSource, DragDropContext, DragDropContextProvider, DropTarget } from 'react-dnd'; 8 | import HTML5Backend from 'react-dnd-html5-backend'; 9 | 10 | const Root = ({ store }) => ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | // export default Root; 19 | export default DragDropContext(HTML5Backend)(Root); 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | node_modules/ 20 | .DS_Store 21 | bundle.js 22 | bundle.js.map 23 | -------------------------------------------------------------------------------- /frontend/reducers/shared_board_reducer.js: -------------------------------------------------------------------------------- 1 | import { ADD_USER_TO_BOARD } from '../actions/board_share_actions'; 2 | import { RECEIVE_BOARD_INDEX } from '../actions/board_actions'; 3 | import { LOGOUT } from '../actions/session_actions'; 4 | import { merge } from 'lodash'; 5 | 6 | 7 | const sharedBoardReducer = (state = {}, action) => { 8 | let newState; 9 | Object.freeze(state); 10 | switch (action.type) { 11 | case ADD_USER_TO_BOARD: 12 | return state; 13 | case RECEIVE_BOARD_INDEX: 14 | return merge({}, state, action.shared_boards); 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export default sharedBoardReducer; 21 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | root to: 'static_pages#root' 4 | namespace :api, defaults: { format: :json } do 5 | resources :users, only: [ :index, :create ] 6 | resource :session, only: [ :create, :destroy ] 7 | resources :boards, only: [:index, :show, :create, :update] 8 | resources :lists, only: [:create, :destroy, :update] 9 | resources :cards, only: [:create, :destroy, :update] 10 | resources :moves, only: [:create, :show, :update] 11 | resources :board_shares, only: [:index, :create] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/list.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: lists 4 | # 5 | # id :integer not null, primary key 6 | # board_id :integer not null 7 | # title :string not null 8 | # ord :integer not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class List < ApplicationRecord 14 | validates :title, :board, :ord, presence: true 15 | 16 | belongs_to :board, 17 | class_name: :Board, 18 | foreign_key: :board_id 19 | 20 | has_one :author, 21 | through: :board, 22 | source: :author 23 | 24 | has_many :cards, dependent: :destroy 25 | end 26 | -------------------------------------------------------------------------------- /app/models/card.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: cards 4 | # 5 | # id :integer not null, primary key 6 | # list_id :integer not null 7 | # ord :integer not null 8 | # body :string not null 9 | # due_date :date 10 | # completed :boolean default("false") 11 | # 12 | 13 | class Card < ApplicationRecord 14 | validates :body, :ord, :list_id, presence: true 15 | 16 | belongs_to :list, 17 | class_name: :List, 18 | foreign_key: :list_id 19 | 20 | has_one :board, 21 | through: :list, 22 | source: :board 23 | 24 | has_one :author, 25 | through: :board, 26 | source: :author 27 | 28 | end 29 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # card_id :integer not null 8 | # body :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Comment < ApplicationRecord 14 | validates :author, :card, :body, presence: true 15 | 16 | belongs_to :author, 17 | class_name: 'User', 18 | primary_key: :id, 19 | foreign_key: :author_id 20 | 21 | belongs_to :card 22 | 23 | has_one :board, 24 | through: :card, 25 | source: :board 26 | end 27 | -------------------------------------------------------------------------------- /frontend/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import configureStore from './store/store'; 4 | import Root from './components/root'; 5 | 6 | import { login, signup, logout } from './actions/session_actions'; 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | 10 | 11 | let store; 12 | if (window.currentUser) { 13 | const preloadedState = { session: { currentUser: window.currentUser } }; 14 | store = configureStore(preloadedState); 15 | delete window.currentUser; 16 | } else { 17 | store = configureStore(); 18 | } 19 | 20 | const root = document.getElementById('root'); 21 | ReactDOM.render(, root); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import session from './session_reducer'; 3 | import boardReducer from './board_reducer'; 4 | import listReducer from './list_reducer'; 5 | import cardReducer from './card_reducer'; 6 | import userReducer from './user_reducer'; 7 | import sharedBoardReducer from './shared_board_reducer'; 8 | import hoverReducer from './hover_reducer'; 9 | 10 | 11 | const rootReducer = combineReducers({ 12 | session: session, 13 | boards: boardReducer, 14 | lists: listReducer, 15 | cards: cardReducer, 16 | users: userReducer, 17 | shared_boards: sharedBoardReducer, 18 | // hover: hoverReducer, 19 | }); 20 | 21 | export default rootReducer; 22 | -------------------------------------------------------------------------------- /app/models/board_share.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: board_shares 4 | # 5 | # id :integer not null, primary key 6 | # board_id :integer not null 7 | # user_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class BoardShare < ApplicationRecord 13 | validates :user, :board, presence: true 14 | validates :user, uniqueness: { scope: :board } 15 | validates_uniqueness_of :user_id, :scope => :board_id 16 | 17 | belongs_to :user, 18 | foreign_key: :user_id, 19 | class_name: :User 20 | 21 | belongs_to :board, 22 | foreign_key: :board_id, 23 | class_name: :Board 24 | 25 | end 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/reducers/async_status_sample_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_INDEX_BOARD } from '../actions/board_actions'; 2 | export const SUCCESS = "SUCCESS"; 3 | 4 | const defaultState = { 5 | asyncStatus: "LOADING", 6 | error: null, 7 | data: {}, 8 | }; 9 | 10 | const boardIndexReducer = (state = defaultState, action) => { 11 | Object.freeze(state); 12 | switch (action.type) { 13 | case RECEIVE_INDEX_BOARD: 14 | let newState = { 15 | asyncStatus: action.asyncStatus, 16 | data: action.asyncStatus === SUCCESS ? action.data : state.data, 17 | error: action.error 18 | }; 19 | return newState; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | export default boardIndexReducer; 26 | -------------------------------------------------------------------------------- /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 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/controllers/api/cards_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CardsController < ApplicationController 2 | before_action :require_login 3 | 4 | def create 5 | @card = Card.new(card_params) 6 | if @card.save 7 | render :show 8 | else 9 | render json: @card.errors.full_messages, status: 422 10 | end 11 | end 12 | 13 | def update 14 | if params[:id] != "undefined" 15 | @card = Card.find(params[:id]) 16 | if @card.update(card_params) 17 | render json: @card 18 | else 19 | render json: @card.errors.full_messages, status: 422 20 | end 21 | end 22 | end 23 | 24 | def card_params 25 | params.require(:card).permit(:list_id, :ord, :body, :due_date, :completed, :cardLoad) 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | # This model initially had no columns defined. If you add columns to the 16 | # model remove the '{}' from the fixture names and add the columns immediately 17 | # below each fixture, per the syntax in the comments below 18 | # 19 | one: {} 20 | # column: value 21 | # 22 | two: {} 23 | # column: value 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | helper_method :logged_in?, :current_user 5 | 6 | def current_user 7 | return nil unless session[:session_token] 8 | @current_user ||= User.find_by_session_token(session[:session_token]) 9 | return @current_user 10 | end 11 | 12 | def login(user) 13 | session[:session_token] = user.reset_session_token! 14 | @current_user = user 15 | end 16 | 17 | def logout 18 | current_user.reset_session_token! 19 | session[:session_token] = nil 20 | end 21 | 22 | def logged_in? 23 | !!current_user 24 | end 25 | 26 | def require_login 27 | render json: {base: ['invalid credentials']}, status: 401 if !current_user 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/api/lists_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ListsController < ApplicationController 2 | before_action :require_login 3 | 4 | def create 5 | @list = List.new(list_params) 6 | if @list.save 7 | render :show 8 | else 9 | render json: @list.errors.full_messages, status: 422 10 | end 11 | end 12 | 13 | def show 14 | @list = List.find(params[:id]) 15 | render :show 16 | end 17 | 18 | def update 19 | if params[:id] != "undefined" 20 | @list = List.find(params[:id]) 21 | if @list.update(list_params) 22 | render json: @list 23 | else 24 | render json: @list.errors.full_messages, status: 422 25 | end 26 | end 27 | end 28 | 29 | 30 | def list_params 31 | params.require(:list).permit(:id, :board_id, :title, :ord) 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials( 5 | user_params[:username], 6 | user_params[:password] 7 | ) 8 | @user_account = User.find_by_username(user_params[:username]) 9 | if @user 10 | login(@user) 11 | render '/api/users/show' 12 | elsif @user_account != nil 13 | render json: ['Invalid password'], status: 422 14 | else 15 | render json: ['There isn\'t an account for this username'], status: 422 16 | end 17 | end 18 | 19 | def destroy 20 | if current_user 21 | logout 22 | render json: {} 23 | else 24 | logout 25 | render json: {}, status: 404 26 | end 27 | end 28 | 29 | private 30 | 31 | def user_params 32 | params.require(:user).permit(:username, :password) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /frontend/actions/list_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | export const RECEIVE_LIST = "RECEIVE_LIST"; 4 | export const EDIT_LIST = "EDIT_LIST"; 5 | 6 | export const receiveList = (response) => { 7 | return { 8 | type: RECEIVE_LIST, 9 | response: response, 10 | }; 11 | }; 12 | 13 | export const receiveListEdit = (response) => { 14 | return { 15 | type: EDIT_LIST, 16 | response: response, 17 | }; 18 | }; 19 | 20 | export const createList = (listParams) => { 21 | return (dispatch) => { 22 | APIUtil.createList(listParams).then( response =>{ 23 | dispatch(receiveList(response)); 24 | }); 25 | }; 26 | }; 27 | 28 | 29 | export const editListText = ( listParams ) => { 30 | 31 | return (dispatch) => { 32 | APIUtil.editList(listParams).then( response => { 33 | dispatch(receiveListEdit(response)); 34 | }); 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_CURRENT_USER, 3 | RECEIVE_ERRORS, 4 | CLEAR_ERRORS, 5 | LOGOUT, 6 | } from '../actions/session_actions'; 7 | 8 | 9 | const defaultState = { 10 | currentUser: null, 11 | errors: [] 12 | }; 13 | 14 | const SessionReducer = (state = defaultState, action) => { 15 | let newState; 16 | Object.freeze(state); 17 | switch (action.type) { 18 | case RECEIVE_CURRENT_USER: 19 | return { currentUser: action.user, errors: [] }; 20 | case RECEIVE_ERRORS: 21 | newState = Object.assign({}, state); 22 | newState.errors = action.errors; 23 | return newState; 24 | case CLEAR_ERRORS: 25 | newState = Object.assign({}, state); 26 | newState.errors = []; 27 | return newState; 28 | case LOGOUT: 29 | return defaultState; 30 | default: 31 | return state; 32 | } 33 | }; 34 | 35 | export default SessionReducer; 36 | -------------------------------------------------------------------------------- /app/models/board.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: boards 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # title :string not null 8 | # privacy_status :boolean default("true") 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Board < ApplicationRecord 14 | validates :author, :title, presence: true 15 | validates :privacy_status, inclusion: [true, false] 16 | 17 | belongs_to :author, 18 | class_name: :User, 19 | primary_key: :id, 20 | foreign_key: :author_id 21 | 22 | has_many :lists, 23 | class_name: :List, 24 | foreign_key: :board_id, 25 | dependent: :destroy 26 | 27 | has_many :cards, 28 | through: :lists, 29 | source: :cards 30 | 31 | has_many :board_shares 32 | 33 | has_many :shared_users, 34 | through: :board_shares, 35 | source: :user 36 | end 37 | -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Redirect, withRouter } from 'react-router-dom'; 4 | 5 | const Auth = ({ component: Component, path, loggedIn }) => ( 6 | ( 7 | !loggedIn ? ( 8 | 9 | ) : ( 10 | 11 | ) 12 | )} /> 13 | ); 14 | 15 | const Protected = ({ component: Component, path, loggedIn }) => ( 16 | ( 17 | loggedIn ? ( 18 | 19 | ) : ( 20 | 21 | ) 22 | )} /> 23 | ); 24 | 25 | const mapStateToProps = state => ( 26 | {loggedIn: Boolean(state.session.currentUser)} 27 | ); 28 | 29 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth)); 30 | 31 | export const ProtectedRoute = withRouter(connect(mapStateToProps, null)(Protected)); 32 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Greeting from './greeting'; 3 | import SessionForm from './session_form'; 4 | import BoardIndex from './board_index'; 5 | import BoardShow from './board_show'; 6 | import Header from './head/header'; 7 | import { Route, Switch } from 'react-router-dom'; 8 | import { Redirect } from 'react-router'; 9 | import { AuthRoute, ProtectedRoute } from '../util/route_util'; 10 | 11 | const App = () => ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | ); 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /frontend/reducers/board_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_BOARD_INDEX, REMOVE_BOARD, RECEIVE_BOARD } from '../actions/board_actions'; 2 | import { LOGOUT } from '../actions/session_actions'; 3 | import { merge } from 'lodash'; 4 | 5 | const boardReducer = (state = {}, action) => { 6 | Object.freeze(state); 7 | //state here should be the partial state of boards: stuff 8 | //we are only returning the partial state back to the root reducer; 9 | 10 | switch (action.type){ 11 | case RECEIVE_BOARD_INDEX: 12 | return merge({}, state, action.boards); //could merge return something weird? 13 | case RECEIVE_BOARD: 14 | return merge({}, state, {[action.response.board.id]: action.response.board}); 15 | case REMOVE_BOARD: 16 | let newState = merge({}, state); 17 | delete newState[action.boardId]; //check for boardId 18 | return newState; 19 | case LOGOUT: 20 | return {}; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export default boardReducer; 27 | -------------------------------------------------------------------------------- /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 | development: 14 | secret_key_base: eaa879badf2f85ff351c1f747ee9d7757a87fd57d3f2e79666235c2953758341138b3351c2e9b8f4b3d1105bff0ca679a0f634c120a971e8183333358b45bf25 15 | 16 | test: 17 | secret_key_base: e15243d04e2f6b0001019322ebf6d0aae1872377c19fb1a343e6f8e515aa9059999fc566c49127b47cfd440f146ff39fae554e437434ea85cabd2b61234ad0e9 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /frontend/reducers/hover_reducer.js: -------------------------------------------------------------------------------- 1 | //due to issues with React Drag and Drop library, the card reducer 2 | //must be set separate from the rendering of the grey box's state: 3 | //the drop library will repeatedly invoke fire thunks, and will override the 4 | 5 | import { CREATE_DROPZONE } from '../actions/hover_actions'; 6 | import { merge } from 'lodash'; 7 | 8 | const hoverReducer = (state = {}, action) => { 9 | Object.freeze(state); 10 | 11 | let output; 12 | let newCard; 13 | let newState; 14 | 15 | switch (action.type){ 16 | case CREATE_DROPZONE: 17 | return state; 18 | // if (action.response.listHoverIndex){ 19 | // let listHoverIndex = action.response.listHoverIndex; 20 | // let cardHoverIndex = action.response.cardHoverIndex; 21 | // 22 | // // newState = merge({}, state, {listHoverIndex: listHoverIndex, cardHoverIndex: cardHoverIndex}); 23 | // // return {listHoverIndex: listHoverIndex, cardHoverIndex: cardHoverIndex}; 24 | // } else { 25 | // return state; 26 | // } 27 | default: 28 | return state; 29 | } 30 | }; 31 | 32 | export default hoverReducer; 33 | -------------------------------------------------------------------------------- /docs/sample-state-2.md: -------------------------------------------------------------------------------- 1 | ```js 2 | state = { 3 | // header state 4 | //note that asyncStatus enables us to display 'Loading' interstitial. 5 | boards: {1: { 6 | author_id: 1, 7 | title: "Building a house", 8 | listIds: [2, 3, 4] 9 | }, 10 | 2: { 11 | author_id: 1, 12 | title: "Building a house", 13 | listIds: [2, 3, 4] 14 | } 15 | }, 16 | lists: {3: { 17 | boardId: 1, 18 | title: "First List", 19 | cardIds: [22, 30, 40] 20 | }, 21 | 4: { 22 | boardId: 1, 23 | title: "Second List", 24 | cardIds: [22, 30, 40] 25 | } 26 | }, 27 | cards: {22: { 28 | listId: 4, 29 | body: "I herd u liek mudkipz", 30 | due_date: "Sun June 18 2017", 31 | commentIds: [3,4,5] 32 | }, 33 | 30: { 34 | listId: 4, 35 | body: "I herd u liek mudkipz", 36 | due_date: "Sun June 18 2017", 37 | commentIds: [3,4,5] 38 | }, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | ### Root 6 | 7 | - `GET /` 8 | 9 | ## JSON API 10 | 11 | ### Users 12 | - `GET /api/users/:id` 13 | - state payload for header information 14 | - `POST /api/users` 15 | - `PATCH /api/users` 16 | 17 | ### Session 18 | - `POST /api/session` 19 | - `DELETE /api/session` 20 | 21 | ### Boards 22 | - `GET /api/boards` 23 | - state payload for index page of boards 24 | - `GET /api/boards/:id` 25 | - state payload for board show page which will 26 | execute list/card fetch (see state page) 27 | - `POST /api/boards` 28 | - `PATCH /api/boards/:id` 29 | - `DELETE /api/boards/:id` 30 | 31 | ### Lists 32 | - `POST /api/lists` 33 | - `PATCH /api/lists/:id` 34 | - `DELETE /api/lists/:id` 35 | 36 | ### Cards 37 | - `GET /api/cards/:id/` 38 | - state payload for card modal page 39 | - returns associated comments as well 40 | - `POST /api/cards` 41 | - `PATCH /api/cards/:id` 42 | - `DELETE /api/cards/:id` 43 | 44 | ### Comments 45 | - `POST /api/comments` 46 | - `PATCH /api/comments/:id` 47 | - `DELETE /api/comments/:id` 48 | 49 | ### Board Shares 50 | - `POST /api/board_shares/` 51 | - `DELETE /api/board_shares/:id` 52 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js 2 | var path = require("path"); 3 | var webpack = require("webpack"); 4 | 5 | var plugins = []; // if using any plugins for both dev and production 6 | var devPlugins = []; // if using any plugins for development 7 | 8 | var prodPlugins = [ 9 | new webpack.DefinePlugin({ 10 | 'process.env': { 11 | 'NODE_ENV': JSON.stringify('production') 12 | } 13 | }), 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compress: { 16 | warnings: true 17 | } 18 | }) 19 | ]; 20 | 21 | plugins = plugins.concat( 22 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins 23 | ); 24 | 25 | // include plugins config 26 | module.exports = { 27 | context: __dirname, 28 | entry: "./frontend/entry.jsx", 29 | output: { 30 | path: path.resolve(__dirname, "app", "assets", "javascripts"), 31 | filename: "bundle.js" 32 | }, 33 | plugins: plugins, 34 | module: { 35 | loaders: [ 36 | { 37 | test: /\.jsx?$/, 38 | exclude: /node_modules/, 39 | loader: 'babel-loader', 40 | query: { 41 | presets: ['react', 'es2015'] 42 | } 43 | } 44 | ] 45 | }, 46 | devtool: 'source-map', 47 | resolve: { 48 | extensions: [".js", ".jsx", "*"] 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.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.0 upgrade. 4 | # 5 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 6 | 7 | Rails.application.config.raise_on_unfiltered_parameters = true 8 | 9 | # Enable per-form CSRF tokens. Previous versions had false. 10 | Rails.application.config.action_controller.per_form_csrf_tokens = true 11 | 12 | # Enable origin-checking CSRF mitigation. Previous versions had false. 13 | Rails.application.config.action_controller.forgery_protection_origin_check = true 14 | 15 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 16 | # Previous versions had false. 17 | ActiveSupport.to_time_preserves_timezone = true 18 | 19 | # Require `belongs_to` associations by default. Previous versions had false. 20 | Rails.application.config.active_record.belongs_to_required_by_default = true 21 | 22 | # Do not halt callback chains when a callback returns false. Previous versions had true. 23 | ActiveSupport.halt_callback_chains_on_return_false = false 24 | 25 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 26 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 27 | -------------------------------------------------------------------------------- /app/views/api/boards/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! :board do 2 | json.id @board.id 3 | json.author_id @board.author_id 4 | json.title @board.title 5 | json.listIds @board.lists.map{|el| el.id} 6 | end 7 | 8 | json.lists({}) 9 | json.set! :lists do 10 | 11 | @board.lists.each do |list| 12 | json.set! list.id do 13 | json.board_id list.board_id 14 | json.title list.title 15 | json.ord list.ord 16 | json.cardIds list.cards.sort_by{|card| card.ord}.map{|card| card.id} 17 | # json.cardIds list.cards.map{|el| el.id} 18 | end 19 | end 20 | end 21 | 22 | json.cards({}) 23 | json.set! :cards do 24 | @board.cards.each do |card| 25 | json.set! card.id do 26 | json.list_id card.list_id 27 | json.body card.body 28 | json.ord card.ord 29 | json.due_date card.due_date 30 | end 31 | end 32 | end 33 | 34 | json.set! :user_sharing do 35 | json.set! :shared_users do 36 | json.shared_user_ids @user_ids_shared_with 37 | json.shared_usernames @usernames_shared_with 38 | end 39 | json.set! :unshared_users do 40 | json.unshared_user_ids @user_ids_not_shared_with 41 | json.unshared_usernames @usernames_not_shared_with 42 | end 43 | # json.unshared_users @users_not_shared_with 44 | end 45 | 46 | json.set! :hovering do 47 | json.listHoverIndex nil 48 | json.cardHoverIndex nil 49 | end 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QuickBoard", 3 | "version": "1.0.0", 4 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 5 | "main": "index.js", 6 | "engines": { 7 | "node": "6.10.0", 8 | "npm": "3.10.10" 9 | }, 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "webpack": "webpack --watch", 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "postinstall": "webpack" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "babel-core": "^6.25.0", 23 | "babel-loader": "^7.0.0", 24 | "babel-preset-es2015": "^6.24.1", 25 | "babel-preset-react": "^6.24.1", 26 | "react": "^15.6.1", 27 | "react-dnd": "^2.4.0", 28 | "react-dnd-html5-backend": "^2.4.1", 29 | "react-dom": "^15.6.1", 30 | "react-masonry-component": "^5.0.7", 31 | "react-onclickoutside": "^6.1.1", 32 | "react-redux": "^5.0.5", 33 | "react-router": "^4.1.1", 34 | "react-router-dom": "^4.1.1", 35 | "redux": "^3.6.0", 36 | "redux-logger": "^3.0.6", 37 | "redux-thunk": "^2.2.0", 38 | "webpack": "^2.6.1" 39 | }, 40 | "devDependencies": { 41 | "babel-cli": "^6.24.1", 42 | "babel-plugin-transform-class-properties": "6.8.0", 43 | "babel-preset-es2017": "^6.24.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 4 | export const RECEIVE_ERRORS = 'RECEIVE_ERRORS'; 5 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 6 | export const LOGOUT = 'LOGOUT'; 7 | 8 | export const receiveCurrentUser = (user) => ({ 9 | type: RECEIVE_CURRENT_USER, 10 | user 11 | }); 12 | 13 | export const receiveErrors = (errors) => ({ 14 | type: RECEIVE_ERRORS, 15 | errors 16 | }); 17 | 18 | export const clearErrors = () => ({ 19 | type: CLEAR_ERRORS 20 | }); 21 | 22 | const logoutCurrentUser = () => ({ 23 | type: LOGOUT 24 | }); 25 | 26 | export const login = (user) => ( 27 | (dispatch) => { 28 | return APIUtil.login(user) 29 | .then( 30 | user => dispatch(receiveCurrentUser(user)), 31 | errors => dispatch(receiveErrors(errors.responseJSON)) 32 | ); 33 | } 34 | ); 35 | 36 | export const signup = (user) => ( 37 | (dispatch) => { 38 | return APIUtil.signup(user) 39 | .then( 40 | user => dispatch(receiveCurrentUser(user)), 41 | errors => dispatch(receiveErrors(errors.responseJSON)) 42 | ); 43 | } 44 | ); 45 | 46 | export const logout = () => ( 47 | (dispatch) => { 48 | return APIUtil.logout() 49 | .then( 50 | () => dispatch(logoutCurrentUser()), 51 | errors => dispatch(receiveErrors(errors.responseJSON)) 52 | ); 53 | } 54 | ); 55 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | ## Component Hierarchy (MVP) 2 | 3 | **AuthForm** 4 | - Greeting 5 | 6 | **App** 7 | - Header 8 | - Router 9 | - BoardIndex 10 | - BoardShow 11 | 12 | **Header** ________________ mapStateProps: header 13 | - BoardMenuDropdown _____ passed Props: header.boards 14 | - NewBoardDropdown 15 | - UserMenuDropdown ______ passed Props: header.user 16 | - Search (later) 17 | 18 | **BoardIndex** ____________ mapStateProps: boardIndex 19 | - BoardLink _____________ passed Props: boardIndex.data (element) 20 | - CreateBoardDropdown 21 | 22 | **BoardShow** _____________ mapStateProps: boardShow 23 | - List __________________ passed Props: boardShow.data.lists (element) 24 | - CreateList ____________ passed Props: boardShow.data.id 25 | 26 | **List** _________________ passed Props: list (from boardShow.data.lists (element)) 27 | - Card _________________ passed Props: list.cards (element) 28 | - NewCardDropdown ______ passed Props: list.id 29 | 30 | **Card** _________________ passed Props: card (from list.cards (element)) 31 | - CardDetailModal ______ mapStateProps: cardModal 32 | 33 | **CardDetailModal** 34 | - NewCommentBox ________ passed Props: card.id 35 | - CommentIndex _________ passed Props: cardModal.data.comments 36 | 37 | ### Routes 38 | Path | Component | 39 | -------------------|--------------| 40 | /signup | Greeting | 41 | /signin | Greeting | 42 | /home | BoardIndex | 43 | /home/:id | BoardShow | 44 | -------------------------------------------------------------------------------- /app/controllers/api/boards_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BoardsController < ApplicationController 2 | before_action :require_login 3 | 4 | def index 5 | @boards = current_user.boards.includes(:lists) 6 | @shared_boards = current_user.shared_boards.includes(:lists) 7 | 8 | render :index 9 | end 10 | 11 | def create 12 | @board = current_user.boards.new(board_params) 13 | # One-liner above equivalent to code below: 14 | # @board = Board.new(board_params) 15 | # @board.author_id = current_user.id 16 | 17 | if @board.save 18 | render :show 19 | else 20 | render json: @board.errors.full_messages, status: 422 21 | end 22 | end 23 | 24 | 25 | def show 26 | @board = Board.find(params[:id]) 27 | @user_ids_shared_with = BoardShare.where(board_id: params[:id]) 28 | .map{|el| el.user_id}.uniq 29 | 30 | @usernames_shared_with = User.where(id: @user_ids_shared_with) 31 | .map{|user| user.username} 32 | 33 | @user_ids_not_shared_with = User.where.not(id: @user_ids_shared_with) 34 | .where.not(id: @board.author_id) 35 | .map{|el| el.id}.uniq 36 | 37 | @usernames_not_shared_with = User.where(id: @user_ids_not_shared_with) 38 | .map{|user| user.username} 39 | @board.lists.each{|list| p list.cards} 40 | render :show 41 | end 42 | 43 | def board_params 44 | params.require(:board).permit(:id, :author_id, :title, :privacy_status) 45 | end 46 | 47 | 48 | 49 | 50 | end 51 | -------------------------------------------------------------------------------- /frontend/components/greeting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { logout } from '../actions/session_actions'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router-dom'; 5 | import { Redirect, withRouter } from 'react-router'; 6 | 7 | class Greeting extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.handleLogout = this.handleLogout.bind(this); 12 | } 13 | 14 | handleLogout() { 15 | this.props.logout().then( 16 | () => this.props.history.push("/login") 17 | ); 18 | } 19 | 20 | render() { 21 | const user = this.props.user; 22 | if (user) { 23 | return ( 24 |
25 |
26 | ); 27 | } else { 28 | return ( 29 | 30 |
31 |
32 | 33 |
Mello
34 |
35 |
36 |
37 | Log In 38 | Sign Up 39 |
40 |
41 | ); 42 | } 43 | } 44 | } 45 | 46 | const mapStateToProps = (state) => ({ 47 | user: state.session.currentUser 48 | }); 49 | 50 | const mapDispatchToProps = (dispatch) => ({ 51 | logout: () => dispatch(logout()) 52 | }); 53 | 54 | export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Greeting)); 55 | -------------------------------------------------------------------------------- /frontend/reducers/card_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_BOARD } from '../actions/board_actions'; 2 | import { RECEIVE_CARD, UPDATE_CARD, EDIT_CARD, CREATE_DROPZONE } 3 | from '../actions/card_actions'; 4 | import { LOGOUT } from '../actions/session_actions'; 5 | 6 | import { merge } from 'lodash'; 7 | 8 | const cardReducer = (state = {}, action) => { 9 | Object.freeze(state); 10 | 11 | let output; 12 | let newCard; 13 | let newState; 14 | switch (action.type){ 15 | case RECEIVE_BOARD: 16 | if (action.response.cards === undefined) { 17 | output = {}; 18 | } else { 19 | output = action.response.cards; 20 | } 21 | return output; 22 | case RECEIVE_CARD: 23 | newCard = action.response.card; 24 | newState = merge({}, state, {[newCard.id]: newCard}); 25 | return newState; 26 | case UPDATE_CARD: 27 | 28 | newCard = action.response.cardLoad; 29 | if (newCard.id){ 30 | newState = merge({}, state, {[newCard.id]: { id: parseInt(newCard.id), 31 | list_id: parseInt(newCard.list_id), 32 | ord: parseInt(newCard.ord), 33 | body: newCard.body 34 | }}); 35 | return newState; 36 | } else { 37 | return state; 38 | } 39 | 40 | case EDIT_CARD: 41 | if (action.response){ 42 | newCard = action.response; 43 | newState = merge({}, state, {[newCard.id]: newCard}); 44 | return newState; 45 | } else { 46 | return state; 47 | } 48 | case LOGOUT: 49 | return {}; 50 | case "IGNORE": 51 | return state; 52 | default: 53 | return state; 54 | } 55 | }; 56 | 57 | export default cardReducer; 58 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class User < ApplicationRecord 14 | 15 | validates :username, :password_digest, presence: true 16 | validates :password, length: {minimum: 6, allow_nil: true} 17 | validates_uniqueness_of :username 18 | 19 | has_many :boards, 20 | foreign_key: :author_id, 21 | class_name: :Board 22 | 23 | has_many :board_shares, 24 | foreign_key: :user_id, 25 | class_name: :BoardShare 26 | 27 | has_many :shared_boards, 28 | through: :board_shares, 29 | source: :board 30 | 31 | attr_accessor :password 32 | 33 | after_initialize :ensure_session_token 34 | 35 | def self.find_by_credentials(username, password) 36 | user = User.find_by(username: username) 37 | return nil if user.nil? 38 | user.is_password?(password) ? user : nil 39 | end 40 | 41 | def is_password?(password) 42 | BCrypt::Password.new(self.password_digest).is_password?(password) 43 | end 44 | 45 | def password=(password) 46 | @password = password 47 | self.password_digest = BCrypt::Password.create(password) 48 | end 49 | 50 | def reset_session_token! 51 | self.session_token = generate_session_token 52 | self.save! 53 | self.session_token 54 | end 55 | 56 | def generate_session_token 57 | SecureRandom.urlsafe_base64(16) 58 | end 59 | 60 | private 61 | def ensure_session_token 62 | self.session_token ||= generate_session_token 63 | end 64 | 65 | 66 | 67 | end 68 | -------------------------------------------------------------------------------- /frontend/actions/board_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | export const RECEIVE_BOARD_INDEX = "RECEIVE_BOARD_INDEX"; 3 | export const REMOVE_BOARD = 'REMOVE_BOARD'; 4 | export const RECEIVE_BOARD = 'RECEIVE_BOARD'; 5 | 6 | import { hashHistory } from 'react-router'; 7 | 8 | 9 | export const receiveBoards = (data) => { 10 | return { 11 | type: RECEIVE_BOARD_INDEX, 12 | boards: data.boards, 13 | lists: data.lists, 14 | cards: data.cards, 15 | shared_boards: data.shared_boards, 16 | }; 17 | }; 18 | 19 | export const removeBoard = (boardId) => { 20 | return { 21 | type: REMOVE_BOARD, 22 | boardId 23 | }; 24 | }; 25 | 26 | export const receiveBoard = (response) => { 27 | return { 28 | type: RECEIVE_BOARD, 29 | response: response, 30 | }; 31 | }; 32 | 33 | export const requestBoard = (id) =>{ 34 | return (dispatch) => { 35 | return APIUtil.boardShow(id) 36 | .then(data => { 37 | return dispatch(receiveBoard(data)); 38 | }); 39 | }; 40 | }; 41 | 42 | export const requestBoards = () => { 43 | return (dispatch) => { 44 | return APIUtil.boardIndex() 45 | .then(data => { 46 | return dispatch(receiveBoards(data)); 47 | } 48 | ); 49 | }; 50 | }; 51 | 52 | export const createBoard = (boardParams) => (dispatch) => { 53 | return APIUtil.createBoard(boardParams).then( 54 | (response) => { 55 | dispatch(receiveBoard(response)); 56 | } 57 | ); 58 | }; 59 | 60 | export const updateBoard = (updatedBoard) => (dispatch) => { 61 | return APIUtil.updateBoard(updatedBoard).then( 62 | (board) => dispatch(receiveBoard(board)) 63 | ); 64 | }; 65 | 66 | export const deleteBoard = (boardId) => (dispatch) => { 67 | return APIUtil.deleteBoard(boardId).then( 68 | () => dispatch(removeBoard(boardId)) 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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=3600' 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 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /frontend/components/head/board_menu_dropdown.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import onClickOutside from 'react-onclickoutside'; 3 | 4 | 5 | 6 | class BoardMenuDropdown extends React.Component{ 7 | 8 | constructor(props){ 9 | super(props); 10 | this.state = {modalPresence: false}; 11 | this.toggleBoardDropdown = this.toggleBoardDropdown.bind(this); 12 | 13 | } 14 | 15 | handleClickOutside(e) { 16 | this.setState(prevState => ({ 17 | modalPresence: false 18 | })); 19 | } 20 | 21 | toggleBoardDropdown() { 22 | this.setState(prevState => ({ 23 | modalPresence: !prevState.modalPresence 24 | })); 25 | } 26 | 27 | handleEnter(e){ 28 | //e.key and e.shiftkey 29 | if (e.key === "Enter"){ 30 | this.setState(prevState => ({ 31 | modalPresence: false 32 | })); 33 | } 34 | } 35 | 36 | render () { 37 | let output = this.props.boardMenu.map((board, idx) => { 38 | return ( 39 |
40 |
{board} 41 |
42 | ); 43 | }); 44 | 45 | let boardMenuModal; 46 | if ( this.state.modalPresence === true ){ 47 | boardMenuModal = ( 48 |
49 | {output} 50 |
51 | ); 52 | } 53 | 54 | return ( 55 |
56 | 64 | {boardMenuModal} 65 |
66 | ); 67 | } 68 | } 69 | 70 | 71 | export default onClickOutside(BoardMenuDropdown); 72 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # QuickBoard 2 | [Heroku link][heroku] 3 | 4 | [Trello link][trello] 5 | 6 | [heroku]: https://herokuapp.com 7 | [trello]: https://trello.com/b/Ha1BAOzo/quickboard 8 | 9 | 10 | 11 | ## Minimum Viable Product 12 | 13 | QuickBoard is a webapp inspired by Trello built on ReactJS and Rails. By the end of Week 9, this app will, at a minimum, satisfy the following criteria with smooth, bug-free navigation, adequate seed data and sufficient CSS styling: 14 | 15 | - [ ] Hosting on Heroku 16 | - [ ] New account creation, login, and guest/demo login 17 | - [ ] Boards 18 | - [ ] Lists 19 | - [ ] Cards 20 | - [ ] Board Sharing 21 | - [ ] Drag & Drop 22 | - [ ] Card Modals 23 | - [ ] Production README sample 24 | - [ ] Design Docs 25 | 26 | ## Design Docs 27 | * [View Wireframes][wireframes] 28 | * [React Components][components] 29 | * [API endpoints][api-endpoints] 30 | * [DB schema][schema] 31 | * [Sample State][sample-state] 32 | 33 | [wireframes]: wireframes 34 | [components]: component-hierarchy.md 35 | [sample-state]: sample-state.md 36 | [api-endpoints]: api-endpoints.md 37 | [schema]: schema.md 38 | 39 | ### Phase 1: Backend setup and Front End User Authentication (2 days) 40 | 41 | **Objective:** Functioning rails project with front-end Authentication 42 | 43 | ### Phase 2: Board Model, API, and components (2 days) 44 | 45 | **Objective:** Boards can be created, read, edited and destroyed through the API, including 46 | loading status, etc. 47 | 48 | ### Phase 3: List Model + Cards, API, and Components (3 days) 49 | 50 | **Objective:** Lists + Cards can be created, read, edited and destroyed through the API. 51 | 52 | ### Phase 4: Card Modals (1 day) 53 | 54 | **Objective:** Clicking on a card will pop up a modal that is editable. 55 | 56 | ### Phase 5: Board Sharing (1 day) 57 | 58 | **Objective:** Boards can be shared among users. 59 | 60 | 61 | ### Bonus Features (TBD) 62 | 63 | Search notes by content 64 | Giphy integration in card modal 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/actions/card_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | import { values } from 'lodash'; 4 | 5 | export const RECEIVE_CARD = "RECEIVE_CARD"; 6 | export const UPDATE_CARD = "UPDATE_CARD"; 7 | export const EDIT_CARD = "EDIT_CARD"; 8 | 9 | export const receiveCard = (response) => { 10 | return { 11 | type: RECEIVE_CARD, 12 | response: response, 13 | }; 14 | }; 15 | 16 | export const updateCard = (response) => { 17 | if (values(response).length === 0){ 18 | return { 19 | type: "IGNORE", 20 | response: {}, 21 | }; 22 | } 23 | return { 24 | type: UPDATE_CARD, 25 | response: response, 26 | }; 27 | }; 28 | 29 | 30 | export const receiveCardEdit = (response) => { 31 | return { 32 | type: EDIT_CARD, 33 | response: response, 34 | }; 35 | }; 36 | 37 | export const createCard = (cardParams) => { 38 | return (dispatch) => { 39 | APIUtil.createCard(cardParams).then( response =>{ 40 | dispatch(receiveCard(response)); 41 | }); 42 | }; 43 | }; 44 | 45 | 46 | export const moveCard = (APIParams, cardParams) => { 47 | return (dispatch) => { 48 | dispatch(updateCard(cardParams)); 49 | APIUtil.moveCard(APIParams).then( response => { 50 | dispatch(updateCard(response)); 51 | }); 52 | }; 53 | }; 54 | 55 | export const renderCardMove = (cardParams) => { 56 | // dispatch(updateCard(earlyResponse)) 57 | // need cardParams for cardIds{fromPile: array ofIds} -- we don't rely on the 58 | // card params input whatsoever 59 | //desired format is same as the response format under my response zy 60 | //which is a hash containing fromPile, toPile keys, pointing to an array of card ids 61 | 62 | return (dispatch) => { 63 | dispatch(updateCard(cardParams)); 64 | }; 65 | }; 66 | 67 | 68 | export const editCardText = ( cardParams ) => { 69 | 70 | return (dispatch) => { 71 | APIUtil.editCard(cardParams).then( response => { 72 | dispatch(receiveCardEdit(response)); 73 | }); 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /app/assets/stylesheets/index.scss: -------------------------------------------------------------------------------- 1 | $offwhite: #EDEFF0; 2 | $header-blue: #026AA7; 3 | $dark-grey: #505050; 4 | $board-item-blue: #0469A4; 5 | 6 | 7 | .board-index-container { 8 | display: flex; 9 | justify-content: flex-start; 10 | flex-wrap: wrap; 11 | padding-left: 20px; 12 | padding-top: 7px; 13 | width: 1300px; 14 | } 15 | 16 | .board-index-container-shared-boards { 17 | display: flex; 18 | justify-content: flex-start; 19 | flex-direction: row; 20 | padding-left: 20px; 21 | padding-top: 7px; 22 | } 23 | 24 | 25 | a.board-index-link { 26 | font-family: "Helvetica Neue"; 27 | font-weight: bold; 28 | text-decoration: none; 29 | color: #333333; 30 | padding: 2px 6px 2px 6px; 31 | border-top: 1px solid #CCCCCC; 32 | border-right: 1px solid #333333; 33 | border-bottom: 1px solid #333333; 34 | border-left: 1px solid #CCCCCC; 35 | width: 295px; 36 | height: 95px; 37 | text-align: center; 38 | display: flex; 39 | justify-content: flex-start; 40 | flex-direction: row; 41 | margin-bottom: 20px; 42 | margin-right: 15px; 43 | background-color: $board-item-blue; 44 | color: white; 45 | padding-top: 10px; 46 | padding-left: 12px; 47 | font-size: 15px; 48 | border-radius: 4px; 49 | background-color: $header-blue; 50 | border: none; 51 | } 52 | 53 | a.board-index-link:hover{ 54 | background-color: #015B8F; 55 | } 56 | 57 | .fa-user-o:before{ 58 | font-size: 19px; 59 | color: gray; 60 | font-weight: bolder; 61 | } 62 | 63 | .board-index-header{ 64 | display: flex; 65 | text-align: center; 66 | padding-top: 38px; 67 | padding-left: 26px; 68 | } 69 | 70 | text.board-index-header-text { 71 | line-height: 20px; 72 | font-weight: bold; 73 | font-family: "Helvetica Neue"; 74 | font-size: 16px; 75 | color: $dark-grey; 76 | padding-left: 11px; 77 | } 78 | 79 | .board-index-section{ 80 | display: flex; 81 | flex-direction: column; 82 | align-self: center; 83 | justify-content: center; 84 | width: 1500px; 85 | position: absolute; 86 | left: 60px; 87 | } 88 | 89 | .index-container{ 90 | 91 | } 92 | -------------------------------------------------------------------------------- /app/assets/images/trello_vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | gem 'font-awesome-sass' 9 | 10 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 11 | gem 'rails', '~> 5.0.3' 12 | # Use postgresql as the database for Active Record 13 | gem 'pg', '~> 0.18' 14 | # Use Puma as the app server 15 | gem 'puma', '~> 3.0' 16 | # Use SCSS for stylesheets 17 | gem 'sass-rails', '~> 5.0' 18 | # Use Uglifier as compressor for JavaScript assets 19 | gem 'uglifier', '>= 1.3.0' 20 | # Use CoffeeScript for .coffee assets and views 21 | gem 'coffee-rails', '~> 4.2' 22 | # See https://github.com/rails/execjs#readme for more supported runtimes 23 | # gem 'therubyracer', platforms: :ruby 24 | gem 'rails_12factor' 25 | # Use jquery as the JavaScript library 26 | gem 'jquery-rails' 27 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 28 | gem 'jbuilder', '~> 2.5' 29 | # Use Redis adapter to run Action Cable in production 30 | # gem 'redis', '~> 3.0' 31 | # Use ActiveModel has_secure_password 32 | gem 'bcrypt', '~> 3.1.7' 33 | 34 | # Use Capistrano for deployment 35 | # gem 'capistrano-rails', group: :development 36 | 37 | group :development, :test do 38 | # Call 'byebug' anywhere in the code to stop execution and get a //debugger console 39 | gem 'byebug', platform: :mri 40 | gem 'annotate' 41 | gem 'better_errors' 42 | gem 'binding_of_caller' 43 | gem 'pry-rails' 44 | end 45 | 46 | group :development do 47 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 48 | gem 'web-console', '>= 3.3.0' 49 | gem 'listen', '~> 3.0.5' 50 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 51 | gem 'spring' 52 | gem 'spring-watcher-listen', '~> 2.0.0' 53 | end 54 | 55 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 56 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 57 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | ## users 2 | column name | data type | details 3 | ----------------|-----------|----------------------- 4 | id | integer | not null, primary key 5 | username | string | not null, indexed, unique 6 | name | string | not null 7 | password_digest | string | not null 8 | session_token | string | not null, indexed, unique 9 | 10 | ## boards 11 | column name | data type | details 12 | -------------------|-----------|----------------------- 13 | id | integer | not null, primary key 14 | author_id | integer | not null, foreign key (references users), indexed 15 | name | string | not null 16 | privacy_status | boolean | default: true 17 | 18 | ## lists 19 | column name | data type | details 20 | ------------|-----------|----------------------- 21 | id | integer | not null, primary key 22 | board_id | integer | not null, foreign key (references boards), indexed 23 | title | string | not null 24 | ord | integer | not null 25 | 26 | ## cards 27 | column name | data type | details 28 | ------------|-----------|----------------------- 29 | id | integer | not null, primary key 30 | list_id | integer | not null, foreign key (references lists), indexed 31 | title | string | not null 32 | description | string | 33 | due_date | datetime | 34 | 35 | ## comments 36 | column name | data type | details 37 | ------------|-----------|----------------------- 38 | id | integer | not null, primary key 39 | author_id | string | not null, foreign key (references users), indexed 40 | card_id | integer | not null, foreign key (references cards), indexed 41 | body | string | not null 42 | 43 | ## board_shares 44 | column name | data type | details 45 | ------------|-----------|----------------------- 46 | id | integer | not null, primary key 47 | board_id | integer | not null, foreign key (references boards), indexed, unique [user_id] 48 | user_id | integer | not null, foreign key (references users), indexed 49 | -------------------------------------------------------------------------------- /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 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 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 | -------------------------------------------------------------------------------- /frontend/reducers/list_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_BOARD } from '../actions/board_actions'; 2 | import { RECEIVE_CARD, UPDATE_CARD } from '../actions/card_actions'; 3 | import { RECEIVE_LIST, EDIT_LIST } from '../actions/list_actions'; 4 | import { LOGOUT } from '../actions/session_actions'; 5 | 6 | 7 | import { merge } from 'lodash'; 8 | 9 | Array.prototype.remove = function() { 10 | var what, a = arguments, L = a.length, ax; 11 | while (L && this.length) { 12 | what = a[--L]; 13 | while ((ax = this.indexOf(what)) !== -1) { 14 | this.splice(ax, 1); 15 | } 16 | } 17 | return this; 18 | }; 19 | 20 | 21 | const listReducer = (state = {}, action) => { 22 | Object.freeze(state); 23 | let output; 24 | let newCard; 25 | let parentList; 26 | let newState; 27 | let newParent; 28 | let newList; 29 | switch (action.type){ 30 | case RECEIVE_BOARD: 31 | output = action.response.lists; 32 | return output; 33 | // we need to merge in the new lists with the old 34 | case RECEIVE_LIST: 35 | output = action.response.list; 36 | return merge({}, state, {[output.id]: output}); 37 | 38 | case RECEIVE_CARD: 39 | newCard = action.response.card; 40 | parentList = state[newCard.list_id]; 41 | newState = merge({}, state, {[newCard.list_id]: parentList}); 42 | newState[newCard.list_id].cardIds.push(action.response.card.id); 43 | return newState; 44 | case UPDATE_CARD: 45 | newState = merge({}, state); 46 | let res = action.response; 47 | newState[res.cardLoad.starting.listId].cardIds = res.cardIds.fromPile; 48 | newState[res.cardLoad.ending.listId].cardIds = res.cardIds.toPile; 49 | return newState; 50 | case EDIT_LIST: 51 | if (action.response){ 52 | newList = action.response; 53 | newState = merge({}, state, {[newList.id]: newList}); 54 | return newState; 55 | } else { 56 | return state; 57 | } 58 | 59 | case LOGOUT: 60 | return {}; 61 | default: 62 | return state; 63 | } 64 | }; 65 | 66 | export default listReducer; 67 | -------------------------------------------------------------------------------- /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 }.to_i 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. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /frontend/components/list_edit_modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect as connectOriginal } from 'react-redux'; 3 | import { values, merge } from 'lodash'; 4 | import { findDOMNode } from 'react-dom'; 5 | import { DragSource, DragDropContext, DragDropContextProvider, DropTarget } 6 | from 'react-dnd'; 7 | import HTML5Backend from 'react-dnd-html5-backend'; 8 | 9 | class ListEditModal extends React.Component{ 10 | constructor(props){ 11 | super(props); 12 | this.toggleModal = this.toggleModal.bind(this); 13 | this.handleModalEdit = this.handleModalEdit.bind(this); 14 | this.onEditSubmit = this.onEditSubmit.bind(this); 15 | this.state = {modalPresence: false, title: props.title}; 16 | this.handleEnter = this.handleEnter.bind(this); 17 | } 18 | 19 | toggleModal(e) { 20 | this.setState(prevState => ({ 21 | modalPresence: !prevState.modalPresence 22 | })); 23 | } 24 | 25 | handleModalEdit(e){ 26 | e.preventDefault(); 27 | this.setState( { title: e.currentTarget.value } ); 28 | } 29 | 30 | handleEnter(e){ 31 | 32 | if (e.key === "Enter" && !e.shiftKey){ 33 | e.preventDefault(); 34 | this.props.handleListEditSubmit(this.props.listId, this.state.title); 35 | } 36 | } 37 | 38 | onEditSubmit(e){ 39 | e.preventDefault(); 40 | this.props.handleListEditSubmit(this.props.listId, this.state.title); 41 | } 42 | 43 | render(){ 44 | var listEditModal; 45 | var bodyLength = 30; 46 | if ( this.state.modalPresence === false ){ 47 | listEditModal = ( 48 |
49 |
50 | { this.props.title } 51 |
52 |
53 | ); 54 | } else { 55 | listEditModal = ( 56 |
59 |