├── 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 |
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 |
--------------------------------------------------------------------------------
/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 |
64 |
65 | );
66 | }
67 | return(
68 |
69 | {listEditModal}
70 |
71 | );
72 | }
73 | }
74 |
75 | export default ListEditModal;
76 |
--------------------------------------------------------------------------------
/frontend/components/head/user_menu.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 | import onClickOutside from 'react-onclickoutside';
7 |
8 |
9 | class UserMenu extends React.Component {
10 |
11 | constructor(props) {
12 | super(props);
13 | this.handleLogout = this.handleLogout.bind(this);
14 | this.toggleUserDropdown = this.toggleUserDropdown.bind(this);
15 | this.state = {modalPresence: false};
16 |
17 | }
18 |
19 | handleLogout() {
20 | this.props.logout().then(
21 | () => this.props.history.push("/login")
22 | );
23 | }
24 |
25 | handleClickOutside(e) {
26 | this.setState(prevState => ({
27 | modalPresence: false
28 | }));
29 | }
30 |
31 | toggleUserDropdown(){
32 | this.setState(prevState => ({
33 | modalPresence: !prevState.modalPresence
34 | }));
35 | }
36 |
37 | handleEnter(e){
38 | if (e.key === "enter"){
39 | this.setState(prevState => ({
40 | modalPresence: false
41 | }));
42 | }
43 | }
44 |
45 | render(){
46 | let user = this.props.user;
47 | let userDropdown;
48 | if ( this.state.modalPresence === true ){
49 | userDropdown = (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 | if (user) {
60 | return (
61 |
62 |
65 |
66 | {userDropdown}
67 |
68 |
69 | );
70 | } else {
71 | return (
);
72 | }
73 |
74 | }
75 |
76 | }
77 |
78 | const mapStateToProps = (state) => ({
79 | user: state.session.currentUser
80 | });
81 |
82 | const mapDispatchToProps = (dispatch) => ({
83 | logout: () => dispatch(logout())
84 | });
85 |
86 | export default connect(mapStateToProps, mapDispatchToProps)(withRouter(onClickOutside(UserMenu)));
87 |
--------------------------------------------------------------------------------
/app/controllers/api/moves_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::MovesController < ApplicationController
2 |
3 | def create
4 | if (params.flatten.include?("cardLoad"))
5 | #need to write case where list ids are the same and the new order is less than old order
6 | start_cards = params[:cardLoad][:starting]
7 | end_cards = params[:cardLoad][:ending]
8 | my_id = start_cards[:id].to_i
9 |
10 | old_list_id = start_cards[:listId].to_i
11 | new_list_id = end_cards[:listId].to_i
12 |
13 | old_card_order = start_cards[:cardIndex].to_i
14 | new_card_order = end_cards[:cardIndex].to_i
15 |
16 | if new_card_order < old_card_order && new_list_id == old_list_id
17 | new_card_order = (end_cards[:cardIndex].to_i - 0.5)
18 | else
19 | new_card_order = (end_cards[:cardIndex].to_i + 0.5)
20 | end
21 |
22 | starting_list_id = start_cards[:listId].to_i
23 |
24 | old_card_update_hash = Hash.new{ |h, k| h[k] = {} }
25 | old_lists_cards = List.find(new_list_id).cards
26 | old_sorted_cards = old_lists_cards.sort_by{|card| card.ord}
27 | old_sorted_cards.each_with_index do |card, i|
28 | old_card_update_hash[card.id] = { ord: i }
29 | end
30 |
31 | ActiveRecord::Base.transaction do
32 | Card.update(old_card_update_hash.keys, old_card_update_hash.values)
33 | end
34 |
35 | Card.update(my_id, {list_id: new_list_id, ord: new_card_order})
36 |
37 | card_update_hash = Hash.new{ |h, k| h[k] = {} }
38 | new_lists_cards = List.find(new_list_id).cards
39 | sorted_cards = new_lists_cards.sort_by{|card| card.ord}
40 | sorted_cards.each_with_index do |card, i|
41 | card_update_hash[card.id] = { ord: i }
42 | end
43 |
44 | ActiveRecord::Base.transaction do
45 | Card.update(card_update_hash.keys, card_update_hash.values)
46 | end
47 |
48 | from_pile = sort_and_map(List.find(starting_list_id).cards)
49 | to_pile = sort_and_map(new_lists_cards)
50 |
51 | render json: { cardLoad: params[:cardLoad],
52 | cardIds: { fromPile: from_pile, toPile: to_pile } }
53 |
54 | else
55 | render json: {}
56 | end
57 | end
58 |
59 | def sort_and_map(array)
60 | array.sort_by{|card| card.ord}.map{|card| card.id}
61 | end
62 |
63 | def move_params
64 | params.require(:cardLoad).permit!
65 | end
66 |
67 | end
68 |
--------------------------------------------------------------------------------
/frontend/components/head/create_board_dropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import onClickOutside from 'react-onclickoutside';
5 |
6 |
7 | class CreateBoardDropdown extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {modalPresence: false, title: ""};
11 | this.handleToggleClick = this.handleToggleClick.bind(this);
12 | this.handleSubmit = this.handleSubmit.bind(this);
13 | }
14 |
15 | handleToggleClick() {
16 | this.setState(prevState => ({
17 | modalPresence: !prevState.modalPresence
18 | }));
19 | }
20 |
21 | handleSubmit(e){
22 | e.preventDefault();
23 | this.props.createBoard(this.state.title);
24 | this.handleToggleClick();
25 | this.setState({title: ""});
26 | }
27 |
28 | handleClickOutside(e) {
29 | this.setState(prevState => ({
30 | modalPresence: false
31 | }));
32 | }
33 |
34 |
35 | handleChange(field) {
36 | return (e) => {
37 | e.preventDefault();
38 | const newState = Object.assign({}, this.state);
39 | newState[field] = e.currentTarget.value;
40 | this.setState(newState);
41 | };
42 | }
43 |
44 | render() {
45 | let createBoardModal;
46 | if ( this.state.modalPresence === true ){
47 | createBoardModal = (
48 |
49 | Create Board
50 |
51 |
52 | Title
53 |
54 |
58 |
);
59 | }
60 | return (
61 |
62 |
65 | {createBoardModal}
66 |
67 | );
68 | }
69 | }
70 |
71 | export default onClickOutside(CreateBoardDropdown);
72 |
--------------------------------------------------------------------------------
/frontend/components/board_index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import { requestBoards, } from '../actions/board_actions';
5 | import { values, keys } from 'lodash';
6 | import Header from './head/header';
7 |
8 | class BoardIndex extends React.Component{
9 | constructor(props){
10 | super(props);
11 | }
12 |
13 | componentDidMount(){
14 | this.props.requestBoards();
15 | }
16 |
17 | render(){
18 | const {boards, lists, cards, shared_boards} = this.props;
19 | if (Object.keys(boards).length === 0){
20 | return null;
21 | } else {
22 | var boardLinkArray = [];
23 | var boardShareLinkArray = [];
24 | for (let key in boards){
25 | boardLinkArray.push(
26 |
27 | {boards[key].title}
28 |
29 | );
30 | }
31 | for (let key in shared_boards){
32 | boardShareLinkArray.push(
33 |
34 | {shared_boards[key].title}
35 |
36 | );
37 | }
38 |
39 | return(
40 |
41 |
42 |
43 |
44 | Personal and Recent Boards
45 |
46 |
47 | {boardLinkArray}
48 |
49 |
50 |
51 | Shared Boards
52 |
53 |
54 |
55 | {boardShareLinkArray}
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 | }
63 |
64 | const mapStateToProps = (state) => {
65 | return {
66 | boards: state.boards,
67 | lists: state.lists,
68 | cards: state.cards,
69 | shared_boards: state.shared_boards,
70 | };
71 | };
72 |
73 | const mapDispatchToProps = dispatch => {
74 | return {
75 | requestBoards: () => {
76 | return dispatch(requestBoards());
77 | },
78 | };
79 | };
80 |
81 | export default connect(mapStateToProps, mapDispatchToProps)(BoardIndex);
82 |
--------------------------------------------------------------------------------
/frontend/components/card_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 CardEditModal 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, body: props.bodyText};
16 | this.handleEnter = this.handleEnter.bind(this);
17 | }
18 |
19 | toggleModal() {
20 | this.setState(prevState => ({
21 | modalPresence: !prevState.modalPresence
22 | }));
23 | }
24 |
25 | handleModalEdit(e){
26 | e.preventDefault();
27 | this.setState( { body: e.currentTarget.value } );
28 | }
29 |
30 | handleEnter(e){
31 | if (e.key === "Enter" && !e.shiftKey){
32 | e.preventDefault();
33 | this.props.handleCardEditSubmit(this.props.id, this.state.body,
34 | this.props.listId, this.props.cardIndex);
35 | }
36 | }
37 |
38 | onEditSubmit(e){
39 | e.preventDefault();
40 | this.props.handleCardEditSubmit(this.props.id, this.state.body,
41 | this.props.listId, this.props.cardIndex);
42 | }
43 |
44 | render(){
45 | var cardEditModal;
46 | var bodyLength = (this.state.body.length) * 0.55 + 35;
47 | if ( this.state.modalPresence === false ){
48 | cardEditModal = (
49 |
50 |
51 | {this.props.bodyText}
52 |
53 |
54 | );
55 | } else {
56 | cardEditModal = (
57 |
67 | );
68 | }
69 | return(
70 |
71 | {cardEditModal}
72 |
73 | );
74 | }
75 | }
76 |
77 | export default CardEditModal;
78 |
--------------------------------------------------------------------------------
/frontend/util/session_api_util.js:
--------------------------------------------------------------------------------
1 | export const signup = ({username, password}) => {
2 | return $.ajax({
3 | method: 'POST',
4 | url: '/api/users',
5 | data: { user: { username, password }}
6 | });
7 | };
8 |
9 | export const login = ({username, password}) => {
10 | return $.ajax({
11 | method: 'POST',
12 | url: '/api/session',
13 | data: { user: { username, password }}
14 | });
15 | };
16 |
17 | export const boardIndex = ( boards ) => {
18 | return $.ajax({
19 | method: 'GET',
20 | url: '/api/boards',
21 | data: { boards }
22 | });
23 | };
24 |
25 | export const boardShow = (id) => {
26 | return $.ajax({
27 | method: 'GET',
28 | url: `/api/boards/${id}`,
29 | });
30 | };
31 |
32 | export const createBoard = (board) => {
33 | return $.ajax({
34 | method: "POST",
35 | url: '/api/boards',
36 | data: { board }
37 | });
38 | };
39 |
40 | export const createCard = (card) => {
41 | return $.ajax({
42 | method: "POST",
43 | url: '/api/cards',
44 | data: { card },
45 | });
46 | };
47 |
48 | export const moveCard = (cardLoad) => {
49 | return $.ajax({
50 | method: "POST",
51 | url: `/api/moves/`,
52 | data: { cardLoad },
53 | });
54 | };
55 |
56 | export const createList = (list) => {
57 | return $.ajax({
58 | method: "POST",
59 | url: '/api/lists',
60 | data: { list: list } //list sets the key: "list"
61 | });
62 | };
63 |
64 | export const editList = (list) => {
65 | return $.ajax({
66 | method: "PATCH",
67 | url: `/api/lists/${list.id}`,
68 | data: { list: list } //list sets the key: "list"
69 | });
70 | };
71 |
72 |
73 | export const editCard = (card) => {
74 | return $.ajax({
75 | method: "PATCH",
76 | url: `/api/cards/${card.id}`,
77 | data: { card: card },
78 | });
79 | };
80 |
81 | export const logout = () => {
82 | return $.ajax({
83 | method: 'DELETE',
84 | url: '/api/session'
85 | });
86 | };
87 |
88 | export const fetchUsers = () => {
89 | return $.ajax({
90 | method: 'GET',
91 | url: '/api/users'
92 | });
93 | };
94 |
95 | export const addUserToBoard = (boardShareParams) => {
96 | return $.ajax({
97 | method: "POST",
98 | url: '/api/board_shares',
99 | data: { board_share: { user_id: boardShareParams.user_id, board_id: boardShareParams.board_id } }
100 | });
101 | };
102 |
103 | export const removeUserFromBoard = (boardShareParams) => {
104 | return $.ajax({
105 | method: "DELETE",
106 | url: '/api/board_shares',
107 | data: { board_share: { user_id: boardShareParams.user_id, board_id: boardShareParams.board_id } }
108 | });
109 | };
110 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 9.1 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On OS X with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On OS X with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem 'pg'
16 | #
17 | default: &default
18 | adapter: postgresql
19 | encoding: unicode
20 | # For details on connection pooling, see rails configuration guide
21 | # http://guides.rubyonrails.org/configuring.html#database-pooling
22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
23 |
24 | development:
25 | <<: *default
26 | database: QuickBoard_development
27 |
28 | # The specified database role being used to connect to postgres.
29 | # To create additional roles in postgres see `$ createuser --help`.
30 | # When left blank, postgres will use the default role. This is
31 | # the same name as the operating system user that initialized the database.
32 | #username: QuickBoard
33 |
34 | # The password associated with the postgres role (username).
35 | #password:
36 |
37 | # Connect on a TCP socket. Omitted by default since the client uses a
38 | # domain socket that doesn't need configuration. Windows does not have
39 | # domain sockets, so uncomment these lines.
40 | #host: localhost
41 |
42 | # The TCP port the server listens on. Defaults to 5432.
43 | # If your server runs on a different port number, change accordingly.
44 | #port: 5432
45 |
46 | # Schema search path. The server defaults to $user,public
47 | #schema_search_path: myapp,sharedapp,public
48 |
49 | # Minimum log levels, in increasing ord:
50 | # debug5, debug4, debug3, debug2, debug1,
51 | # log, notice, warning, error, fatal, and panic
52 | # Defaults to warning.
53 | #min_messages: notice
54 |
55 | # Warning: The database defined as "test" will be erased and
56 | # re-generated from your development database when you run "rake".
57 | # Do not set this db to the same as development or production.
58 | test:
59 | <<: *default
60 | database: QuickBoard_test
61 |
62 | # As with config/secrets.yml, you never want to store sensitive information,
63 | # like your database password, in your source code. If your source code is
64 | # ever seen by anyone, they now have access to your database.
65 | #
66 | # Instead, provide the password as a unix environment variable when you boot
67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
68 | # for a full rundown on how to provide these environment variables in a
69 | # production deployment.
70 | #
71 | # On Heroku and other platform providers, you may have a full connection URL
72 | # available as an environment variable. For example:
73 | #
74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
75 | #
76 | # You can use this database configuration with:
77 | #
78 | # production:
79 | # url: <%= ENV['DATABASE_URL'] %>
80 | #
81 | production:
82 | <<: *default
83 | database: QuickBoard_production
84 | username: QuickBoard
85 | password: <%= ENV['QuickBoard_DATABASE_PASSWORD'] %>
86 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20170809043134) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "board_shares", force: :cascade do |t|
19 | t.integer "board_id", null: false
20 | t.integer "user_id", null: false
21 | t.datetime "created_at", null: false
22 | t.datetime "updated_at", null: false
23 | t.index ["board_id"], name: "index_board_shares_on_board_id", using: :btree
24 | t.index ["user_id"], name: "index_board_shares_on_user_id", using: :btree
25 | end
26 |
27 | create_table "boards", force: :cascade do |t|
28 | t.integer "author_id", null: false
29 | t.string "title", null: false
30 | t.boolean "privacy_status", default: true
31 | t.datetime "created_at", null: false
32 | t.datetime "updated_at", null: false
33 | t.index ["author_id"], name: "index_boards_on_author_id", using: :btree
34 | end
35 |
36 | create_table "cards", force: :cascade do |t|
37 | t.integer "list_id", null: false
38 | t.float "ord", null: false
39 | t.string "body", null: false
40 | t.date "due_date"
41 | t.boolean "completed", default: false
42 | t.index ["list_id"], name: "index_cards_on_list_id", using: :btree
43 | end
44 |
45 | create_table "comments", force: :cascade do |t|
46 | t.integer "author_id", null: false
47 | t.integer "card_id", null: false
48 | t.string "body", null: false
49 | t.datetime "created_at", null: false
50 | t.datetime "updated_at", null: false
51 | t.index ["author_id"], name: "index_comments_on_author_id", using: :btree
52 | t.index ["card_id"], name: "index_comments_on_card_id", using: :btree
53 | end
54 |
55 | create_table "lists", force: :cascade do |t|
56 | t.integer "board_id", null: false
57 | t.string "title", null: false
58 | t.integer "ord", null: false
59 | t.datetime "created_at", null: false
60 | t.datetime "updated_at", null: false
61 | t.index ["board_id"], name: "index_lists_on_board_id", using: :btree
62 | end
63 |
64 | create_table "users", force: :cascade do |t|
65 | t.string "username", null: false
66 | t.string "password_digest", null: false
67 | t.string "session_token"
68 | t.datetime "created_at", null: false
69 | t.datetime "updated_at", null: false
70 | t.string "name"
71 | t.index ["username"], name: "index_users_on_username", unique: true, using: :btree
72 | end
73 |
74 | end
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mello
2 |
3 | [Mello Live Site][heroku]
4 |
5 | [heroku]: https://melloboard.herokuapp.com
6 |
7 | Mello is a superfast Trello-inspired note-taking application built on ReactJS + Rails and follows a Redux/Flux pattern. Mello shines in showing off the incredible performance benefits of ReactJS and the huge benefits of organizing with React components.
8 |
9 | Additionally, drag and drop is a notoriously difficult front-end problem to get right from a user-experience perspective, and one that I've wanted to work on for a long time.
10 |
11 |
12 | ## Features & Implementation
13 |
14 | ### Lightning-fast
15 |
16 | Mello is substantially faster than other Kanban-style/Trello-style boards.
17 |
18 | Trello -- roughly 1000ms JS response time.
19 |
20 |
21 |
22 |
23 | Mello -- roughly 100ms JS response time. 10x faster!
24 |
25 |
26 |
27 | ### Boards
28 |
29 | Users have boards, which can be shared to many users. The board page navigation is always achievable through the menu bar on the left.
30 |
31 | 
32 |
33 | 
34 |
35 | ### Lists and Cards
36 |
37 | Boards components contain list components, which contain card components, which themselves contain card modal components. Cards can be moved among lists.
38 | Notice the Pinterest-style "masonry" layout here: we can now re-size our browser windows to any size we want and lists will neatly and efficiently re-allocate themselves.
39 |
40 | 
41 |
42 | 
43 |
44 |
45 | ### Drag and Drop
46 |
47 | Implementing drag & drop uses the ReactDnD library for mouse positioning and drag-detection only. All other logic, such as list ordering, event handling, and back-end updates were written natively.
48 |
49 | Drag & drop is mostly cleanly implemented by having the back-end drive the front-end view, with the front-end passively listening for a JSON response object to render. This reduces the possibility of front-end state becoming out-of-sync.
50 |
51 |
52 | ### Board Sharing
53 |
54 | Board sharing is achieved through a join table, `board_shares`, and the JSON response is provided through the BoardIndex routes.
55 |
56 | ```ruby
57 |
58 | @boards = current_user.boards.includes(:lists)
59 | @shared_boards = current_user.shared_boards.includes(:lists)
60 |
61 | ...
62 |
63 | json.set! :boards do
64 | @boards.each do |board|
65 | json.set! board.id do
66 | json.author_id board.author_id
67 | json.title board.title
68 | json.listIds board.lists.map{|el| el.id}
69 | end
70 | end
71 | end
72 | json.set! :shared_boards do
73 | @shared_boards.each do |board|
74 | json.set! board.id do
75 | json.author_id board.author_id
76 | json.title board.title
77 | json.listIds board.lists.map{|el| el.id}
78 | end
79 | end
80 | end
81 |
82 | ```
83 |
84 | ## Future Directions for the Project
85 |
86 | In addition to the features already implemented, I plan to continue work on this project. The next steps for Mello are outlined below.
87 |
88 | ### Search
89 |
90 | I plan to implement fuzzy searching across shared boards, lists, and cards using Fuse.js.
91 |
92 | ### Async Drag & Drop
93 | Drag and drop is best executed asynchronously for ideal user experience. A back-end-driven response cycle will create perceived latency. In the future, I plan to execute front-end JS animations while asynchronously executing back-end updates, to create the perception of instantaneous drag-and-drop.
94 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Disable serving static files from the `/public` folder by default since
18 | # Apache or NGINX already handles this.
19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
20 |
21 | # Compress JavaScripts and CSS.
22 | config.assets.js_compressor = :uglifier
23 | # config.assets.css_compressor = :sass
24 |
25 | # Do not fallback to assets pipeline if a precompiled asset is missed.
26 | config.assets.compile = false
27 |
28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
29 |
30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
31 | # config.action_controller.asset_host = 'http://assets.example.com'
32 |
33 | # Specifies the header that your server uses for sending files.
34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
36 |
37 | # Mount Action Cable outside main process or domain
38 | # config.action_cable.mount_path = nil
39 | # config.action_cable.url = 'wss://example.com/cable'
40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
41 |
42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
43 | # config.force_ssl = true
44 |
45 | # Use the lowest log level to ensure availability of diagnostic information
46 | # when problems arise.
47 | config.log_level = :debug
48 |
49 | # Prepend all log lines with the following tags.
50 | config.log_tags = [ :request_id ]
51 |
52 | # Use a different cache store in production.
53 | # config.cache_store = :mem_cache_store
54 |
55 | # Use a real queuing backend for Active Job (and separate queues per environment)
56 | # config.active_job.queue_adapter = :resque
57 | # config.active_job.queue_name_prefix = "QuickBoard_#{Rails.env}"
58 | config.action_mailer.perform_caching = false
59 |
60 | # Ignore bad email addresses and do not raise email delivery errors.
61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
62 | # config.action_mailer.raise_delivery_errors = false
63 |
64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
65 | # the I18n.default_locale when a translation cannot be found).
66 | config.i18n.fallbacks = true
67 |
68 | # Send deprecation notices to registered listeners.
69 | config.active_support.deprecation = :notify
70 |
71 | # Use default logging formatter so that PID and timestamp are not suppressed.
72 | config.log_formatter = ::Logger::Formatter.new
73 |
74 | # Use a different logger for distributed setups.
75 | # require 'syslog/logger'
76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
77 |
78 | if ENV["RAILS_LOG_TO_STDOUT"].present?
79 | logger = ActiveSupport::Logger.new(STDOUT)
80 | logger.formatter = config.log_formatter
81 | config.logger = ActiveSupport::TaggedLogging.new(logger)
82 | end
83 |
84 | # Do not dump schema after migrations.
85 | config.active_record.dump_schema_after_migration = false
86 | end
87 |
--------------------------------------------------------------------------------
/frontend/components/head/header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BoardMenuDropdown from './board_menu_dropdown';
3 | import Greeting from '../greeting';
4 | import UserMenu from './user_menu';
5 | import BoardSharingDropdown from './board_sharing_dropdown';
6 | import CreateBoardDropdown from './create_board_dropdown';
7 | import { createBoard, requestBoards } from '../../actions/board_actions';
8 | import { requestUsers } from '../../actions/user_actions';
9 | import { addUserToBoard } from '../../actions/board_share_actions';
10 | import { connect } from 'react-redux';
11 | import { Link } from 'react-router-dom';
12 |
13 | class Header extends React.Component{
14 | constructor(props){
15 | super(props);
16 | }
17 |
18 | componentDidMount(){
19 | this.props.requestBoards();
20 | // this.props.requestUsers();
21 | }
22 |
23 | render(){
24 | const {boards, lists, cards} = this.props;
25 | var boardLinkArray = [];
26 | var menuPropsArray = [];
27 |
28 | if (Object.keys(this.props.boards).length !== 0){
29 | for (let key in boards){
30 | let titleString = boards[key].title;
31 | let outputString;
32 | if (titleString.length > 20){
33 | outputString = titleString.slice(0,20)+"...";
34 | } else {
35 | outputString = titleString;
36 | }
37 | menuPropsArray.push(
38 |
39 | {outputString}
40 |
41 | );
42 | boardLinkArray.push(
43 |
44 | {boards[key].title}
45 |
46 | );
47 | }
48 | }
49 | let boardSharingDropdown;
50 | if (this.props.location.pathname === "/home"){
51 | boardSharingDropdown = null;
52 | } else {
53 | boardSharingDropdown = ;
59 | }
60 |
61 | return(
62 |
63 |
64 |
65 |
66 | Mello
67 |
68 |
69 | {boardSharingDropdown}
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 |
77 | const mapStateToProps = (state) => {
78 | return {
79 | boards: state.boards,
80 | lists: state.lists,
81 | cards: state.cards,
82 | users: state.users,
83 | shared_boards: state.shared_boards,
84 | shared_users: state.users.shared_users,
85 | unshared_users: state.users.unshared_users
86 | };
87 | };
88 |
89 | const mapDispatchToProps = dispatch => {
90 | return {
91 | createBoard: (title) => {
92 | return dispatch(createBoard({title: title, privacy_status: false, listIds: []}));
93 | },
94 | requestBoards: () => {
95 | return dispatch(requestBoards());
96 | },
97 | requestUsers: () => {
98 | return dispatch(requestUsers());
99 | },
100 | addUserToBoard: (boardShareParams) => {
101 | return dispatch(addUserToBoard(boardShareParams));
102 | },
103 | };
104 | };
105 |
106 | export default connect(mapStateToProps, mapDispatchToProps)(Header);
107 |
--------------------------------------------------------------------------------
/frontend/components/head/board_sharing_dropdown.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router-dom';
4 | import { keys, merge, values, isEmpty } from 'lodash';
5 | import onClickOutside from 'react-onclickoutside';
6 |
7 |
8 | class BoardSharingDropdown extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {modalPresence: false, title: "", availableUsers: []};
12 | this.handleToggleClick = this.handleToggleClick.bind(this);
13 | this.handleSubmit = this.handleSubmit.bind(this);
14 | }
15 |
16 | handleToggleClick() {
17 | this.setState(prevState => ({
18 | modalPresence: !prevState.modalPresence
19 | }));
20 | }
21 |
22 | handleSubmit(boardShareParams) {
23 | return (e) => {
24 | e.preventDefault();
25 | this.props.addUserToBoard(boardShareParams);
26 | };
27 | }
28 |
29 | handleChange(field) {
30 | return (e) => {
31 | e.preventDefault();
32 | const newState = Object.assign({}, this.state);
33 | newState[field] = e.currentTarget.value;
34 | this.setState(newState);
35 | };
36 | }
37 |
38 | handleClickOutside(e) {
39 | this.setState(prevState => ({
40 | modalPresence: false
41 | }));
42 | }
43 |
44 | render() {
45 | if (isEmpty(this.props.users)) {
46 | return null;
47 | }
48 | let boardSharingModal;
49 | let availabilityButton;
50 | let unshared_users_output = [];
51 | if (this.props.unshared_users.unshared_user_ids){
52 | for (let i = 0; i < this.props.unshared_users.unshared_user_ids.length; i++) {
53 | let boardShareParams = {user_id: null, board_id: this.props.boardId };
54 | boardShareParams['user_id'] = parseInt(this.props.unshared_users.unshared_user_ids[i]);
55 | let currentUserName = this.props.unshared_users.unshared_usernames[i]
56 | unshared_users_output.push(
57 |
58 |
61 |
62 | );
63 | }
64 | }
65 |
66 | let shared_users_output = [];
67 | if (this.props.shared_users.shared_user_ids){
68 | for (let i = 0; i < this.props.shared_users.shared_user_ids.length; i++) {
69 | let boardShareParams = {user_id: null, board_id: this.props.boardId };
70 | boardShareParams['user_id'] = parseInt(this.props.shared_users.shared_user_ids[i]);
71 | let currentUnsharedUserName = this.props.unshared_users.unshared_usernames[i]
72 | shared_users_output.push(
73 |
74 |
77 |
78 | );
79 | }
80 | }
81 |
82 |
83 | if ( this.state.modalPresence === true ){
84 | boardSharingModal = (
85 |
86 | Add Users to Board
87 |
88 |
89 |
90 | Add Users to Board
91 |
92 |
93 | {unshared_users_output}
94 |
95 |
96 |
97 | Remove Users from Board
98 |
99 |
100 |
101 | {shared_users_output}
102 |
103 |
104 | );
105 | }
106 | return (
107 |
108 |
111 | {boardSharingModal}
112 |
113 | );
114 | }
115 | }
116 |
117 |
118 | export default onClickOutside(BoardSharingDropdown);
119 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/board_show.scss:
--------------------------------------------------------------------------------
1 | $offwhite: #EDEFF0;
2 | $light-grey: #DCDCDC;
3 | $babby-blue: #C7E2F1;
4 | $header-blue: #026AA7;
5 | $dark-grey: #505050;
6 | $menu-hover-blue:#298FCA;
7 | $list-grey: #E2E4E6;
8 |
9 | .board-show-title{
10 | background-color: #0079BF;
11 | color: white;
12 | font-family: "Helvetica Neue";
13 | font-size: 17px;
14 | font-weight: bold;
15 | padding-top: 10px;
16 | padding-left: 20px;
17 |
18 | }
19 |
20 | .board-show-wrapper{
21 | overflow-x: auto;
22 | overflow-y: auto;
23 | height: auto;
24 | }
25 |
26 | .board-show-container{
27 | min-height: 100vh;
28 | display: flex;
29 | flex-direction: row;
30 | justify-content: flex-start;
31 | color: white;
32 | font-family: "Helvetica Neue";
33 | height: 100%;
34 | // padding-left: 10px;
35 | // padding-top: 14px;
36 | background-color: #0079BF;
37 | flex-wrap: wrap;
38 | align-items: flex-start;
39 | margin: 0 auto;
40 | background-color: #0079BF;
41 | // background: linear-gradient(180deg, #0079BF, #C7E2F1);
42 | padding-left: 5px;
43 |
44 | }
45 |
46 | .list-element{
47 | padding: 10px 7px 7px;
48 | background-color: $list-grey;
49 | color: $dark-grey;
50 | margin: 5px;
51 | margin-bottom: 8px;
52 | border-radius: 3px;
53 | width: 270px;
54 | // flex: 1 0 auto; // <--
55 | // position: relative;
56 | // break-inside: avoid;
57 | padding: 8px;
58 |
59 | }
60 |
61 | .list-title-element{
62 | margin-bottom: 7px;
63 | font-weight: bolder;
64 | margin-left: 3px;
65 | font-size: 14px;
66 | color: $dark-grey;
67 | word-break: break-all;
68 | }
69 |
70 |
71 |
72 | .card-item-element, .add-card-input-element{
73 | background-color: white;
74 | margin-bottom: 4px;
75 | width: 100%;
76 | padding: 7px;
77 | border-radius: 3px;
78 | border: 1px solid $light-grey;
79 | box-shadow: 0px 0.5px $light-grey;
80 | font-size: 13px;
81 | color: $dark-grey;
82 | white-space: pre-wrap;
83 |
84 | word-wrap: break-word;
85 | overflow-wrap: break-word;
86 | }
87 |
88 | .card-item-element-parent-grab{
89 | position: relative;
90 | width: 100%;
91 | height: 180%;
92 | min-height: 20px;
93 | // outline: 1px solid black;
94 | }
95 |
96 | .card-item-element:hover{
97 | background-color: #EDEFF0;
98 | }
99 |
100 |
101 | .add-card-button-container{
102 |
103 |
104 | }
105 |
106 |
107 | textarea.add-card-input-element{
108 | background-color: white;
109 | margin-bottom: 4px;
110 | padding-left: 7px;
111 | padding-top: 7px;
112 | padding-bottom: 7px;
113 | border-radius: 3px;
114 | color: blue;
115 | border: 1px solid $light-grey;
116 | box-shadow: 0px 0.5px $light-grey;
117 | font-size: 13px;
118 | color: $dark-grey;
119 | word-break: normal;
120 |
121 | width: 100%;
122 | //copy this for other textareas
123 | overflow: hidden;
124 | outline: none;
125 | resize: vertical;
126 |
127 |
128 | }
129 |
130 |
131 |
132 | .add-card-button-element{
133 | margin-top: 7px;
134 | height: 30px;
135 | width: 60px;
136 | background-color: #61BD4F;
137 | box-shadow: 0px 0.5px #0C3953;
138 | border: none;
139 | color: white;
140 | border-radius: 3px;
141 | font-weight: bolder;
142 | font-size: 15px;
143 | }
144 |
145 | .add-list-title{
146 | color: $dark-grey;
147 | padding-top: 2px;
148 | padding-left: 2px;
149 | font-weight: bold;
150 | padding-bottom: 4px;
151 | }
152 |
153 | .add-list-button-container{
154 | display: flex;
155 | flex-direction: column;
156 | height: 145px;
157 | padding: 7px;
158 | font-size: 14px;
159 | text-align: left;
160 | vertical-align: text-top;
161 | width: 270px;
162 | border: none;
163 | margin-left: 4px;
164 | background-color: $light-grey;
165 | border-radius: 3px;
166 | margin-top: 5px;
167 | }
168 |
169 | .add-list-input-element{
170 | margin-top: 2px;
171 | height: 65px;
172 | padding: 7px;
173 | text-align: left;
174 | vertical-align: text-top;
175 | width: 100%;
176 | border-radius: 3px;
177 | border: none;
178 | outline: none;
179 | font-size: 13px;
180 | resize: vertical;
181 |
182 | }
183 |
184 | .add-list-button-element{
185 | margin-top: 10px;
186 | height: 30px;
187 | width: 60px;
188 | background-color: #61BD4F;
189 | box-shadow: 0px 0.5px #0C3953;
190 | border: none;
191 | color: white;
192 | border-radius: 3px;
193 | font-weight: bolder;
194 | font-size: 15px;
195 |
196 | }
197 |
198 | .card-item-element-true-modal{
199 | background-color: white;
200 | margin-bottom: 4px;
201 | width: 100%;
202 | padding: 7px;
203 | border-radius: 3px;
204 | border: 1px solid $light-grey;
205 | box-shadow: 0px 0.5px $light-grey;
206 | font-size: 13px;
207 | color: $dark-grey;
208 | word-break: normal;
209 | }
210 |
211 | .card-item-element-input, .list-item-element-input{
212 | background-color: white;
213 | margin-bottom: 4px;
214 | padding: 7px;
215 | border-radius: 3px;
216 | border: 1px solid $light-grey;
217 | box-shadow: 0px 0.5px $light-grey;
218 | font-size: 13px;
219 | color: $dark-grey;
220 | word-break: normal;
221 | min-height: 1.1em;
222 | width: 100%;
223 |
224 | //copy this for other textareas
225 | overflow: hidden;
226 | outline: none;
227 | resize: vertical;
228 | }
229 |
230 | .list-item-element-input {
231 | font-weight: bold;
232 | background-color: $list-grey;
233 | }
234 |
235 | .opacity-handler{
236 | opacity: 1;
237 | }
238 |
239 | .grey-box{
240 | width: 200px;
241 | height: 100px;
242 | background-color: grey;
243 |
244 | }
245 |
--------------------------------------------------------------------------------
/frontend/components/session_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link, withRouter, Redirect } from 'react-router-dom';
4 | import {login, signup, clearErrors} from '../actions/session_actions';
5 |
6 |
7 | class SessionForm extends React.Component{
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | username: "",
12 | password: ""
13 | };
14 | this.handleSubmit = this.handleSubmit.bind(this);
15 | this.handleChange = this.handleChange.bind(this);
16 | this.handleDemo = this.handleDemo.bind(this);
17 | this.processForm = this.props.processForm.bind(this);
18 | this.formType = this.props.formType
19 | }
20 |
21 | handleSubmit(e) {
22 | e.preventDefault();
23 | const user = Object.assign({}, this.state);
24 | this.props.processForm(this.props.formType, user);
25 | }
26 |
27 | handleDemo(e) {
28 | let that = this;
29 | e.preventDefault();
30 | // let prom = new Promise(function (resolve, reject) {
31 |
32 | // });
33 | this.setState(
34 | { username: 'demo@mello.com', password: 'password' },
35 | () => {
36 | const user = Object.assign({}, this.state);
37 | setTimeout(function () {
38 | that.props.processForm('login', user); }, 1000);
39 | });
40 | }
41 |
42 | handleDemo(e) {
43 | let that = this;
44 | e.preventDefault();
45 |
46 | this.setState(
47 | { username: 'demo@mello.com', password: 'password' },
48 | () => {
49 | const user = Object.assign({}, this.state);
50 | that.props.processForm('login', user);
51 | });
52 | }
53 |
54 | //
55 | // handleDemo(e) {
56 | // e.preventDefault();
57 | // const user = Object.assign({}, this.state);
58 | // this.setState(
59 | // { username: 'demo@mello.com', password: 'password' },
60 | // () => this.props.processForm(this.props.formType, user)
61 | // );
62 | // }
63 |
64 | handleChange(field) {
65 | return (e) => {
66 | e.preventDefault();
67 | const newState = Object.assign({}, this.state);
68 | newState[field] = e.currentTarget.value;
69 | this.setState(newState);
70 | };
71 | }
72 |
73 | componentWillReceiveProps(nextProps){
74 | if(this.props.location.pathname !== nextProps.location.pathname){
75 | this.props.clearErrors();
76 | }
77 | }
78 |
79 | render() {
80 | if (this.props.loggedIn) {
81 | return (
82 |
83 | );
84 | }
85 |
86 |
87 | const formType = this.props.formType;
88 | let header, label, link;
89 |
90 | if (formType === 'login') {
91 | header = "Log in to Mello";
92 | label = "Log In";
93 | } else if (formType === 'signup') {
94 | header = "Sign Up for Mello";
95 | label = "Sign Up";
96 | }
97 | // debugger
98 |
99 | const errors = this.props.errors.map((error, idx) => {error});
100 |
101 | let errorsWrapped;
102 | if (errors.length > 0 ){
103 | errorsWrapped = ( {errors}
);
104 | } else {
105 | errorsWrapped = "";
106 | }
107 |
108 | const {username, password} = this.state;
109 | return (
110 |
111 |
112 |
113 | Welcome to Mello!
114 |
115 |
116 | {header}
117 |
118 |
119 |
150 |
151 |
152 |
153 |
154 | );
155 | }
156 |
157 | }
158 |
159 | const mapStateToProps = (state, ownProps) => {
160 | return ({
161 | loggedIn: Boolean(state.session.currentUser),
162 | errors: state.session.errors,
163 | formType: ownProps.location.pathname.slice(1)
164 | });
165 | };
166 |
167 | const mapDispatchToProps = (dispatch, ownProps) => ({
168 | processForm: (formType, user) => {
169 | if (formType === 'login'){
170 | dispatch(login(user));
171 | } else if (formType === 'signup') {
172 | dispatch(signup(user));
173 | }
174 | },
175 | clearErrors: () => {
176 | dispatch(clearErrors());
177 | }
178 | });
179 |
180 | export default withRouter(connect(
181 | mapStateToProps,
182 | mapDispatchToProps)(SessionForm));
183 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.0.4)
5 | actionpack (= 5.0.4)
6 | nio4r (>= 1.2, < 3.0)
7 | websocket-driver (~> 0.6.1)
8 | actionmailer (5.0.4)
9 | actionpack (= 5.0.4)
10 | actionview (= 5.0.4)
11 | activejob (= 5.0.4)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 2.0)
14 | actionpack (5.0.4)
15 | actionview (= 5.0.4)
16 | activesupport (= 5.0.4)
17 | rack (~> 2.0)
18 | rack-test (~> 0.6.3)
19 | rails-dom-testing (~> 2.0)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.0.4)
22 | activesupport (= 5.0.4)
23 | builder (~> 3.1)
24 | erubis (~> 2.7.0)
25 | rails-dom-testing (~> 2.0)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
27 | activejob (5.0.4)
28 | activesupport (= 5.0.4)
29 | globalid (>= 0.3.6)
30 | activemodel (5.0.4)
31 | activesupport (= 5.0.4)
32 | activerecord (5.0.4)
33 | activemodel (= 5.0.4)
34 | activesupport (= 5.0.4)
35 | arel (~> 7.0)
36 | activesupport (5.0.4)
37 | concurrent-ruby (~> 1.0, >= 1.0.2)
38 | i18n (~> 0.7)
39 | minitest (~> 5.1)
40 | tzinfo (~> 1.1)
41 | annotate (2.7.2)
42 | activerecord (>= 3.2, < 6.0)
43 | rake (>= 10.4, < 13.0)
44 | arel (7.1.4)
45 | bcrypt (3.1.11)
46 | better_errors (2.1.1)
47 | coderay (>= 1.0.0)
48 | erubis (>= 2.6.6)
49 | rack (>= 0.9.0)
50 | bindex (0.5.0)
51 | binding_of_caller (0.7.2)
52 | debug_inspector (>= 0.0.1)
53 | builder (3.2.3)
54 | byebug (9.0.6)
55 | coderay (1.1.1)
56 | coffee-rails (4.2.2)
57 | coffee-script (>= 2.2.0)
58 | railties (>= 4.0.0)
59 | coffee-script (2.4.1)
60 | coffee-script-source
61 | execjs
62 | coffee-script-source (1.12.2)
63 | concurrent-ruby (1.0.5)
64 | debug_inspector (0.0.3)
65 | erubis (2.7.0)
66 | execjs (2.7.0)
67 | ffi (1.9.18)
68 | font-awesome-sass (4.7.0)
69 | sass (>= 3.2)
70 | globalid (0.4.0)
71 | activesupport (>= 4.2.0)
72 | i18n (0.8.4)
73 | jbuilder (2.7.0)
74 | activesupport (>= 4.2.0)
75 | multi_json (>= 1.2)
76 | jquery-rails (4.3.1)
77 | rails-dom-testing (>= 1, < 3)
78 | railties (>= 4.2.0)
79 | thor (>= 0.14, < 2.0)
80 | listen (3.0.8)
81 | rb-fsevent (~> 0.9, >= 0.9.4)
82 | rb-inotify (~> 0.9, >= 0.9.7)
83 | loofah (2.0.3)
84 | nokogiri (>= 1.5.9)
85 | mail (2.6.6)
86 | mime-types (>= 1.16, < 4)
87 | method_source (0.8.2)
88 | mime-types (3.1)
89 | mime-types-data (~> 3.2015)
90 | mime-types-data (3.2016.0521)
91 | mini_portile2 (2.2.0)
92 | minitest (5.10.2)
93 | multi_json (1.12.1)
94 | nio4r (2.1.0)
95 | nokogiri (1.8.0)
96 | mini_portile2 (~> 2.2.0)
97 | pg (0.21.0)
98 | pry (0.10.4)
99 | coderay (~> 1.1.0)
100 | method_source (~> 0.8.1)
101 | slop (~> 3.4)
102 | pry-rails (0.3.6)
103 | pry (>= 0.10.4)
104 | puma (3.9.1)
105 | rack (2.0.3)
106 | rack-test (0.6.3)
107 | rack (>= 1.0)
108 | rails (5.0.4)
109 | actioncable (= 5.0.4)
110 | actionmailer (= 5.0.4)
111 | actionpack (= 5.0.4)
112 | actionview (= 5.0.4)
113 | activejob (= 5.0.4)
114 | activemodel (= 5.0.4)
115 | activerecord (= 5.0.4)
116 | activesupport (= 5.0.4)
117 | bundler (>= 1.3.0, < 2.0)
118 | railties (= 5.0.4)
119 | sprockets-rails (>= 2.0.0)
120 | rails-dom-testing (2.0.3)
121 | activesupport (>= 4.2.0)
122 | nokogiri (>= 1.6)
123 | rails-html-sanitizer (1.0.3)
124 | loofah (~> 2.0)
125 | rails_12factor (0.0.3)
126 | rails_serve_static_assets
127 | rails_stdout_logging
128 | rails_serve_static_assets (0.0.5)
129 | rails_stdout_logging (0.0.5)
130 | railties (5.0.4)
131 | actionpack (= 5.0.4)
132 | activesupport (= 5.0.4)
133 | method_source
134 | rake (>= 0.8.7)
135 | thor (>= 0.18.1, < 2.0)
136 | rake (12.0.0)
137 | rb-fsevent (0.9.8)
138 | rb-inotify (0.9.10)
139 | ffi (>= 0.5.0, < 2)
140 | sass (3.4.24)
141 | sass-rails (5.0.6)
142 | railties (>= 4.0.0, < 6)
143 | sass (~> 3.1)
144 | sprockets (>= 2.8, < 4.0)
145 | sprockets-rails (>= 2.0, < 4.0)
146 | tilt (>= 1.1, < 3)
147 | slop (3.6.0)
148 | spring (2.0.2)
149 | activesupport (>= 4.2)
150 | spring-watcher-listen (2.0.1)
151 | listen (>= 2.7, < 4.0)
152 | spring (>= 1.2, < 3.0)
153 | sprockets (3.7.1)
154 | concurrent-ruby (~> 1.0)
155 | rack (> 1, < 3)
156 | sprockets-rails (3.2.0)
157 | actionpack (>= 4.0)
158 | activesupport (>= 4.0)
159 | sprockets (>= 3.0.0)
160 | thor (0.19.4)
161 | thread_safe (0.3.6)
162 | tilt (2.0.7)
163 | tzinfo (1.2.3)
164 | thread_safe (~> 0.1)
165 | uglifier (3.2.0)
166 | execjs (>= 0.3.0, < 3)
167 | web-console (3.5.1)
168 | actionview (>= 5.0)
169 | activemodel (>= 5.0)
170 | bindex (>= 0.4.0)
171 | railties (>= 5.0)
172 | websocket-driver (0.6.5)
173 | websocket-extensions (>= 0.1.0)
174 | websocket-extensions (0.1.2)
175 |
176 | PLATFORMS
177 | ruby
178 |
179 | DEPENDENCIES
180 | annotate
181 | bcrypt (~> 3.1.7)
182 | better_errors
183 | binding_of_caller
184 | byebug
185 | coffee-rails (~> 4.2)
186 | font-awesome-sass
187 | jbuilder (~> 2.5)
188 | jquery-rails
189 | listen (~> 3.0.5)
190 | pg (~> 0.18)
191 | pry-rails
192 | puma (~> 3.0)
193 | rails (~> 5.0.3)
194 | rails_12factor
195 | sass-rails (~> 5.0)
196 | spring
197 | spring-watcher-listen (~> 2.0.0)
198 | tzinfo-data
199 | uglifier (>= 1.3.0)
200 | web-console (>= 3.3.0)
201 |
202 | BUNDLED WITH
203 | 1.14.6
204 |
--------------------------------------------------------------------------------
/frontend/components/board_show.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import { values } from 'lodash';
4 | import Header from './head/header';
5 | import List from './list';
6 | import { requestBoard } from '../actions/board_actions';
7 | import { createList, editListText } from '../actions/list_actions';
8 | import { createCard, receiveCardEdit, editCardText } from '../actions/card_actions';
9 | import onClickOutside from 'react-onclickoutside';
10 | import Masonry from 'react-masonry-component';
11 |
12 | class BoardShow extends React.Component{
13 | constructor(props){
14 | super(props);
15 | this.state = { listTitle: ""};
16 | this.handleCreateListTitleChange = this.handleCreateListTitleChange.bind(this);
17 | this.handleCreateList = this.handleCreateList.bind(this);
18 | this.handleEnter = this.handleEnter.bind(this);
19 | }
20 |
21 | handleCreateListTitleChange(e){
22 | e.preventDefault();
23 | this.setState( { listTitle: e.currentTarget.value } );
24 | }
25 |
26 | handleCreateList(e) {
27 | e.preventDefault();
28 | const boardId = parseInt(this.props.match.params.id);
29 | this.props.createList(boardId, this.props.board.listIds.length, this.state.listTitle);
30 | this.setState( { listTitle: "" } );
31 | }
32 |
33 | handleEnter(e){
34 | if (e.key === "Enter" && !e.shiftKey){
35 | e.preventDefault();
36 | const boardId = parseInt(this.props.match.params.id);
37 | this.props.createList(boardId, this.props.board.listIds.length, this.state.listTitle);
38 | this.setState( { listTitle: "" } );
39 | }
40 | }
41 |
42 | componentDidMount(){
43 | let boardId = parseInt(this.props.match.params.id);
44 | this.props.requestBoard(boardId);
45 | }
46 |
47 | componentWillReceiveProps(nextProps){
48 | const nextPropsId = nextProps.match.params.id;
49 | if (this.props.match.params.id !== nextPropsId) {
50 | this.props.requestBoard(parseInt(nextPropsId));
51 | }
52 | }
53 |
54 | render() {
55 | const {board, lists, cards, hovering} = this.props;
56 | let outputListArray = [];
57 | let addCardElement = (
58 | < form key={ 1001 } value="Add a List" onSubmit={this.handleCreateList} className="add-list-button-container">
59 |
60 | Add a List
61 |
62 |
63 |
64 |
65 | )
66 | let boardTitle = "";
67 | if ((Object.keys(lists).length > 0) && board && (parseInt(this.props.match.params.id) === parseInt(board.id) )) {
68 | boardTitle = (this.props.board.title || "");
69 | for (let key in lists) {
70 | let listObj = lists[key];
71 | outputListArray.push(
72 |
);
81 | }
82 | }
83 | outputListArray.push(addCardElement);
84 |
85 | var masonryOptions = {
86 | transitionDuration: 0,
87 | gutter: 3,
88 | fitWidth: true,
89 | horizontalOrder: true,
90 | stagger: 300,
91 |
92 | };
93 |
94 | return (
95 |
96 |
97 | {boardTitle}
98 |
99 |
100 |
107 | {outputListArray}
108 |
109 |
110 |
111 |
112 |
113 | );
114 | }
115 | }
116 |
117 |
118 |
119 | const mapStateToProps = (state, ownProps) => {
120 |
121 | let ownLists = {};
122 | for (let key in state.lists){
123 | if (key === ownProps.match.params.id){
124 | ownLists[key] = state.lists[key];
125 | }
126 | }
127 | return {
128 | board: state.boards[ownProps.match.params.id],
129 | lists: state.lists, // select the lists for this board - write a SELECTOR
130 | cards: state.cards, // select the cards for these lists
131 | hovering: state.hover
132 | };
133 | };
134 |
135 |
136 | const mapDispatchToProps = (dispatch) => {
137 | return {
138 | requestBoard: (id) => {
139 | return dispatch(requestBoard(id));
140 | },
141 | createList: (board_id, ord, title) => {
142 | return dispatch(createList({ board_id: board_id, ord: ord, title: title} ));
143 | },
144 | handleCardEditSubmit: (card_id, body, list_id, order) => {
145 |
146 | return dispatch( editCardText( {id: card_id, body: body, list_id: list_id, order: order }) );
147 |
148 | },
149 | handleListEditSubmit: (listId, title) => {
150 | return dispatch( editListText( {id: listId, title: title} ) )
151 | },
152 | createCard: (list_id, ord, body) => {
153 | return dispatch(createCard({ list_id: list_id, ord: ord, body: body, due_date: null, completed: false } ));
154 | },
155 |
156 | };
157 |
158 | };
159 |
160 |
161 | export default connect(mapStateToProps, mapDispatchToProps)(BoardShow);
162 |
--------------------------------------------------------------------------------
/frontend/components/card.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 | import { moveCard, renderCardMove } from '../actions/card_actions';
9 | import { generateDropZone } from '../actions/hover_actions';
10 | import CardEditModal from './card_edit_modal';
11 |
12 |
13 | // const style = {
14 | // border: 'none',
15 | // padding: 'none',
16 | // marginBottom: '.5rem',
17 | // backgroundColor: 'none',
18 | // cursor: 'pointer',
19 | // opacity: 1.0,
20 | // };
21 |
22 | const ItemTypes = {
23 | CARD: 'card',
24 | LIST: 'list',
25 | };
26 | var myState = {starting: {}, ending: {}};
27 | var movedCardId;
28 | var startingListId;
29 | var updatedIndex;
30 | var cardDroppedOnId;
31 | var endingListId;
32 | var dropProps;
33 | var beginDragProps;
34 |
35 |
36 | const cardSource = {
37 | beginDrag(props) {
38 | beginDragProps = props;
39 | movedCardId = props.id;
40 | startingListId = props.listId;
41 | return {
42 | card_id: props.id,
43 | cardIndex: props.cardIndex,
44 | listIndex: props.listId,
45 | };
46 | },
47 | endDrag: function (props, monitor, component) {
48 |
49 |
50 | if (!monitor.didDrop()) {
51 | return;
52 | }
53 |
54 | const item = monitor.getItem();
55 |
56 | var dropResult = monitor.getDropResult();
57 |
58 | myState = {id: movedCardId, list_id: endingListId, ord: updatedIndex,
59 | starting: beginDragProps, ending: dropProps};
60 |
61 |
62 | let fromPile = props.lists[startingListId].cardIds.filter( (cardId) => {
63 | return cardId !== movedCardId;
64 | });
65 |
66 | let toPile = props.lists[endingListId].cardIds.filter( (cardId) => {
67 | return cardId !== movedCardId;
68 | });
69 |
70 | toPile.splice(updatedIndex, 0, movedCardId);
71 |
72 | let cardParams = { cardLoad: {starting: {listId: startingListId},
73 | ending: {listId: endingListId}},
74 | cardIds: { toPile: toPile, fromPile: fromPile } };
75 |
76 |
77 | props.moveCard(myState, cardParams);
78 |
79 | }
80 |
81 | };
82 |
83 |
84 | const cardTarget = {
85 |
86 | drop(props, monitor, component) {
87 | if (monitor.didDrop()) {
88 | return;
89 | }
90 | const item = monitor.getItem();
91 |
92 | cardDroppedOnId = props.id;
93 | endingListId = props.listId;
94 | dropProps = props;
95 |
96 | if (startingListId === endingListId){
97 | updatedIndex = props.cardIndex;
98 | } else {
99 | updatedIndex = props.cardIndex + 1;
100 | }
101 |
102 | return { moved: true };
103 | },
104 |
105 | hover(props, monitor, component) {
106 | const cardStartingIndex = monitor.getItem().cardIndex;
107 | const listStartingIndex = monitor.getItem().listIndex;
108 |
109 | const cardHoverIndex = props.cardIndex;
110 | const listHoverIndex = props.listId;
111 | // console.log(props);
112 | // props.dropZone({cardHoverIndex: props.id, listHoverIndex: listHoverIndex});
113 | if (cardStartingIndex === cardHoverIndex) {
114 | return;
115 | }
116 |
117 | // Determine rectangle on screen
118 | const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
119 |
120 | // Get vertical middle
121 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
122 |
123 | // Determine mouse position
124 | const clientOffset = monitor.getClientOffset();
125 |
126 | // Get pixels to the top
127 | const hoverClientY = clientOffset.y - hoverBoundingRect.top;
128 |
129 | // Only perform the move when the mouse has crossed half of the items height
130 | // When dragging downwards, only move when the cursor is below 50%
131 | // When dragging upwards, only move when the cursor is above 50%
132 |
133 | // Dragging downwards
134 | if (cardStartingIndex < cardHoverIndex && hoverClientY < hoverMiddleY) {
135 | return;
136 | }
137 |
138 | // Dragging upwards
139 | if (cardStartingIndex > cardHoverIndex && hoverClientY > hoverMiddleY) {
140 | return;
141 | }
142 |
143 | },
144 | };
145 |
146 | class Card extends React.Component{
147 | constructor(props){
148 | super(props);
149 | this.state = { modalPresence: false, title: "", greyCard: false };
150 | }
151 |
152 | handleToggleClick() {
153 | this.setState(prevState => ({
154 | modalPresence: !prevState.modalPresence
155 | }));
156 | }
157 |
158 | render(){
159 | if (!this.props.body) {
160 | return ;
161 | }
162 |
163 | let bodyText = this.props.body;
164 | const { isDragging, connectDragSource, connectDropTarget } = this.props;
165 |
166 | const opacity = 1;
167 | return connectDragSource(connectDropTarget(
168 |
169 | {}
176 |
177 | )
178 | );
179 | }
180 | }
181 |
182 | function connectSource(connect, monitor){
183 | return{
184 | connectDragPreview: connect.dragPreview(),
185 | connectDragSource: connect.dragSource(),
186 | isDragging: monitor.isDragging(),
187 | };
188 | }
189 |
190 | function connectTarget(connect){
191 | return {
192 | connectDropTarget: connect.dropTarget(),
193 | };
194 | }
195 |
196 | const mapStateToProps = (state) => {
197 | return {
198 | boards: state.boards,
199 | lists: state.lists,
200 | cards: state.cards,
201 | shared_boards: state.shared_boards,
202 | };
203 | };
204 |
205 | const mapDispatchToProps = (dispatch) => {
206 | return {
207 | moveCard: (APIParams, cardParams) => {
208 | return dispatch(moveCard( APIParams, cardParams ));
209 | },
210 | dropZone: (dropZoneParams) => {
211 | return dispatch(generateDropZone( dropZoneParams ));
212 | },
213 | };
214 | };
215 |
216 |
217 | export default connectOriginal(mapStateToProps, mapDispatchToProps)(
218 | DragSource( ItemTypes.CARD, cardSource, connectSource)(
219 | DropTarget(ItemTypes.CARD, cardTarget, connectTarget)(Card))
220 | );
221 |
--------------------------------------------------------------------------------
/frontend/components/list.jsx:
--------------------------------------------------------------------------------
1 | import { connect as connectOriginal } from 'react-redux';
2 |
3 | import React from 'react';
4 | import { values, merge } from 'lodash';
5 | import { findDOMNode } from 'react-dom';
6 | import { DragSource, DragDropContext, DragDropContextProvider,
7 | DropTarget } from 'react-dnd';
8 | import HTML5Backend from 'react-dnd-html5-backend';
9 | import Card from './card';
10 | import Masonry from 'react-masonry-component';
11 | import { moveCard } from '../actions/card_actions';
12 | import ListEditModal from './list_edit_modal';
13 |
14 |
15 | var myState = {starting: {}, ending: {}};
16 |
17 | const cardSource = {
18 | beginDrag(props) {
19 |
20 | myState = merge(myState, {starting: props});
21 | return {
22 | card_id: props.id,
23 | cardIndex: props.cardIndex,
24 | listIndex: props.listId,
25 | };
26 | },
27 | };
28 |
29 | const listSource = {
30 | beginDrag(props) {
31 | return {
32 | list_id: props.listId,
33 | listTarget: props.listObj.ord
34 | };
35 | },
36 | };
37 |
38 | const style = {
39 | border: 'none',
40 | padding: '0px',
41 | marginBottom: '.5rem',
42 | backgroundColor: 'none',
43 | cursor: 'pointer',
44 | opacity: 1.0,
45 | };
46 |
47 | const ItemTypes = {
48 | CARD: 'card',
49 | LIST: 'list',
50 | };
51 |
52 | const listTarget = {
53 | drop(props, monitor, component) {
54 | if (monitor.didDrop()) {
55 | // If you want, you can check whether some nested
56 | // target already handled drop
57 | return;
58 | }
59 | // Obtain the dragged item
60 | const item = monitor.getItem();
61 | return { moved: true };
62 | },
63 |
64 | hover(props, monitor, component) {
65 | let listStartingIndex = monitor.getItem().listTarget;
66 | let listHoverIndex = props.listTarget;
67 |
68 | console.log("LIST hover PROPS");
69 | console.log(props);
70 |
71 |
72 | if (listStartingIndex === listHoverIndex) {
73 | return;
74 | }
75 |
76 | // Determine rectangle on screen
77 | const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
78 |
79 | // Get vertical middle
80 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
81 |
82 | // Determine mouse position
83 | const clientOffset = monitor.getClientOffset();
84 |
85 | // Get pixels to the top
86 | const hoverClientY = clientOffset.y - hoverBoundingRect.top;
87 |
88 | // Only perform the move when the mouse has crossed half of the items height
89 | // When dragging downwards, only move when the cursor is below 50%
90 | // When dragging upwards, only move when the cursor is above 50%
91 |
92 | // Dragging downwards
93 | if (listStartingIndex < listHoverIndex && hoverClientY < hoverMiddleY) {
94 | return;
95 | }
96 |
97 | // Dragging upwards
98 | if (listStartingIndex > listHoverIndex && hoverClientY > hoverMiddleY) {
99 | return;
100 | }
101 |
102 | },
103 | };
104 |
105 |
106 | class List extends React.Component{
107 | constructor(props){
108 | super(props);
109 | this.state = { cardBody: "", listId: props.listId,
110 | ord: values(props.listObj.cardIds).length };
111 | this.handleCreateCard = this.handleCreateCard.bind(this);
112 | this.handleCreateCardBodyChange = this.handleCreateCardBodyChange.bind(this);
113 | this.handleEnter = this.handleEnter.bind(this);
114 | }
115 |
116 | handleCreateCard(e) {
117 | e.preventDefault();
118 | this.props.createCard(this.state.listId, this.state.ord,
119 | this.state.cardBody);
120 | this.setState({
121 | cardBody: ""
122 | });
123 | }
124 |
125 | handleCreateCardBodyChange(e){
126 | e.preventDefault();
127 | this.setState({
128 | cardBody: e.currentTarget.value,
129 | });
130 | }
131 |
132 | handleEnter(e){
133 | if (e.key === "Enter" && !e.shiftKey){
134 | e.preventDefault();
135 | this.props.createCard(this.state.listId, this.state.ord,
136 | this.state.cardBody);
137 | }
138 | }
139 |
140 | render(){
141 | if (!this.props.listObj) {
142 | return ;
143 | }
144 |
145 | const opacity = 1;
146 | const listTitle = this.props.listObj.title;
147 | let listEditModal = ;
152 | const { isDragging, connectDragSource,
153 | connectDropTarget, hovering } = this.props;
154 | const allCards = this.props.cards;
155 | const cardsBodyArray = this.props.listObj.cardIds.map( (cardId, idx) => {
156 | let currentCard = allCards[cardId];
157 | return (
158 |
159 |
166 |
167 | );
168 | }
169 | );
170 |
171 | var bodyLength = 70;
172 |
173 | let listElement = (
174 |
175 | { listEditModal }
176 |
177 | { cardsBodyArray }
178 |
179 |
192 | );
193 |
194 | return (
195 | connectDragSource(connectDropTarget(
196 |
197 | { listElement }
198 |
199 | ))
200 | );
201 | }
202 | }
203 |
204 | function connectSource(connect, monitor){
205 | return{
206 | connectDragPreview: connect.dragPreview(),
207 | connectDragSource: connect.dragSource(),
208 | isDragging: monitor.isDragging(),
209 | };
210 | }
211 |
212 | function connectTarget(connect){
213 | return {
214 | connectDropTarget: connect.dropTarget(),
215 | };
216 | }
217 |
218 | const mapDispatchToProps = (dispatch) => {
219 | return {};
220 | };
221 |
222 | export default connectOriginal(null, mapDispatchToProps)(
223 | DragSource(ItemTypes.CARD, cardSource, connectSource)(
224 | DropTarget(ItemTypes.LIST, listTarget, connectTarget)(List))
225 | );
226 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
17 | @import "font-awesome-sprockets";
18 | @import "font-awesome";
19 |
20 | $offwhite: #EDEFF0;
21 | $light-grey: #DCDCDC;
22 | $babby-blue: #C7E2F1;
23 | $header-blue: #026AA7;
24 | $dark-grey: #505050;
25 | $menu-hover-blue:#298FCA;
26 |
27 | html, head, body, header,
28 | nav, h1, a, ul, li, main,
29 | section, p, footer, small {
30 | margin: 0;
31 | padding: 0;
32 | border: 0;
33 | outline: 0;
34 | font: inherit;
35 | color: inherit;
36 | text-align: inherit;
37 | text-decoration: inherit;
38 | vertical-align: inherit;
39 | box-sizing: inherit;
40 | background: transparent;
41 | font-family: "Helvetica Neue";
42 |
43 | }
44 |
45 | header, nav, main, section,
46 | footer, small {
47 | display: block;
48 | }
49 |
50 | ul {
51 | list-style: none;
52 | }
53 |
54 | /* Clearfix */
55 |
56 | .group:after {
57 | content: "";
58 | display: block;
59 | clear: both;
60 | }
61 |
62 | body{
63 | margin: 0px;
64 |
65 | }
66 | *, *:before, *:after {
67 | box-sizing: inherit;
68 | }
69 |
70 | html {
71 | box-sizing: border-box;
72 | }
73 | button:focus {outline:0;}
74 |
75 | .production-readme-response-time {
76 | width: 200px;
77 | align-self: left;
78 | padding: 15px;
79 | }
80 |
81 | // .landing-icon{
82 | // max-width: 5%;
83 | // height:auto;
84 | // padding: 12px;
85 | // position: absolute;
86 | // left: 2vw;
87 | // -moz-transform: scaleX(-1);
88 | // -o-transform: scaleX(-1);
89 | // -webkit-transform: scaleX(-1);
90 | // transform: scaleX(-1);
91 | // filter: FlipH;
92 | // -ms-filter: "FlipH";
93 | // }
94 |
95 | .trello-logo-wrapper{
96 | display: flex;
97 | flex-direction: row;
98 |
99 |
100 | }
101 |
102 | .trello-landing-logo{
103 | color: white;
104 | width: 33px;
105 | height: 33px;
106 |
107 | }
108 |
109 | .trello-header-title-landing{
110 | font-family: "Brush Script MT", cursive;
111 | font-size: 55px;
112 | font-style: normal;
113 | font-variant: normal;
114 | font-weight: 600;
115 | line-height: 32px;
116 | color: white;
117 | position: relative;
118 | left: 1vw;
119 | bottom: 0vh;
120 | }
121 |
122 | .error-box{
123 | margin-top: 20px;
124 | background-color: #B04632;
125 | box-shadow: 2px 2px 2px #0C3953;
126 | padding: 10px;
127 | font-weight: bold;
128 | font-size: 12px;
129 | height: 100%;
130 | border-radius: 2px;
131 | color: #EDEFF0;
132 | width: 150px;
133 | border: none;
134 | align-self: center;
135 | word-wrap: normal;
136 | text-align: center;
137 | }
138 |
139 | ul{
140 | justify-content: center;
141 | display: flex;
142 | }
143 | .landing-header{
144 | font-family: "Helvetica Neue";
145 | display: flex;
146 | flex-direction: row;
147 | justify-content: space-between;
148 | width: 100%;
149 | background: #0079BF;
150 | padding: 25px;
151 | align-items: center;
152 | height: 76px;
153 | }
154 |
155 | .landing-header-left{
156 | position: absolute;
157 | top: 19px;
158 | right: 40px;
159 |
160 | display: block;
161 | height: 100%;
162 | width: 215px;
163 | display: flex;
164 | flex-direction: row;
165 | align-content: center;
166 | justify-content: space-between;
167 | }
168 |
169 |
170 | .landing-sign-up-bar, .landing-login-bar{
171 | border-radius: 5px;
172 | font-size: 19px;
173 | color: #EDEFF0;
174 | text-decoration: none;
175 | text-align: center
176 | }
177 |
178 |
179 | .landing-login-bar{
180 | height: 40px;
181 | width: 88px;
182 | background: #026AA7;
183 | box-shadow: 2px 2px 2px #0C3953;
184 | padding: 9px;
185 | }
186 |
187 | .landing-login-bar:hover{
188 | background: #055A8C;
189 | }
190 |
191 | .landing-sign-up-bar:hover, button.landing-body-button:hover{
192 | background: #519839;
193 | }
194 |
195 |
196 |
197 | .landing-sign-up-bar{
198 | background: #61BD4F;
199 | box-shadow: 2px 2px 2px #0C3953;
200 | padding: 9px;
201 | font-weight: bold;
202 | font-size: 18px;
203 | height: 40px;
204 | width: 102px;
205 | }
206 |
207 |
208 | .landing-body-text{
209 | border: 1px solid black;
210 | }
211 |
212 | .landing-body-container{
213 | font-family: "Helvetica Neue";
214 | background: #026AA7;
215 | color: #EDEFF0;
216 | height: 100vh;
217 | display: flex;
218 | flex-direction: column;
219 | align-items: center;
220 | align-self: center;
221 | }
222 |
223 | .landing-body-title{
224 | align-self: center;
225 | font-size: 42px;
226 | font-weight: bold;
227 | padding-top: 65px;
228 | padding-bottom: 30px;
229 | }
230 |
231 | .landing-body-header{
232 | align-self: center;
233 | font-family: "Helvetica Neue";
234 | font-size: 42px;
235 | font-weight: bold;
236 | padding: 20px;
237 | }
238 |
239 | .landing-body-form{
240 | align-self: center;
241 | }
242 |
243 | .landing-body-form-box-username{
244 | border: none;
245 |
246 | border-radius: 5px;
247 | height: 35px;
248 | width: 400px;
249 | font-size: 20px;
250 | color: gray;
251 | box-shadow: 1px 1px 2px #A0A0A0;
252 | }
253 |
254 | input.landing-body-form-box-username, input.landing-body-form-box-password{
255 | padding: 10px;
256 | background: #EDEFF0;
257 |
258 | }
259 |
260 | input.landing-body-form-box-password{
261 | -webkit-text-security: disc;
262 | }
263 |
264 | button.landing-body-button{
265 | background-color: #61BD4F;
266 | box-shadow: 2px 2px 2px #0C3953;
267 | padding: 7px;
268 | font-weight: bold;
269 | font-size: 18px;
270 | height: 40px;
271 | border-radius: 10px;
272 | color: #EDEFF0;
273 | width: 240px;
274 | border: none;
275 | align-self: center
276 | }
277 |
278 | .landing-body-button-spacer{
279 | height: 30px;
280 |
281 | }
282 |
283 | div.landing-body-button{
284 | align-items: center;
285 | position: relative;
286 | left: 88px;
287 | }
288 |
289 | div.landing-body-form-password{
290 | padding-bottom: 27px;
291 |
292 | }
293 |
294 | .landing-body-form-box-password{
295 | border: none;
296 | border-radius: 5px;
297 | height: 35px;
298 | width: 400px;
299 | font-size: 20px;
300 | box-shadow: 1px 1px 2px #A0A0A0;
301 | color: gray;
302 | }
303 |
304 | .landing-body-alignment-container{
305 | align-items: center;
306 | height: 100vh;
307 | width: 100vh;
308 | align-content: center;
309 | flex-direction: column;
310 | align-items: center;
311 | display: flex;
312 | }
313 |
314 |
315 | .landing-body-form-username, .landing-body-form-password{
316 | padding: 10px;
317 | font-weight: lighter;
318 | }
319 |
320 | .landing-body-form-username-text, .landing-body-form-password-text{
321 | font-size: 20px;
322 | font-weight: bold;
323 | padding: 1px;
324 | };
325 |
326 | .landing-body-form{
327 | align-self: center;
328 | }
329 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/header.scss:
--------------------------------------------------------------------------------
1 | $offwhite: #EDEFF0;
2 | $light-grey: #DCDCDC;
3 | $babby-blue: #C7E2F1;
4 | $header-blue: #026AA7;
5 | button:focus {outline:0;};
6 | $dark-grey: #505050;
7 | $menu-hover-blue:#298FCA;
8 |
9 | .header-container{
10 | display: flex;
11 | justify-content: center;
12 | flex-direction: column;
13 | background-color: white;
14 | }
15 |
16 | .header-nav-bar{
17 | display: flex;
18 | flex-direction: row;
19 | justify-content: space-between;
20 | padding: 4px;
21 | padding-top: 6px;
22 | height: 40px;
23 | background-color: $header-blue;
24 | }
25 |
26 | .trello-image-link {
27 | height: 30px;
28 | position: absolute;
29 | left: 45vw;
30 |
31 | -moz-transform: scaleX(-1);
32 | -o-transform: scaleX(-1);
33 | -webkit-transform: scaleX(-1);
34 | transform: scaleX(-1);
35 | filter: FlipH;
36 | -ms-filter: "FlipH";
37 |
38 | }
39 |
40 | .trello-header-title{
41 | font-family: "Brush Script MT", cursive;
42 | font-size: 36px;
43 | font-style: normal;
44 | font-variant: normal;
45 | font-weight: bolder;
46 | line-height: 30px;
47 | color: white;
48 | position: absolute;
49 | left: 48vw;
50 | }
51 |
52 | #trello-header-icon{
53 | font-size: 31px;
54 |
55 | }
56 |
57 |
58 | .create-board-dropdown-form{
59 | display: flex;
60 | flex-direction: column;
61 | }
62 |
63 |
64 | .board-menu-container{
65 | width: 200px;
66 | background-color: #FFFFFF;
67 | font-family: "Helvetica Neue";
68 | border-radius: 3px;
69 | box-shadow: 1px 1px 1px $light-grey;
70 | padding-top: 15px;
71 | padding-bottom: 4px;
72 | padding-right: 4px;
73 | padding-left: 3px;
74 | position: absolute;
75 | top: 40px;
76 | left: 3px;
77 | border: 1px solid $light-grey;
78 |
79 | }
80 |
81 | .board-menu-container{
82 |
83 | top: -100px;
84 | left: -300px;
85 |
86 |
87 | }
88 |
89 | .expanded-board-menu-container{
90 | z-index: 3;
91 | width: 235px;
92 | background-color: #FFFFFF;
93 | font-family: "Helvetica Neue";
94 | border-radius: 3px;
95 | box-shadow: 1px 1px 1px gray;
96 | padding-top: 15px;
97 | padding-bottom: 4px;
98 | padding-right: 4px;
99 | padding-left: 5px;
100 | position: absolute;
101 | top: 40px;
102 | left: 3px;
103 | border: 1px solid $light-grey;
104 | overflow: hidden;
105 | height: 90%;
106 |
107 | word-wrap: break-word;
108 | overflow-wrap: break-word;
109 | }
110 |
111 | .board-menu-item-left-box{
112 | height: 34px;
113 | width: 34px;
114 | background-color: #5BA4CF;
115 | border-radius: 3px;
116 | border-top-right-radius: 0px;
117 | border-bottom-right-radius: 0px;
118 | padding-left: 5px;
119 | padding-right: 0px;
120 | }
121 |
122 | .board-item-wrapper{
123 | display: flex;
124 | flex-direction: row;
125 | width: auto;
126 | padding: 0px;
127 |
128 | }
129 |
130 | .board-menu-item{
131 | width: 180px;
132 | background-color: #E4F0F6;
133 | margin-left: 0px;
134 | margin-bottom: 5px;
135 | padding-left: 10px;
136 | padding-top: 9px;
137 | border-radius: 3px;
138 | height: 34px;
139 | align-content: center;
140 | text-align: justify;
141 | font-weight: bolder;
142 | font-size: 13px;
143 | color: $dark-grey;
144 | border-top-left-radius: 0px;
145 | border-bottom-left-radius: 0px;
146 | }
147 |
148 | .board-menu-item:hover{
149 | background-color: $babby-blue;
150 | }
151 |
152 | .board-menu-text{
153 | height: 200px;
154 | width: 100px;
155 |
156 | }
157 |
158 | .board-menu-button{
159 | color: white;
160 | display: flex;
161 | flex-direction: row;
162 | justify-content: space-between;
163 | height: 28px;
164 | width: 88px;
165 | border-radius: 3px;
166 | font-family: "Helvetica Neue";
167 | background-color: #298FCA;
168 | cursor: pointer;
169 | border: none;
170 | }
171 |
172 | .board-menu-button:hover{
173 | background-color: #5BA4CF;
174 | }
175 |
176 | button.board-sharing-dropdown-container{
177 | height: 28px;
178 | width: 28px;
179 | font-size: 11px;
180 | font-weight: bold;
181 | background: $menu-hover-blue;
182 | font-family: "Helvetica Neue";
183 | border-radius: 3px;
184 | padding-left: 7px;
185 | padding-top: 4px;
186 | color: white;
187 | border: none;
188 | position: absolute;
189 | right: 103px;
190 |
191 | }
192 |
193 | .board-menu-button-icon{
194 | padding-top: 1px;
195 | padding-left: 3px;
196 | font-size: 16px;
197 | }
198 |
199 |
200 | .board-menu-button-text{
201 | padding-top: 2px;
202 | padding-right: 4px;
203 | font-weight: 700;
204 | font-size: 14px;
205 | }
206 |
207 |
208 | .new-board-dropdown-container{
209 | height: 50px;
210 | width: 100px;
211 | }
212 |
213 |
214 | .expanded-user-menu-container{
215 | display: flex;
216 | flex-direction: row;
217 | position: relative;
218 | top: 39px;
219 | right: 175px;
220 | // right: 500em;
221 | width: 200px;
222 | background-color: #FFFFFF;
223 | font-family: "Helvetica Neue";
224 | border-radius: 3px;
225 | box-shadow: 1px 1px 1px grey;
226 | padding-top: 15px;
227 | padding-bottom: 4px;
228 | border: 1px solid $light-grey;
229 | align-content: center;
230 | align-items: center;
231 | padding: 15px;
232 | z-index: 3;
233 | }
234 |
235 | .user-menu-container.expanded-board-menu-container{
236 | padding-left: 14px;
237 | padding-bottom: 15px;
238 | }
239 | div.user-menu-container.expanded-board-menu-container{
240 | z-index: 3;
241 | width: 200px;
242 | }
243 |
244 | div.outerEnclosingDiv{
245 | width: 30px;
246 | }
247 |
248 | .user-profile-photo{
249 | height: 28px;
250 | width: 28px;
251 | font-size: 11px;
252 | font-weight: bold;
253 | background: $light-grey;
254 | font-family: "Helvetica Neue";
255 | border-radius: 3px;
256 | padding-left: 8px;
257 | padding-top: 2px;
258 | color: #4d4d4d;
259 | position: absolute;
260 | right: 15px;
261 | border: none;
262 | }
263 |
264 | .user-profile-logout-button{
265 | width: 160px;
266 | padding: 10px;
267 |
268 | color: $dark-grey;
269 | border: none;
270 | background-color: white;
271 | padding-left: 20px;
272 | align-self: center;
273 | text-align: left;
274 | font-weight: bolder;
275 | }
276 |
277 | .user-profile-photo:hover{
278 | background-color: #B6BBBF;
279 | }
280 |
281 | .user-menu-container-spacer{
282 | border-top: 1px solid $dark-grey;
283 | border-bottom: 1px solid $dark-grey;
284 | padding: 5px;
285 | background-color: white
286 |
287 | }
288 |
289 | .user-profile-logout-button:hover{
290 | background-color: $menu-hover-blue;
291 | color: white;
292 | }
293 |
294 | .create-board-dropdown-container{
295 | height: 50px;
296 | width: 40px;
297 | }
298 |
299 | .create-board-dropdown-button{
300 | height: 28px;
301 | width: 28px;
302 | font-size: 11px;
303 | font-weight: bold;
304 | background: $menu-hover-blue;
305 | font-family: "Helvetica Neue";
306 | border-radius: 3px;
307 | padding-left: 7px;
308 | padding-top: 4px;
309 | color: white;
310 | border: none;
311 | position: relative;
312 | right: -70px;
313 | }
314 |
315 | .create-board-dropdown-button:hover{
316 | background-color: #5BA4CF
317 | }
318 |
319 | .create-board-dropdown-menu-container {
320 | z-index: 3;
321 | display: flex;
322 | flex-direction: column;
323 | position: absolute;
324 | top: 45px;
325 | right: 14px;
326 | width: 144px;
327 | background-color: #FFFFFF;
328 | font-family: "Helvetica Neue";
329 | border-radius: 3px;
330 | box-shadow: 1px 1px 1px #DCDCDC;
331 | padding-top: 3px;
332 | padding-bottom: 4px;
333 | border: 1px solid #DCDCDC;
334 | width: 271px;
335 | height: 151px;
336 | padding-right: 10px;
337 | padding-left: 10px;
338 | }
339 |
340 | section.share-board-dropdown-menu-container {
341 | z-index: 3;
342 | display: flex;
343 | flex-direction: column;
344 | position: absolute;
345 | top: 45px;
346 | right: 14px;
347 | width: 144px;
348 | background-color: #FFFFFF;
349 | font-family: "Helvetica Neue";
350 | border-radius: 3px;
351 | box-shadow: 1px 1px 1px #DCDCDC;
352 | padding-top: 3px;
353 | padding-bottom: 4px;
354 | border: 1px solid #DCDCDC;
355 | width: 271px;
356 | height: auto;
357 | padding-right: 10px;
358 | padding-left: 10px;
359 | padding-bottom: 10px;
360 | }
361 |
362 | .create-board-dropdown-menu-header{
363 | color: gray;
364 | font-family: "Helvetica Neue";
365 | border-bottom: 1px solid gray;
366 | padding: 5px;
367 | text-align: center;
368 | font-size: 14px;
369 |
370 | }
371 |
372 | .create-board-dropdown-menu-title{
373 | color: $dark-grey;
374 | font-size: 12px;
375 | font-weight: bolder;
376 | padding-bottom: 5px;
377 | padding-top: 5px;
378 | }
379 |
380 | input.create-board-dropdown-menu-input {
381 | border-radius: 4px;
382 | border: 1px solid #298FCA;
383 | height: 26px;
384 | color: gray;
385 | padding-left: 3px;
386 | font-size: 13px;
387 | }
388 |
389 | button.create-board-dropdown-menu-button {
390 | background-color: #61BD4F;
391 | box-shadow: 1px 1px 1px #0C3953;
392 | padding: 7px;
393 | font-weight: bold;
394 | font-size: 13px;
395 | height: 32px;
396 | border-radius: 10px;
397 | color: #EDEFF0;
398 | width: 87px;
399 | border: none;
400 | align-self: left;
401 | margin-top: 10px;
402 | }
403 |
404 | button.create-board-dropdown-menu-button:hover{
405 | background-color: #519839;
406 | }
407 |
408 | button.create-board-dropdown-button {
409 | position: absolute;
410 | right: 59px;
411 | }
412 |
--------------------------------------------------------------------------------