├── .gitignore
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── assets
│ ├── config
│ │ └── manifest.js
│ ├── images
│ │ ├── .keep
│ │ ├── icons
│ │ │ ├── Pixel-Tracker-Logo-light.png
│ │ │ ├── Pixel-Tracker-Logo-main.png
│ │ │ ├── check-icon-white.png
│ │ │ ├── icebox-icon-white.png
│ │ │ ├── list-icon-white.png
│ │ │ ├── lock-icon.png
│ │ │ ├── menu-white.png
│ │ │ ├── people-icon.png
│ │ │ ├── person-icon-white.png
│ │ │ ├── plus-icon-white.png
│ │ │ └── xmark-icon-white.png
│ │ └── splash-background-compressed.jpg
│ ├── javascripts
│ │ ├── api
│ │ │ ├── comments.coffee
│ │ │ ├── pixels.coffee
│ │ │ ├── sessions.coffee
│ │ │ ├── tasks.coffee
│ │ │ └── users.coffee
│ │ ├── application.js
│ │ ├── cable.js
│ │ └── channels
│ │ │ └── .keep
│ └── stylesheets
│ │ ├── api
│ │ ├── comments.scss
│ │ ├── pixels.scss
│ │ └── tasks.scss
│ │ ├── application.css
│ │ ├── auth.css
│ │ ├── dashboard_header.css
│ │ ├── main_header.css
│ │ ├── pixels.scss
│ │ ├── project_detail.scss
│ │ ├── project_form.css
│ │ ├── projects_list.scss
│ │ ├── react-joyride.scss
│ │ ├── reset.css
│ │ ├── spinner.scss
│ │ ├── splash_page.css
│ │ └── tasks.scss
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── controllers
│ ├── api
│ │ ├── comments_controller.rb
│ │ ├── pixels_controller.rb
│ │ ├── project_members_controller.rb
│ │ ├── projects_controller.rb
│ │ ├── sessions_controller.rb
│ │ ├── tasks_controller.rb
│ │ └── users_controller.rb
│ ├── application_controller.rb
│ ├── concerns
│ │ └── .keep
│ └── static_pages_controller.rb
├── helpers
│ ├── api
│ │ ├── comments_helper.rb
│ │ ├── pixels_helper.rb
│ │ ├── projects_helper.rb
│ │ ├── sessions_helper.rb
│ │ ├── tasks_helper.rb
│ │ └── users_helper.rb
│ └── application_helper.rb
├── jobs
│ └── application_job.rb
├── mailers
│ └── application_mailer.rb
├── models
│ ├── application_record.rb
│ ├── comment.rb
│ ├── concerns
│ │ └── .keep
│ ├── pixel.rb
│ ├── project.rb
│ ├── project_member.rb
│ ├── task.rb
│ └── user.rb
└── views
│ ├── api
│ ├── pixels
│ │ ├── _pixel.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── projects
│ │ ├── _projects.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ └── users
│ │ ├── _user.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── layouts
│ ├── application.html.erb
│ ├── mailer.html.erb
│ └── mailer.text.erb
│ └── static_pages
│ └── root.html.erb
├── bin
├── bundle
├── rails
├── rake
├── setup
├── spring
└── update
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── application_controller_renderer.rb
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── cookies_serializer.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── new_framework_defaults.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
├── puma.rb
├── routes.rb
├── secrets.yml
└── spring.rb
├── db
├── migrate
│ ├── 20161206151623_create_users.rb
│ ├── 20161206160412_add_email_to_users.rb
│ ├── 20161207171903_create_projects.rb
│ ├── 20161207172015_create_project_members.rb
│ ├── 20161207191211_add_unique_index_to_project_members.rb
│ ├── 20161209213350_create_pixels.rb
│ ├── 20161209220715_change_ord_name_in_pixel_table.rb
│ ├── 20161211152624_add_icebox_boolean_to_pixels.rb
│ ├── 20161212220648_create_comments.rb
│ └── 20161213040831_create_tasks.rb
├── schema.rb
└── seeds.rb
├── docs
├── README.md
├── api-endpoints.md
├── component-hierarchy.md
├── sample-state.md
├── schema.md
├── screenshots
│ ├── screenshot-create-project.png
│ ├── screenshot-dashboard.png
│ ├── screenshot-dnd.png
│ ├── screenshot-form.png
│ └── screenshot-sidebar.png
└── wireframes
│ ├── add-project-member.png
│ ├── create-project-popup.png
│ ├── new-story-page.png
│ ├── project-page.png
│ ├── projects-homepage.png
│ ├── sign-in-page.png
│ └── sign-up-page.png
├── frontend
├── actions
│ ├── error_actions.js
│ ├── header_actions.js
│ ├── pixel_actions.js
│ ├── project_actions.js
│ ├── session_actions.js
│ └── sidebar_actions.js
├── components
│ ├── dashboard.jsx
│ ├── headers
│ │ ├── dashboard_header.jsx
│ │ ├── dashboard_header_container.js
│ │ ├── errors_header.jsx
│ │ ├── main_header.jsx
│ │ ├── main_header_container.js
│ │ └── tour.jsx
│ ├── main.jsx
│ ├── pixels
│ │ ├── comments.jsx
│ │ ├── comments_container.js
│ │ ├── comments_list.jsx
│ │ ├── confirm_modal.jsx
│ │ ├── drop_area.jsx
│ │ ├── pixel_form.jsx
│ │ ├── pixel_form_container.js
│ │ ├── pixel_form_helper.jsx
│ │ ├── pixel_list.jsx
│ │ ├── pixel_list_container.js
│ │ ├── pixel_list_drop_area.jsx
│ │ ├── pixel_list_drop_area_container.js
│ │ ├── pixel_list_item.jsx
│ │ ├── pixel_list_item_container.js
│ │ ├── pixel_panel.jsx
│ │ ├── pixel_panel_container.js
│ │ ├── pixel_summary.jsx
│ │ ├── pixel_summary_helper.jsx
│ │ ├── task_form.jsx
│ │ ├── task_list_checkbox.jsx
│ │ ├── task_list_form.jsx
│ │ ├── task_list_item.jsx
│ │ ├── task_list_item_container.js
│ │ ├── tasks.jsx
│ │ └── tasks_container.js
│ ├── project_detail
│ │ ├── filter_list_item.jsx
│ │ ├── private_page.jsx
│ │ ├── project_detail.jsx
│ │ ├── project_detail_container.js
│ │ ├── sidebar.jsx
│ │ └── sidebar_container.js
│ ├── projects
│ │ ├── member_list.jsx
│ │ ├── members.jsx
│ │ ├── members_container.jsx
│ │ ├── project_form.jsx
│ │ ├── project_form_container.js
│ │ ├── project_list.jsx
│ │ ├── project_list_item.jsx
│ │ ├── projects.jsx
│ │ └── projects_container.js
│ ├── root.jsx
│ ├── sessions
│ │ ├── session_form.jsx
│ │ └── session_form_container.js
│ ├── spinners
│ │ └── spinners.jsx
│ └── splash_page
│ │ ├── splash_page.jsx
│ │ └── splash_page_container.js
├── modules
│ └── dnd_item_types.js
├── pixel_tracker.jsx
├── reducers
│ ├── error_reducer.js
│ ├── header_reducer.js
│ ├── loading_reducer.js
│ ├── pixel_reducer.js
│ ├── project_reducer.js
│ ├── root_reducer.js
│ ├── selector.js
│ ├── session_reducer.js
│ └── sidebar_reducer.js
├── store
│ └── store.js
└── util
│ ├── comments_api_util.js
│ ├── pixel_state_util.js
│ ├── pixels_api_util.js
│ ├── projects_api_util.js
│ ├── session_api_util.js
│ └── tasks_api_util.js
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── log
└── .keep
├── package.json
├── public
├── 404.html
├── 422.html
├── 500.html
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── favicon.ico
└── robots.txt
├── test
├── controllers
│ ├── .keep
│ ├── api
│ │ ├── comments_controller_test.rb
│ │ ├── pixels_controller_test.rb
│ │ ├── project_members_controller_test.rb
│ │ ├── projects_controller_test.rb
│ │ ├── sessions_controller_test.rb
│ │ ├── tasks_controller_test.rb
│ │ └── users_controller_test.rb
│ ├── comments_controller_test.rb
│ └── static_pages_controller_test.rb
├── fixtures
│ ├── .keep
│ ├── comments.yml
│ ├── files
│ │ └── .keep
│ ├── pixels.yml
│ ├── project_members.yml
│ ├── projects.yml
│ ├── tasks.yml
│ └── users.yml
├── helpers
│ └── .keep
├── integration
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── comment_test.rb
│ ├── pixel_test.rb
│ ├── project_member_test.rb
│ ├── project_test.rb
│ ├── task_test.rb
│ └── user_test.rb
└── test_helper.rb
├── tmp
└── .keep
├── vendor
└── assets
│ ├── javascripts
│ └── .keep
│ └── stylesheets
│ └── .keep
└── webpack.config.js
/.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 | bundle.js
21 | bundle.js.map
22 | .DS_Store
23 | npm-debug.log
24 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | ruby '2.3.1'
4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
5 | gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
6 | # Use postgresql as the database for Active Record
7 | gem 'pg', '~> 0.18'
8 | # Use Puma as the app server
9 | gem 'puma', '~> 3.0'
10 | # Use SCSS for stylesheets
11 | gem 'sass-rails', '~> 5.0'
12 | # Use Uglifier as compressor for JavaScript assets
13 | gem 'uglifier', '>= 1.3.0'
14 | # Use CoffeeScript for .coffee assets and views
15 | gem 'coffee-rails', '~> 4.2'
16 | # See https://github.com/rails/execjs#readme for more supported runtimes
17 | # gem 'therubyracer', platforms: :ruby
18 |
19 | # Use jquery as the JavaScript library
20 | gem 'jquery-rails'
21 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
22 | gem 'jbuilder', '~> 2.5'
23 | # Use Redis adapter to run Action Cable in production
24 | # gem 'redis', '~> 3.0'
25 | # Use ActiveModel has_secure_password
26 | gem 'bcrypt', '~> 3.1.7'
27 |
28 |
29 | gem 'pry-rails'
30 | # Use Capistrano for deployment
31 | # gem 'capistrano-rails', group: :development
32 |
33 | group :development, :test do
34 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
35 | gem 'byebug', platform: :mri
36 | end
37 |
38 | group :development do
39 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
40 | gem 'web-console'
41 | gem 'listen', '~> 3.0.5'
42 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
43 | gem 'annotate'
44 | gem 'spring'
45 | gem 'spring-watcher-listen', '~> 2.0.0'
46 | end
47 |
48 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
49 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
50 |
--------------------------------------------------------------------------------
/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/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../javascripts .js
3 | //= link_directory ../stylesheets .css
4 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/icons/Pixel-Tracker-Logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/Pixel-Tracker-Logo-light.png
--------------------------------------------------------------------------------
/app/assets/images/icons/Pixel-Tracker-Logo-main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/Pixel-Tracker-Logo-main.png
--------------------------------------------------------------------------------
/app/assets/images/icons/check-icon-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/check-icon-white.png
--------------------------------------------------------------------------------
/app/assets/images/icons/icebox-icon-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/icebox-icon-white.png
--------------------------------------------------------------------------------
/app/assets/images/icons/list-icon-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/list-icon-white.png
--------------------------------------------------------------------------------
/app/assets/images/icons/lock-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/lock-icon.png
--------------------------------------------------------------------------------
/app/assets/images/icons/menu-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/menu-white.png
--------------------------------------------------------------------------------
/app/assets/images/icons/people-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/people-icon.png
--------------------------------------------------------------------------------
/app/assets/images/icons/person-icon-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/person-icon-white.png
--------------------------------------------------------------------------------
/app/assets/images/icons/plus-icon-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/plus-icon-white.png
--------------------------------------------------------------------------------
/app/assets/images/icons/xmark-icon-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/icons/xmark-icon-white.png
--------------------------------------------------------------------------------
/app/assets/images/splash-background-compressed.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/images/splash-background-compressed.jpg
--------------------------------------------------------------------------------
/app/assets/javascripts/api/comments.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/assets/javascripts/api/pixels.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/assets/javascripts/api/sessions.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/assets/javascripts/api/tasks.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/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/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/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 |
--------------------------------------------------------------------------------
/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/assets/javascripts/channels/.keep
--------------------------------------------------------------------------------
/app/assets/stylesheets/api/comments.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the api/comments controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/api/pixels.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the api/pixels controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/api/tasks.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the api/tasks controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
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 reset
14 | *= require_tree .
15 | *= require pixels
16 | *= require_self
17 | */
18 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/auth.css:
--------------------------------------------------------------------------------
1 | .main-content{
2 | position: relative;
3 | }
4 |
5 | section.session-form {
6 | display: block;
7 | position: absolute;;
8 | top: 10%;
9 | left: 50%;
10 | transform: translateX(-50%);
11 | border-radius: 4px;
12 | background: #F3F4F4;
13 | border: 1px solid #ddd;
14 | width: 40%;
15 | max-width: 430px;
16 | min-width: 200px;
17 | }
18 |
19 | .session-form > h2 {
20 | text-align: center;
21 | font-weight: 700;
22 | margin: 20px 20px 0 20px;
23 | font-size: 16px;
24 | color: #444
25 | }
26 |
27 | .login-box {
28 | font-size: 14px;
29 | color: #888;
30 | padding: 20px;
31 | }
32 |
33 | .login-box label, input {
34 | display: block;
35 | }
36 |
37 | .login-box label {
38 | margin-left: 5%;
39 | margin-bottom: 15px;
40 | }
41 |
42 | .errored-input {
43 | color: #B61437;
44 | font-weight: 600;
45 | }
46 |
47 | .login-box input {
48 | margin-top: 2px;
49 | color: black;
50 | border-radius: 3px;
51 | border: 1px solid black;
52 | padding: 8px 7px;
53 | background: white;
54 | width: 90%;
55 | }
56 |
57 | input.submit-button {
58 | margin: 20px auto;
59 | margin-top: 20px;
60 | width: 50%;
61 | text-align: center;
62 | background: #5483AE;
63 | color: white;
64 | text-transform: uppercase;
65 | border: 0;
66 | }
67 |
68 | input.submit-button:hover {
69 | background: #41709a;
70 | cursor: pointer;
71 | }
72 |
73 | button.guest-button {
74 | display: block;
75 | margin: 0 auto;
76 | font-size: 16px;
77 | color: #4d85b6;
78 | }
79 |
80 | ul.error-display {
81 | margin: 0 auto;
82 | width: 80%;
83 | color: #c7284a;
84 | font-size: 14px;
85 | text-align: center;
86 | margin-bottom: 10px;
87 | }
88 |
89 | .error-display li {
90 | font-weight: 400;
91 | }
92 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/main_header.css:
--------------------------------------------------------------------------------
1 | header.main-header {
2 | background: #fff;
3 | }
4 |
5 | .header-container {
6 | margin: 0 auto;
7 | padding: 10px 20px;
8 | max-width: 860px;
9 | }
10 |
11 | .header-logo {
12 | float: left;
13 | padding: 10px 0;
14 | font-size: 22px;
15 | color: #517C9F;
16 | font-family: 'Merriweather', serif;
17 | font-weight: 700;
18 | }
19 |
20 | .header-logo > img{
21 | width: auto;
22 | height: 24px;
23 | display: inline-block;
24 | position: relative;
25 | top: 3px;
26 | margin-right: 3px;
27 | }
28 |
29 | #header-logo-inside {
30 | color: #DE752C;
31 | font-family: 'Merriweather', serif;
32 | font-weight: 700;
33 | font-size: 22px;
34 | padding: 0;
35 | margin: 0;
36 | }
37 |
38 | .right-nav-list {
39 | float: right;
40 | width: 50%;
41 | min-width: 94px;
42 | }
43 |
44 | .right-nav-list li {
45 | float: right;
46 | padding: 10px 0;
47 | margin: 0 4%;
48 | }
49 |
50 | .right-nav-list a, span, button {
51 | display: inline-block;
52 | color: #426FA0;
53 | text-shadow: 0 0 1px #94D2ED;
54 | font-size: 14px;
55 | border-radius: 3px;
56 | padding: 4px 8px;
57 | font-weight: 300;
58 | }
59 |
60 | .right-nav-list a:hover, button:hover {
61 | cursor: pointer;
62 | color: #1a3d61;
63 | }
64 |
65 | .right-nav-list span {
66 | color: #73b2c8;
67 | }
68 |
69 | .sign-up-link a{
70 | color: #fff;
71 | background: #F39E40;
72 | text-shadow: 0 0 0;
73 | }
74 |
75 | .sign-up-link a:hover{
76 | color: #fff;
77 | background: #EE7D00;
78 | }
79 |
80 | .session-header-message {
81 | margin: 5px 0 0 0;
82 | text-transform: uppercase;
83 | font-size: 12px;
84 | font-weight: 600;
85 | letter-spacing: 1px;
86 | position: relative;
87 | }
88 |
89 | .session-header-message a{
90 | text-transform: uppercase;
91 | font-size: 12px;
92 | font-weight: 600;
93 | letter-spacing: 1px;
94 | }
95 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/project_detail.scss:
--------------------------------------------------------------------------------
1 | .project-detail-container {
2 | font-size: 12px;
3 | background: #484F57;
4 | height: 100vh;
5 | overflow: hidden;
6 | }
7 |
8 | .main-content-container {
9 | height: 100%;
10 | }
11 |
12 | .side-bar {
13 | float:left;
14 | background: #2F3337;
15 | width: 200px;
16 | height: 100%;
17 | border: 1px solid black;
18 | overflow: hidden;
19 | transition: width .4s;
20 | }
21 |
22 | .shrink{
23 | width: 42px;
24 | }
25 |
26 | .side-bar-header {
27 | padding: 9px 8px;
28 | background: #212123;
29 | border-bottom: 1px solid #000;
30 | border-top: 1px solid #000;
31 | width: 200px;
32 | }
33 |
34 | .side-bar-shrink {
35 | border-top: 1px solid #393A3C;
36 | width: 200px;
37 | padding:10px 10px;
38 | }
39 |
40 | .side-bar-shrink > button {
41 | background: image-url('icons/menu-white.png');
42 | background-size: cover;
43 | height: 10px;
44 | width: 6px;
45 | opacity: .3;
46 | }
47 |
48 | .side-bar-header > button {
49 | background: image-url('icons/person-icon-white.png');
50 | background-size: cover;
51 | height: 9px;
52 | width: 3px;
53 | opacity: .3;
54 | }
55 |
56 | .side-bar-header > button:hover {
57 | opacity: .6;
58 | }
59 |
60 | .side-bar-header > span {
61 | font-size: 9px;
62 | font-weight: 400;
63 | color: #BBB8B6;
64 | padding: 0;
65 | }
66 |
67 | .private-page {
68 | text-align: center;
69 | background: #F3F4F4;
70 | color: #3C4858;
71 | height: 100vh;
72 | padding-top: 15%;
73 | }
74 |
75 | .private-page h1 {
76 | font-size: 32px;
77 | font-weight:600;
78 | }
79 |
80 | .private-page h2 {
81 | font-size: 20px;
82 | color: #6F6F6F;
83 | }
84 |
85 | .side-bar-nav {
86 | color: #CCCCCC;
87 | font-size: 12px;
88 | font-weight: normal;
89 | padding: 0 10px;
90 | }
91 |
92 | .side-bar-nav li {
93 | padding: 7px 8px 6px 0;
94 | margin: 0;
95 | text-shadow: 0 0 3px black;
96 | width: 190px;
97 | }
98 |
99 | .side-bar-nav li:hover {
100 | cursor: pointer;
101 | color: #a7bacc;
102 | }
103 |
104 | .side-bar-nav span {
105 | height: 6px;
106 | margin-right: 18px;
107 | opacity: .5;
108 | }
109 |
110 |
111 | li.selected-filter {
112 | color: #87C7F8;
113 | }
114 |
115 | .selected-filter span {
116 | opacity: 1;
117 | }
118 |
119 | li.selected-filter:hover {
120 | color: #87C7F8;
121 | }
122 |
123 | li.newPixel-item {
124 |
125 | }
126 |
127 | .current-icon {
128 | background: image-url('icons/list-icon-white.png');
129 | background-size: cover;
130 | }
131 |
132 | .icebox-icon {
133 | background: image-url('icons/icebox-icon-white.png');
134 | background-size: cover;
135 | }
136 |
137 | .done-icon {
138 | background: image-url('icons/check-icon-white.png');
139 | background-size: cover;
140 | }
141 |
142 | .newPixel-icon {
143 | background: image-url('icons/plus-icon-white.png');
144 | background-size: cover;
145 | }
146 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/reset.css:
--------------------------------------------------------------------------------
1 | html, body, header, nav, h1, a, ul, li,
2 | strong, main, section, img, div, h2, h3, h4, p,
3 | form, fieldset, label, input, textarea,
4 | button, article, footer, small, span {
5 | margin: 0;
6 | padding: 0;
7 | border: 0;
8 | outline: 0;
9 | font: inherit;
10 | box-sizing: inherit;
11 | text-align: inherit;
12 | text-decoration: inherit;
13 | vertical-align: inherit;
14 | color: inherit;
15 | background: transparent;
16 | }
17 |
18 | ul, li {
19 | list-style: none;
20 | }
21 |
22 | .group:after {
23 | clear: both;
24 | content: "";
25 | display: block;
26 | }
27 |
28 | body {
29 | font-family: 'Open Sans', sans-serif;
30 | background: #F3F4F4;
31 | }
32 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/splash_page.css:
--------------------------------------------------------------------------------
1 |
2 | .splash-page {
3 | position: relative;
4 | background: #b2d1f1;
5 | border-top: 1px solid #7fb5ed;
6 | border-bottom: 1px solid #7fb5ed;
7 | overflow: hidden;
8 | height: 100vh;
9 | }
10 |
11 | .splash-page > img {
12 | min-width: 2469px;
13 | min-height: 1029px;
14 | display: block;
15 | position: relative;
16 | opacity: 0.6;
17 | filter: blur(1.4px);
18 | margin: 0 auto;
19 | width: 100%;
20 | }
21 |
22 | img.blur {
23 | filter: blur(2.7px);
24 | }
25 |
26 | .splash-center-box {
27 | position: absolute;
28 | left: 50%;
29 | top: 40%;
30 | transform: translate(-50%, -50%);
31 | width: 90%;
32 | }
33 |
34 | .splash-page-text {
35 | display: block;
36 | color: #fff;
37 | font-size: 32px;
38 | font-weight: 400;
39 | text-align: center;
40 | font-family: 'Merriweather', serif;
41 | width: 100%;
42 | text-shadow: 0 0 20px black;
43 | transition: opacity 0.7s;
44 | }
45 |
46 | .splash-page-footer {
47 | display: block;
48 | border-top: 3px solid #F39E40;
49 | background: #eee;
50 | width: 100%;
51 | color: #50565c;
52 | position: absolute;
53 | bottom: 0;
54 | }
55 |
56 | .splash-footer-logo {
57 | margin-left: 2%;
58 | color: #50565c;
59 | font-size: 12px;
60 | margin: 10px 20px;
61 | padding: 2px;
62 | font-weight: 400;
63 | }
64 |
65 | .splash-footer-logo > a:hover {
66 | text-decoration: underline;
67 | color: #899eb3;
68 | }
69 |
70 | .splash-footer-container > a {
71 | float: right;
72 | display: block;
73 | font-size: 12px;
74 | padding: 2px;
75 | margin: 10px 20px;
76 | }
77 |
78 | .splash-footer-container > a:hover {
79 | color: #899eb3;
80 | text-decoration: underline;
81 | }
82 |
83 | .splash-footer-container {
84 | max-width: 860px;
85 | margin: 0 auto;
86 | }
87 |
88 | .splash-get-started-button {
89 | display: block;
90 | width: 120px;
91 | color: #fff;
92 | text-align: center;
93 | margin: 30px auto;
94 | padding: 8px 10px;
95 | font-size: 18px;
96 | background: #F39E40;
97 | border-radius: 3px;
98 | font-weight: 300;
99 | }
100 |
101 | .splash-get-started-button:hover {
102 | background: #EE7D00;
103 | }
104 |
105 | .fade {
106 | opacity: 0;
107 | }
108 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/tasks.scss:
--------------------------------------------------------------------------------
1 | .tasks-container {
2 | padding: 5px;
3 |
4 | h2 {
5 | text-transform: uppercase;
6 | font-size: 11px;
7 | font-weight: 700;
8 | margin-bottom: 2px;
9 | }
10 |
11 | .new-task-form {
12 | background: #DFDCD0;
13 | border-radius: 3px;
14 | padding: 5px;
15 | display: flex;
16 | align-items: center;
17 | margin-bottom: 5px;
18 | }
19 |
20 | input, button {
21 | display: inline-block;
22 | }
23 |
24 | .add-task-button {
25 | padding: 4px 12px;
26 | color: #fff;
27 | text-shadow: 0 0 0;
28 | font-size: 10px;
29 | background: #666666;
30 | border: 1px solid #555555;
31 | border-radius: 3px;
32 | font-weight: 600;
33 | }
34 |
35 | .add-task-button:hover {
36 | background: #2B4C74;
37 | border: 1px solid #24364C;
38 | }
39 |
40 | .new-task-body {
41 | background: #fff;
42 | border: 1px solid #ADB0B7;
43 | border-radius: 3px;
44 | padding: 1px 6px 2px 6px;
45 | margin-left: 5px;
46 | margin-right: 8px;
47 | flex-grow: 1;
48 | flex-shrink: 1;
49 | }
50 |
51 | p {
52 | flex-grow: 1;
53 | margin-left: 5px;
54 | margin-right: 8px;
55 | padding: 3px 7px 4px 7px;
56 | }
57 |
58 | .edit-button {
59 | padding: 0;
60 | margin-left: 5px;
61 | }
62 |
63 | .cancel-task-button {
64 | font-size: 11px;
65 | padding: 0;
66 | margin-right: 8px;
67 | }
68 |
69 | .cancel-task-button:hover {
70 | text-decoration: underline;
71 | }
72 |
73 | .update-button {
74 | padding-left: 4px;
75 | padding-right: 4px;
76 | }
77 |
78 | .complete-task {
79 | background: #B9D089;
80 | }
81 |
82 | .task-complete {
83 | width: 12px;
84 | height: 12px;
85 | flex-grow: 0;
86 | flex-shrink: 0;
87 | margin-left: 4px;
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/api/comments_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::CommentsController < ApplicationController
2 |
3 | before_action :require_logged_in
4 |
5 | def create
6 | comment = current_user.comments.new(comment_params);
7 | if comment.save
8 | @pixel = comment.pixel
9 | render 'api/pixels/show'
10 | else
11 | render json: comment.errors, status: 422
12 | end
13 | end
14 |
15 | def update
16 | comment = Comment.find(params[:id])
17 | if comment.update(comment_params)
18 | @pixel = comment.pixel
19 | render 'api/pixels/show'
20 | else
21 | render json: comment.errors, status: 422
22 | end
23 | end
24 |
25 | def destroy
26 | comment = Comment.find_by(id: params[:id])
27 | if comment
28 | comment.destroy!
29 | @pixel = comment.pixel
30 | render 'api/pixels/show'
31 | else
32 | render json: { errors: "Cannot Find Comment" }, status: 404
33 | end
34 | end
35 |
36 | private
37 |
38 | def comment_params
39 | params.require(:comment).permit(:body, :pixel_id)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/controllers/api/pixels_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::PixelsController < ApplicationController
2 | before_action :check_member, only: [:create, :update, :destroy]
3 | before_action :require_logged_in
4 |
5 | def index
6 | project = Project.find(params[:project_id])
7 | @pixels = project.pixels.order(:pixel_ord).includes(:requester).includes(:comments).includes(:commenters).includes(:tasks)
8 | render :index
9 | end
10 |
11 | def show
12 | @pixel = Pixel.includes(:comments).includes(:commenters).includes(:tasks).find(params[:id])
13 | render :show
14 | end
15 |
16 | def create
17 | project = Project.find(params[:project_id])
18 | @pixel = project.pixels.new(pixel_params)
19 | @pixel.requester_id = current_user.id
20 | if @pixel.save
21 | render :show
22 | else
23 | render json: @pixel.errors, status: 422
24 | end
25 | end
26 |
27 | def update
28 | @pixel = Pixel.find(params[:id])
29 | if @pixel.update(pixel_params)
30 | render :show
31 | else
32 | render json: @pixel.errors, status: 422
33 | end
34 | end
35 |
36 | def destroy
37 | @pixel = Pixel.find_by(id: params[:id])
38 | if @pixel
39 | @pixel.destroy!
40 | render json: {}
41 | else
42 | render json: { error: "Not Found" }, status: 404
43 | end
44 | end
45 |
46 | def mass_update
47 | pixels = params[:pixels]
48 | updatedPixels = Pixel.update(pixels.keys, pixels.values)
49 | if updatedPixels
50 | render json: { sucess: "Sucessfully Updated" }
51 | else
52 | render json: { error: "Update Failed" }, status: 422
53 | end
54 | end
55 |
56 | private
57 |
58 | def pixel_params
59 | params.require(:pixel).permit(:state, :icebox, :title, :category, :pixel_ord, :description, :points, tasks_attributes: [:body, :pixel_id, :task_ord, :complete])
60 | end
61 |
62 | def check_member()
63 | project = Project.find_by(id: params[:project_id])
64 | project ||= Pixel.find(params[:id]).project
65 | unless project.members.pluck(:username).include?(current_user.username)
66 | render json: { base:["Cannot Edit Project If You are Not the User"] }, status: 401
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/app/controllers/api/project_members_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::ProjectMembersController < ApplicationController
2 | before_action :require_logged_in
3 |
4 | def create
5 | @project = Project.find(params[:project_member][:project_id])
6 | user = User.find_by(username: params[:project_member][:username])
7 |
8 | if user
9 | project_member = @project.project_members.new(user_id: user.id)
10 | if project_member.save
11 | render 'api/projects/show'
12 | else
13 | render json: project_member.errors.full_messages, status: 422
14 | end
15 | else
16 | render json: ["Cannot Find User"], status: 404
17 | end
18 |
19 | end
20 |
21 | def destroy
22 | project_member = ProjectMember.find(params[:id])
23 | if project_member
24 | @project = project_member.project
25 | project_member.destroy!
26 | render 'api/projects/show'
27 | else
28 | render json: ["Member Not Found"], status: 404
29 | end
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/app/controllers/api/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::ProjectsController < ApplicationController
2 | before_action :require_logged_in
3 |
4 | def index
5 | @projects = current_user.projects.includes(:project_members).includes(:members)
6 | render :index
7 | end
8 |
9 | def show
10 | @project = Project.includes(:project_members).includes(:members).find_by(id: params[:id])
11 |
12 | if @project && allow_access?(@project)
13 | render :show
14 | else
15 | render json: ["Project Not Found or Private"], status: 404
16 | end
17 | end
18 |
19 | def create
20 | @project = current_user.projects.new(project_params)
21 |
22 | if @project.save
23 | render :show
24 | else
25 | render json: @project.errors.full_messages, status: 422
26 | end
27 |
28 | end
29 |
30 | private
31 |
32 | def project_params
33 | params.require(:project).permit(:name, :private)
34 | end
35 |
36 |
37 | def allow_access?(project)
38 | !project.private || project.members.pluck(:username).include?(current_user.username)
39 | end
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/app/controllers/api/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::SessionsController < ApplicationController
2 | def create
3 | @user = User.find_by_credentails(params[:user][:username], params[:user][:password])
4 | if @user
5 | login(@user)
6 | render 'api/users/show'
7 | else
8 | render json: ["Invalid Login"], status: 401
9 | end
10 | end
11 |
12 | def destroy
13 | if logged_in?
14 | logout
15 | render json: {}
16 | else
17 | render json: ["Not Logged In"], status: 404
18 | end
19 | end
20 |
21 | end
22 |
--------------------------------------------------------------------------------
/app/controllers/api/tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::TasksController < ApplicationController
2 | before_action :require_logged_in
3 |
4 | def create
5 | task = Task.new(task_params);
6 | if task.save
7 | @pixel = task.pixel
8 | render 'api/pixels/show'
9 | else
10 | render json: task.errors, status: 422
11 | end
12 | end
13 |
14 | def update
15 | task = Task.find_by(id: params[:id])
16 | if task
17 | if task.update(task_params)
18 | @pixel = task.pixel
19 | render 'api/pixels/show'
20 | else
21 | render json: task.errors, status: 422
22 | end
23 | else
24 | render json: { errors: "Cannot Find Task" }, status: 404
25 | end
26 | end
27 |
28 | def destroy
29 | task = Task.find_by(id: params[:id])
30 | if task
31 | task.destroy!
32 | @pixel = task.pixel
33 | render 'api/pixels/show'
34 | else
35 | render json: task.errors, status: 422
36 | end
37 | end
38 |
39 | private
40 |
41 | def task_params
42 | params.require(:task).permit(:body, :pixel_id, :task_ord, :complete)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/controllers/api/users_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::UsersController < ApplicationController
2 |
3 | def create
4 | @user = User.new(user_params)
5 | if @user.save
6 | login(@user)
7 | render :show
8 | else
9 | render json: @user.errors.full_messages, status: 422
10 | end
11 | end
12 |
13 | private
14 |
15 | def user_params
16 | params.require(:user).permit(:username, :password, :email)
17 | end
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 |
4 | helper_method :current_user, :logged_in?
5 |
6 | private
7 |
8 | def current_user
9 | return nil unless session[:session_token]
10 | @current_user ||= User.find_by(session_token: session[:session_token])
11 | end
12 |
13 | def logged_in?
14 | !!current_user
15 | end
16 |
17 | def login(user)
18 | session[:session_token] = user.reset_session_token!
19 | @current_user = user
20 | end
21 |
22 | def logout
23 | current_user.reset_session_token!
24 | @current_user = nil
25 | session[:session_token] = nil
26 | end
27 |
28 | def require_logged_in
29 | render json: { base: ['FORBIDDEN'] }, status: 401 if !current_user
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/static_pages_controller.rb:
--------------------------------------------------------------------------------
1 | class StaticPagesController < ApplicationController
2 | def root
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/helpers/api/comments_helper.rb:
--------------------------------------------------------------------------------
1 | module Api::CommentsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/api/pixels_helper.rb:
--------------------------------------------------------------------------------
1 | module Api::PixelsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/api/projects_helper.rb:
--------------------------------------------------------------------------------
1 | module Api::ProjectsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/api/sessions_helper.rb:
--------------------------------------------------------------------------------
1 | module Api::SessionsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/api/tasks_helper.rb:
--------------------------------------------------------------------------------
1 | module Api::TasksHelper
2 | end
3 |
--------------------------------------------------------------------------------
/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/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/comment.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: comments
4 | #
5 | # id :integer not null, primary key
6 | # body :text not null
7 | # pixel_id :integer not null
8 | # user_id :integer not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 |
13 | class Comment < ApplicationRecord
14 | validates :body, :user, :pixel, presence: true
15 |
16 | belongs_to :pixel
17 | belongs_to :user
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/pixel.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: pixels
4 | #
5 | # id :integer not null, primary key
6 | # pixel_ord :integer not null
7 | # state :string not null
8 | # title :string not null
9 | # category :string not null
10 | # description :text
11 | # points :integer not null
12 | # project_id :integer not null
13 | # requester_id :integer not null
14 | # created_at :datetime not null
15 | # updated_at :datetime not null
16 | # icebox :boolean default(TRUE), not null
17 | #
18 |
19 | class Pixel < ApplicationRecord
20 | valid_states = %w(Unstarted Started Finished Delivered Rejected Accepted)
21 | valid_categories = %w(Feature Bug Chore Release)
22 |
23 | validates :pixel_ord,
24 | :state,
25 | :title,
26 | :category,
27 | :points,
28 | :project,
29 | :requester,
30 | presence: true
31 |
32 | validates :state, inclusion: { in: valid_states, message: "is an invalid state" }
33 | validates :category, inclusion: {in: valid_categories, message: "is an invalid category"}
34 | validates :icebox, inclusion: { in: [ true, false ] }
35 |
36 | belongs_to :project
37 |
38 | belongs_to :requester,
39 | class_name: :User,
40 | foreign_key: :requester_id
41 |
42 | has_many :comments, dependent: :destroy
43 |
44 | has_many :commenters,
45 | through: :comments,
46 | source: :user
47 |
48 | has_many :tasks,
49 | dependent: :destroy,
50 | inverse_of: :pixel
51 |
52 | accepts_nested_attributes_for :tasks, allow_destroy: true
53 | end
54 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: projects
4 | #
5 | # id :integer not null, primary key
6 | # name :string not null
7 | # private :boolean not null
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 |
12 | class Project < ApplicationRecord
13 | validates :name, presence: true
14 | validates :private, inclusion: { in: [ true, false ], message: "is not defined" }
15 |
16 | has_many :project_members, dependent: :destroy, inverse_of: :project
17 |
18 | has_many :members,
19 | through: :project_members,
20 | source: :user
21 |
22 | has_many :pixels,
23 | dependent: :destroy
24 | end
25 |
--------------------------------------------------------------------------------
/app/models/project_member.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_members
4 | #
5 | # id :integer not null, primary key
6 | # project_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 ProjectMember < ApplicationRecord
13 | validates :project, :user, presence: :true
14 | validates :user_id, uniqueness: { scope: :project_id }
15 |
16 | belongs_to :user, inverse_of: :project_members
17 | belongs_to :project, inverse_of: :project_members
18 | end
19 |
--------------------------------------------------------------------------------
/app/models/task.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: tasks
4 | #
5 | # id :integer not null, primary key
6 | # body :text not null
7 | # complete :boolean default(FALSE), not null
8 | # pixel_id :integer not null
9 | # task_ord :integer not null
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | #
13 |
14 | class Task < ApplicationRecord
15 | validates :body, :pixel, :task_ord, presence: true
16 | validates :complete, inclusion: { in: [ true, false ], message: "is not defined" }
17 |
18 | belongs_to :pixel,
19 | inverse_of: :tasks
20 |
21 | default_scope { order(:task_ord) }
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/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 not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | # email :string
12 | #
13 |
14 | class User < ApplicationRecord
15 | validates :username, presence: true, uniqueness: true
16 | validates :password_digest, presence: true
17 | validates :session_token, presence: true, uniqueness: true
18 | validates :password, length: { minimum: 6, allow_nil: true }
19 |
20 | after_initialize :ensure_session_token
21 |
22 | has_many :project_members, dependent: :destroy, inverse_of: :user
23 |
24 | has_many :projects,
25 | through: :project_members,
26 | source: :project
27 |
28 | has_many :comments
29 |
30 | attr_reader :password
31 |
32 | def self.generate_session_token
33 | SecureRandom.urlsafe_base64(32)
34 | end
35 |
36 | def self.find_by_credentails(username, password)
37 | user = User.find_by(username: username)
38 | user && user.is_password?(password) ? user : nil
39 | end
40 |
41 | def reset_session_token!
42 | self.session_token = User.generate_session_token
43 | save!
44 | self.session_token
45 | end
46 |
47 | def password=(password)
48 | @password = password
49 | self.password_digest = BCrypt::Password.create(password)
50 | end
51 |
52 | def is_password?(password)
53 | BCrypt::Password.new(self.password_digest).is_password?(password)
54 | end
55 |
56 | private
57 |
58 | def ensure_session_token
59 | self.session_token ||= User.generate_session_token
60 | end
61 |
62 | end
63 |
--------------------------------------------------------------------------------
/app/views/api/pixels/_pixel.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! pixel, :id, :title, :category, :description, :points, :project_id, :pixel_ord, :icebox, :state
2 | json.requester pixel.requester.username
3 | json.updated_at pixel.updated_at.strftime("%m-%d-%Y %H:%M:%S")
4 | json.comments do
5 | json.array! pixel.comments do |comment|
6 | json.id comment.id
7 | json.body comment.body
8 | json.user comment.user.username
9 | json.pixel_id comment.pixel_id
10 | json.created_at comment.created_at.strftime("%m-%d-%Y, %I:%M %p")
11 | end
12 | end
13 | json.tasks do
14 | json.array! pixel.tasks do |task|
15 | json.id task.id
16 | json.body task.body
17 | json.pixel_id task.pixel_id
18 | json.task_ord task.task_ord
19 | json.complete task.complete
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/app/views/api/pixels/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @pixels.each do |pixel|
2 | json.set! pixel.id do
3 | json.extract! pixel, :id, :title, :category, :description, :points, :project_id, :pixel_ord, :icebox, :state
4 | json.requester pixel.requester.username
5 | json.updated_at pixel.updated_at.strftime("%m-%d-%Y %H:%M:%S")
6 | json.comments do
7 | json.array! pixel.comments do |comment|
8 | json.id comment.id
9 | json.body comment.body
10 | json.user comment.user.username
11 | json.pixel_id comment.pixel_id
12 | json.created_at comment.created_at.strftime("%m-%d-%Y, %I:%M %p")
13 | end
14 | end
15 | json.tasks do
16 | json.array! pixel.tasks do |task|
17 | json.id task.id
18 | json.body task.body
19 | json.pixel_id task.pixel_id
20 | json.task_ord task.task_ord
21 | json.complete task.complete
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/api/pixels/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! "api/pixels/pixel", pixel: @pixel
2 |
--------------------------------------------------------------------------------
/app/views/api/projects/_projects.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.id project.id
2 | json.name project.name
3 | json.privacy project.private
4 | json.updated_at project.updated_at.strftime("%m-%d-%Y %H:%M:%S")
5 | json.members do
6 | json.array! project.project_members do |project_member|
7 | json.project_member_id project_member.id
8 | json.member_name project_member.user.username
9 | json.member_email project_member.user.email
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/views/api/projects/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @projects.each do |project|
2 | json.set! project.id do
3 | json.id project.id
4 | json.name project.name
5 | json.privacy project.private
6 | json.updated_at project.updated_at.strftime("%m-%d-%Y %H:%M:%S")
7 | json.members do
8 | json.array! project.project_members do |project_member|
9 | json.project_member_id project_member.id
10 | json.member_name project_member.user.username
11 | json.member_email project_member.user.email
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/api/projects/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! "api/projects/projects", project: @project
2 |
--------------------------------------------------------------------------------
/app/views/api/users/_user.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! user, :username
2 |
--------------------------------------------------------------------------------
/app/views/api/users/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! "api/users/user", user: @user
2 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PixelTracker
5 | <%= csrf_meta_tags %>
6 |
7 | <%= stylesheet_link_tag 'application', media: 'all' %>
8 | <%= javascript_include_tag 'application' %>
9 |
10 |
11 |
12 |
13 |
14 | <%= yield %>
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/views/static_pages/root.html.erb:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | if spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
13 | gem 'spring', spring.version
14 | require 'spring/binstub'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/config/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 PixelTracker
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/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/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 |
--------------------------------------------------------------------------------
/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: pixel_tracker_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: pixel_tracker
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 order:
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: pixel_tracker_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: pixel_tracker_production
84 | username: pixel_tracker
85 | password: <%= ENV['PIXEL_TRACKER_DATABASE_PASSWORD'] %>
86 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 = "pixel_tracker_#{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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 Rails 5.0 release notes for more info on each option.
6 |
7 | # Enable per-form CSRF tokens. Previous versions had false.
8 | Rails.application.config.action_controller.per_form_csrf_tokens = true
9 |
10 | # Enable origin-checking CSRF mitigation. Previous versions had false.
11 | Rails.application.config.action_controller.forgery_protection_origin_check = true
12 |
13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
14 | # Previous versions had false.
15 | ActiveSupport.to_time_preserves_timezone = true
16 |
17 | # Require `belongs_to` associations by default. Previous versions had false.
18 | Rails.application.config.active_record.belongs_to_required_by_default = true
19 |
20 | # Do not halt callback chains when a callback returns false. Previous versions had true.
21 | ActiveSupport.halt_callback_chains_on_return_false = false
22 |
23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false.
24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } }
25 |
--------------------------------------------------------------------------------
/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: '_pixel_tracker_session'
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 "static_pages#root"
4 |
5 | namespace :api, defaults: { format: :json } do
6 | resources :users, only: [:create]
7 | resource :session, only: [:create, :destroy]
8 | resources :projects, only: [:index, :show, :create] do
9 | resources :pixels, only: [:index, :create]
10 | end
11 | resources :project_members, only: [:destroy, :create]
12 | resources :pixels ,only: [:destroy, :update, :show]
13 | resources :comments, only: [:create, :update, :destroy]
14 | resources :tasks, only: [:create, :update, :destroy]
15 | patch '/mass_update_pixels', :to => 'pixels#mass_update'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/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: 6bfe3c00dedcb94f2ecee4c0dd008e4c7f14aba039898f19c31a0492ff1fbebb450afe70c89d91398a9d3b39330f68f1e56440382a491f19277308ec0ec2ad91
15 |
16 | test:
17 | secret_key_base: d634a878f92e3d624bda68bad605eecf32d1bbf905f0b7b55abe95ffbc2d5ddc64f3e22840eccfbd2bd4b7e3135a9fe8dd7e099ea9d22acfb341e8c6ee639b02
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/db/migrate/20161206151623_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, null: false
7 |
8 | t.timestamps
9 | end
10 |
11 | add_index :users, :username, unique: true
12 | add_index :users, :session_token, unique: true
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/migrate/20161206160412_add_email_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddEmailToUsers < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :users, :email, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20161207171903_create_projects.rb:
--------------------------------------------------------------------------------
1 | class CreateProjects < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :projects do |t|
4 | t.string :name, null: false
5 | t.boolean :private, null: false
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20161207172015_create_project_members.rb:
--------------------------------------------------------------------------------
1 | class CreateProjectMembers < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :project_members do |t|
4 | t.integer :project_id, null: false, index: true
5 | t.integer :user_id, null: false, index: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20161207191211_add_unique_index_to_project_members.rb:
--------------------------------------------------------------------------------
1 | class AddUniqueIndexToProjectMembers < ActiveRecord::Migration[5.0]
2 | def change
3 | add_index :project_members, [:user_id, :project_id], unique: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20161209213350_create_pixels.rb:
--------------------------------------------------------------------------------
1 | class CreatePixels < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :pixels do |t|
4 | t.integer :story_ord, null: false
5 | t.string :state, null: false, index: true
6 | t.string :title, null: false
7 | t.string :category, null: false, index: true
8 | t.text :description
9 | t.integer :points, null: false
10 | t.integer :project_id, null: false, index: true
11 | t.integer :requester_id, null: false, index: true
12 |
13 | t.timestamps
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/migrate/20161209220715_change_ord_name_in_pixel_table.rb:
--------------------------------------------------------------------------------
1 | class ChangeOrdNameInPixelTable < ActiveRecord::Migration[5.0]
2 | def change
3 | rename_column :pixels, :story_ord, :pixel_ord
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20161211152624_add_icebox_boolean_to_pixels.rb:
--------------------------------------------------------------------------------
1 | class AddIceboxBooleanToPixels < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :pixels, :icebox, :boolean, null: false, default: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20161212220648_create_comments.rb:
--------------------------------------------------------------------------------
1 | class CreateComments < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :comments do |t|
4 | t.text :body, null: false
5 | t.integer :pixel_id, null: false, index: true
6 | t.integer :user_id ,null: false, index: true
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20161213040831_create_tasks.rb:
--------------------------------------------------------------------------------
1 | class CreateTasks < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :tasks do |t|
4 | t.text :body, null: false
5 | t.boolean :complete, null: false, default: false
6 | t.integer :pixel_id, null: false, index: true
7 | t.integer :task_ord, null: false
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/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: 20161213040831) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "comments", force: :cascade do |t|
19 | t.text "body", null: false
20 | t.integer "pixel_id", null: false
21 | t.integer "user_id", null: false
22 | t.datetime "created_at", null: false
23 | t.datetime "updated_at", null: false
24 | t.index ["pixel_id"], name: "index_comments_on_pixel_id", using: :btree
25 | t.index ["user_id"], name: "index_comments_on_user_id", using: :btree
26 | end
27 |
28 | create_table "pixels", force: :cascade do |t|
29 | t.integer "pixel_ord", null: false
30 | t.string "state", null: false
31 | t.string "title", null: false
32 | t.string "category", null: false
33 | t.text "description"
34 | t.integer "points", null: false
35 | t.integer "project_id", null: false
36 | t.integer "requester_id", null: false
37 | t.datetime "created_at", null: false
38 | t.datetime "updated_at", null: false
39 | t.boolean "icebox", default: true, null: false
40 | t.index ["category"], name: "index_pixels_on_category", using: :btree
41 | t.index ["project_id"], name: "index_pixels_on_project_id", using: :btree
42 | t.index ["requester_id"], name: "index_pixels_on_requester_id", using: :btree
43 | t.index ["state"], name: "index_pixels_on_state", using: :btree
44 | end
45 |
46 | create_table "project_members", force: :cascade do |t|
47 | t.integer "project_id", null: false
48 | t.integer "user_id", null: false
49 | t.datetime "created_at", null: false
50 | t.datetime "updated_at", null: false
51 | t.index ["project_id"], name: "index_project_members_on_project_id", using: :btree
52 | t.index ["user_id", "project_id"], name: "index_project_members_on_user_id_and_project_id", unique: true, using: :btree
53 | t.index ["user_id"], name: "index_project_members_on_user_id", using: :btree
54 | end
55 |
56 | create_table "projects", force: :cascade do |t|
57 | t.string "name", null: false
58 | t.boolean "private", null: false
59 | t.datetime "created_at", null: false
60 | t.datetime "updated_at", null: false
61 | end
62 |
63 | create_table "tasks", force: :cascade do |t|
64 | t.text "body", null: false
65 | t.boolean "complete", default: false, null: false
66 | t.integer "pixel_id", null: false
67 | t.integer "task_ord", null: false
68 | t.datetime "created_at", null: false
69 | t.datetime "updated_at", null: false
70 | t.index ["pixel_id"], name: "index_tasks_on_pixel_id", using: :btree
71 | end
72 |
73 | create_table "users", force: :cascade do |t|
74 | t.string "username", null: false
75 | t.string "password_digest", null: false
76 | t.string "session_token", null: false
77 | t.datetime "created_at", null: false
78 | t.datetime "updated_at", null: false
79 | t.string "email"
80 | t.index ["session_token"], name: "index_users_on_session_token", unique: true, using: :btree
81 | t.index ["username"], name: "index_users_on_username", unique: true, using: :btree
82 | end
83 |
84 | end
85 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Pixel Tracker
2 |
3 | [Live Website](http://www.pixeltracker.org)
4 | [Trello](https://trello.com/b/hOvAm6f2/pixeltracker)
5 |
6 | ### Minimum Viable Product
7 |
8 | Pixel Tracker is an agile project management app based off of the popular
9 | Pivotal Tracker.
10 |
11 | #### Features
12 | - [ ] New account creation, login, and guest/demo login
13 | - [ ] Projects/Project Page
14 | - [ ] Stories
15 | - [ ] Story Workflow
16 | - [ ] Drag and Drop Prioritization
17 | - [ ] A production README
18 | - [ ] Hosting on Heroku
19 |
20 | ### Design Docs
21 | 1. [Wireframes](/docs/wireframes)
22 | 2. [React Components](/docs/component-hierarchy.md)
23 | 3. [Sample State](/docs/sample-state.md)
24 | 4. [DB Schema](/docs/schema.md)
25 | 5. [API Endponts](/docs/api-endpoints.md)
26 |
27 | ### Implementation Timeline
28 | #### Phase 1: User Authentication (2 days)
29 | __Objective:__ Complete full back-end and front-end user authentication
30 | #### Phase 2: Projects (2 days)
31 | __Objective:__ Create projects homepage and the ability to create new projects. Also create the singular project page layout with no stories.
32 | #### Phase 3: Stories Back-End and Components (2 days)
33 | __Objective:__ Have the ability to create, update, and view stories, as well as add labels, activity, and tasks through the api.
34 | #### Phase 4: Stories Display (1 day)
35 | __Objective:__ All new stories should be displayed in the icebox in the main project page.
36 | #### Phase 5: Story Workflow (1 day)
37 | __Objective:__ Story status changes will move them to the appropriate display categories in the main project page. Users will have the ability to restart the chain of events by rejecting at the final step.
38 | #### Phase 6: Drag and Drop Prioritization (1 day)
39 | __Objective:__ By default items with higher points will be placed on top, but users can manually drag and drop items to change the order. These can only apply to stories with similar statuses.
40 |
41 | #### Additional Features
42 | - [ ] Iterations
43 | - [ ] Velocity
44 | - [ ] User Permissions
45 |
--------------------------------------------------------------------------------
/docs/api-endpoints.md:
--------------------------------------------------------------------------------
1 | ## API Endpoints
2 | ### HTML API
3 | #### Root
4 | * GET / - gets root React web app
5 |
6 | ### JSON API
7 | #### Users
8 | * POST /api/users
9 | * PATCH /api/users
10 |
11 | #### Session
12 | * POST /api/session
13 | * DELETE /api/session
14 |
15 | #### Project
16 | * POST /api/projects
17 | * DELETE /api/projects
18 | * GET /api/projects
19 | * GET /api/projects/:id
20 |
21 | #### ProjectMembers
22 | * GET /api/projects/:id/members
23 | * POST /api/projects/:id/members
24 | * DELETE /api/projects/:project_id/members/:member_id
25 |
26 | #### Story
27 | * POST /api/projects/:id/stories
28 | * DELETE /api/projects/:project_id/stories/:story_id
29 | * GET /api/projects/:project_id/stories/:story_id
30 | * GET /api/projects/:project_id/stories/
31 | * PATCH /api/projects/:project_id/stories/:story_id
32 |
33 | ### StoryFollowers
34 | * POST /api/storyfollowers/
35 | * DELETE /api/storyfollowers/:id
36 |
37 | ### StoryOwners
38 | * POST /api/storyowners/
39 | * DELETE /api/storyowners/:id
40 |
41 | #### Tasks
42 | * Shows up on story detail view
43 | * POST /api/projects/:project_id/stories/:story_id/tasks
44 | * PATCH /api/projects/:project_id/stories/:story_id/tasks/:task_id
45 | * DELETE /api/projects/:project_id/stories/:story_id/tasks/:task_id
46 |
47 | #### Labels
48 | * Shows up on story detail view
49 | * GET /api/labels/
50 | * POST /api/labels/
51 |
52 | #### StoryLabels
53 | * POST /api/storylabels
54 | * DELETE /api/storylabels/:id
55 |
56 | #### Activity
57 | * Shows up on story detail view
58 | * POST /api/projects/:project_id/stories/:story_id/activities
59 | * PATCH /api/projects/:project_id/stories/:story_id/activities/:activity_id
60 | * DELETE /api/projects/:project_id/stories/:story_id/activities/:activity_id
61 |
--------------------------------------------------------------------------------
/docs/component-hierarchy.md:
--------------------------------------------------------------------------------
1 | ### Component Hierarchy
2 |
3 | #### AuthformContainer
4 | * AuthForm
5 |
6 | #### DashboardContainer
7 | * Header
8 | * ProjectIndex
9 |
10 | #### ProjectContainer
11 | * ProjectHeader
12 | * Sidebar
13 | * StoryIndex
14 |
15 | #### NewProjectContainer
16 | * NewProject
17 |
18 | #### StoryContainer
19 | * StoryDetailView
20 |
21 | #### MemberContainer
22 | * ProjectHeader
23 | * NewMember
24 | * MemberIndex
25 |
26 | #### ProjectIndex
27 | * ProjectSummaryView
28 |
29 | #### StoryIndex
30 | * StoryFilterColumn
31 | * StorySummaryView
32 |
33 | #### StoryDetailView
34 | * StoryName
35 | * StoryTags
36 | * StoryDescription
37 | * LabelsContainer
38 | * LabelsIndex
39 | * NewLabels
40 | * TasksContainer
41 | * TasksIndex
42 | * NewTasks
43 | * ActivityContainer
44 | * ActivityIndex
45 | * NewActivity
46 |
47 | ### Routes
48 | | Path | Component |
49 | | --- | --- |
50 | | /signup | AuthformContainer |
51 | | /signin | AuthformContainer |
52 | | /dashboard | DashboardContainer |
53 | | /projects/new | NewProjectContainer |
54 | | /projects/:projectid | ProjectContainer |
55 | | /projects/:projectid/stories/:storyid | StoryContainer |
56 | | /projects/:projectid/members/ | MemberContainer |
57 |
--------------------------------------------------------------------------------
/docs/sample-state.md:
--------------------------------------------------------------------------------
1 | ```javascript
2 | {
3 | currentUser: {
4 | id: 1,
5 | username: 'guest',
6 | errors: [],
7 | },
8 | projects: {
9 | 1: {
10 | id: 1,
11 | name: 'Sample Project',
12 | }
13 | 2: {
14 | id: 2,
15 | name: 'Sample Project 2',
16 | }
17 | }
18 | stories: {
19 | 5: {
20 | id: 5,
21 | category: 'chore',
22 | state: 'accepted',
23 | labels: ['deployment, epic'],
24 | followers: [],
25 | points: 0,
26 | ord: 1,
27 | }
28 | 7: {
29 | id: 7,
30 | category: 'feature',
31 | state: 'started',
32 | labels: ['important'],
33 | followers: [],
34 | points: 3,
35 | ord: 2,
36 | }
37 | }
38 | storyDetails: {
39 | 7: {
40 | id: 7,
41 | category: 'feature',
42 | points: 3,
43 | state: 'started',
44 | requester: { id: 1, username: 'guest'},
45 | owners: [{id: 1, username: 'guest'}],
46 | followers: [],
47 | description: 'description would go here',
48 | updated_at: 10-4-2016,
49 | labels: ['important'],
50 | tasks: {
51 | 3: {
52 | id: 3,
53 | name: 'task',
54 | complete: false,
55 | ord: 1,
56 | }
57 | }
58 | activity: [
59 | {
60 | id: 3,
61 | body: 'activity here',
62 | username: 'guest',
63 | created-at: 'Dec 4 9:59pm'
64 | }
65 | ]
66 | }
67 | }
68 |
69 | members: {
70 | 1: {
71 | id: 1,
72 | username: 'guest',
73 | }
74 | 2: {
75 | id: 2,
76 | username: 'frankye',
77 | }
78 | }
79 |
80 |
81 | }
82 |
83 | ```
84 |
--------------------------------------------------------------------------------
/docs/schema.md:
--------------------------------------------------------------------------------
1 | ## Schema Information
2 |
3 | ### users
4 | | column name | data type | details |
5 | | --- | --- | --- |
6 | | id | integer | not null, primary key |
7 | | username | string | not null, index, unique |
8 | | email | string | not null, index, unique |
9 | | password_digest | string | not null |
10 | | session_token | string | not null, index, unique |
11 |
12 | ### projects
13 | | column name | data type | details |
14 | | --- | --- | --- |
15 | | id | integer | not null, primary key |
16 | | name | string | not null |
17 | | private | boolean | not null |
18 |
19 | ### project_members
20 | | column name | data type | details |
21 | | --- | --- | --- |
22 | | id | integer | not null, primary key |
23 | | project_id | integer | not null, index |
24 | | user_id | integer | not null, index |
25 |
26 | ### pixels
27 | | column name | data type | details |
28 | | --- | --- | --- |
29 | | id | integer | not null, primary key |
30 | | title | string | not null |
31 | | category | string | not null |
32 | | points | integer | not null |
33 | | state | string | not null |
34 | | requester_id | integer | not null, index |
35 | | description | text | |
36 | | project_id | integer | not null, index |
37 | | pixel_ord | integer | not null |
38 |
39 | ### pixel_followers
40 | | column name | data type | details |
41 | | --- | --- | --- |
42 | | id | integer | not null, primary key |
43 | | pixel_id | integer | not null, index |
44 | | user_id | integer | not null, index |
45 |
46 | ### pixel_owners
47 | | column name | data type | details |
48 | | --- | --- | --- |
49 | | id | integer | not null, primary key |
50 | | pixel_id | integer | not null, index |
51 | | user_id | integer | not null, index |
52 |
53 | ### tasks
54 | | column name | data type | details |
55 | | --- | --- | --- |
56 | | id | integer | not null, primary key |
57 | | body | string | not null |
58 | | complete | boolean | not null, default false |
59 | | pixel_id | integer | not null, index |
60 | | task_ord | integer | not null |
61 |
62 | ### labels
63 | | column name | data type | details |
64 | | --- | --- | --- |
65 | | id | integer | not null, primary key |
66 | | name | string | not null |
67 |
68 | ### pixel_labels
69 | | column name | data type | details |
70 | | --- | --- | --- |
71 | | id | integer | not null, primary key |
72 | | pixel_id | integer | not null, index |
73 | | label_id | integer | not null, index |
74 |
75 | ### comments
76 | | column name | data type | details |
77 | | --- | --- | --- |
78 | | id | integer | not null, primary key |
79 | | body | string | not null |
80 | | pixel_id | integer | not null, index |
81 |
--------------------------------------------------------------------------------
/docs/screenshots/screenshot-create-project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/screenshots/screenshot-create-project.png
--------------------------------------------------------------------------------
/docs/screenshots/screenshot-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/screenshots/screenshot-dashboard.png
--------------------------------------------------------------------------------
/docs/screenshots/screenshot-dnd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/screenshots/screenshot-dnd.png
--------------------------------------------------------------------------------
/docs/screenshots/screenshot-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/screenshots/screenshot-form.png
--------------------------------------------------------------------------------
/docs/screenshots/screenshot-sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/screenshots/screenshot-sidebar.png
--------------------------------------------------------------------------------
/docs/wireframes/add-project-member.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/add-project-member.png
--------------------------------------------------------------------------------
/docs/wireframes/create-project-popup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/create-project-popup.png
--------------------------------------------------------------------------------
/docs/wireframes/new-story-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/new-story-page.png
--------------------------------------------------------------------------------
/docs/wireframes/project-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/project-page.png
--------------------------------------------------------------------------------
/docs/wireframes/projects-homepage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/projects-homepage.png
--------------------------------------------------------------------------------
/docs/wireframes/sign-in-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/sign-in-page.png
--------------------------------------------------------------------------------
/docs/wireframes/sign-up-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/docs/wireframes/sign-up-page.png
--------------------------------------------------------------------------------
/frontend/actions/error_actions.js:
--------------------------------------------------------------------------------
1 | export const DRAG_ERROR = 'DRAG_ERROR';
2 | export const CLEAR_ERRORS = 'CLEAR_ERRORS';
3 |
4 | export const dragAndDropError = () => ({ type: DRAG_ERROR });
5 |
6 | export const clearErrors = () => ({ type: CLEAR_ERRORS });
7 |
--------------------------------------------------------------------------------
/frontend/actions/header_actions.js:
--------------------------------------------------------------------------------
1 | export const RECEIVE_NEW_HEADER_TYPE = 'RECEIVE_CURRENT_HEADER_TYPE';
2 |
3 | export const receiveNewHeaderType = (headerType, tooltip = false) => {
4 | return {
5 | type: RECEIVE_NEW_HEADER_TYPE,
6 | headerType,
7 | tooltip,
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/frontend/actions/project_actions.js:
--------------------------------------------------------------------------------
1 | import * as ProjectsAPIUtil from '../util/projects_api_util.js';
2 |
3 | export const RECEIVE_PROJECTS = 'RECEIVE_PROJECTS';
4 | export const RECEIVE_ONE_PROJECT = 'RECEIVE_PROJECT';
5 | export const RECEIVE_PROJECT_ERRORS = 'RECEIVE_PROJECT_ERRORS';
6 | export const LOADING_PROJECTS = 'LOADING_PROJECTS';
7 | export const LOADING_PROJECT_MEMBERS = 'LOADING_PROJECT_MEMBERS';
8 |
9 | export const receiveProjects = (projects) => {
10 | return {
11 | type: RECEIVE_PROJECTS,
12 | projects: projects,
13 | };
14 | };
15 |
16 | export const resetProjects = () => {
17 | return {
18 | type: RECEIVE_PROJECTS,
19 | projects: [],
20 | };
21 | };
22 |
23 | export const receiveOneProject = (project) => {
24 | return {
25 | type: RECEIVE_ONE_PROJECT,
26 | project: project,
27 | };
28 | };
29 |
30 | export const receiveProjectErrors = (errors) => {
31 | return {
32 | type: RECEIVE_PROJECT_ERRORS,
33 | errors: errors,
34 | };
35 | };
36 |
37 | export const resetProjectErrors = () => {
38 | return {
39 | type: RECEIVE_PROJECT_ERRORS,
40 | errors: [],
41 | };
42 | };
43 |
44 | export const loadingProjects = () => {
45 | return { type: LOADING_PROJECTS };
46 | };
47 |
48 | export const loadingProjectMembers = () => {
49 | return { type: LOADING_PROJECT_MEMBERS };
50 | };
51 |
52 | export const fetchProjects = () => {
53 | return (dispatch) => {
54 | dispatch(loadingProjects());
55 | return ProjectsAPIUtil.fetchProjects().then(
56 | projects => dispatch(receiveProjects(projects)),
57 | errors => dispatch(receiveProjectErrors(errors.responseJSON))
58 | );
59 | };
60 | };
61 |
62 | export const fetchOneProject = (projectId) => {
63 | return (dispatch) => {
64 | dispatch(loadingProjects());
65 | return ProjectsAPIUtil.fetchOneProject(projectId).then(
66 | project => dispatch(receiveOneProject(project)),
67 | errors => dispatch(receiveProjectErrors(errors.responseJSON))
68 | );
69 | };
70 | };
71 |
72 | export const createProject = (project) => {
73 | return (dispatch) => {
74 | dispatch(loadingProjects());
75 | return ProjectsAPIUtil.newProject(project).then(
76 | singleProject => dispatch(receiveOneProject(singleProject)),
77 | errors => dispatch(receiveProjectErrors(errors.responseJSON))
78 | );
79 | };
80 | };
81 |
82 | export const createProjectMember = (projectMember) => {
83 | return (dispatch) => {
84 | dispatch(loadingProjectMembers());
85 | return ProjectsAPIUtil.newProjectMember(projectMember).then(
86 | singleProject => dispatch(receiveOneProject(singleProject)),
87 | errors => dispatch(receiveProjectErrors(errors.responseJSON))
88 | );
89 | };
90 | };
91 |
92 | export const deleteProjectMember = (projectMemberId) => {
93 | return (dispatch) => {
94 | dispatch(loadingProjectMembers());
95 | return ProjectsAPIUtil.destroyProjectMember(projectMemberId).then(
96 | singleProject => dispatch(receiveOneProject(singleProject)),
97 | errors => dispatch(receiveProjectErrors(errors.responseJSON))
98 | );
99 | };
100 | };
101 |
--------------------------------------------------------------------------------
/frontend/actions/session_actions.js:
--------------------------------------------------------------------------------
1 | import * as SessionAPIUtil from '../util/session_api_util';
2 | import { resetProjects } from './project_actions';
3 |
4 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
5 | export const RECEIVE_ERRORS = 'RECEIVE_ERRORS';
6 |
7 | export const receiveCurrentUser = (currentUser) => {
8 | return {
9 | type: RECEIVE_CURRENT_USER,
10 | currentUser,
11 | };
12 | };
13 |
14 | export const receiveErrors = (errors) => {
15 | return {
16 | type: RECEIVE_ERRORS,
17 | errors: errors.responseJSON,
18 | };
19 | };
20 |
21 | export const resetErrors = () => {
22 | return {
23 | type: RECEIVE_ERRORS,
24 | errors: [],
25 | };
26 | };
27 |
28 | export const login = (user) => {
29 | return (dispatch) => {
30 | return SessionAPIUtil.login(user).then(
31 | currentUser => dispatch(receiveCurrentUser(currentUser)),
32 | errors => dispatch(receiveErrors(errors))
33 | );
34 | };
35 | };
36 |
37 | export const logout = () => {
38 | return (dispatch) => {
39 | return SessionAPIUtil.logout().then(
40 | user => {
41 | dispatch(resetProjects());
42 | return dispatch(receiveCurrentUser(null));
43 | }
44 | );
45 | };
46 | };
47 |
48 | export const signup = (user) => {
49 | return (dispatch) => {
50 | return SessionAPIUtil.signup(user).then(
51 | currentUser => dispatch(receiveCurrentUser(currentUser)),
52 | errors => dispatch(receiveErrors(errors))
53 | );
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/frontend/actions/sidebar_actions.js:
--------------------------------------------------------------------------------
1 | export const SHOW_COLUMN = 'SHOW_COLUMN';
2 | export const HIDE_COLUMN = 'HIDE_COLUMN';
3 | export const RESET_VIEW = 'RESET_VIEW';
4 |
5 | export const showColumn = (columnName) => {
6 | return {
7 | type: SHOW_COLUMN,
8 | columnName,
9 | };
10 | };
11 |
12 | export const hideColumn = (columnName) => {
13 | return {
14 | type: HIDE_COLUMN,
15 | columnName,
16 | };
17 | };
18 |
19 | export const resetView = () => {
20 | return { type: RESET_VIEW };
21 | };
22 |
--------------------------------------------------------------------------------
/frontend/components/dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DashboardHeaderContainer from './headers/dashboard_header_container';
3 | import ProjectsContainer from './projects/projects_container';
4 |
5 | class Dashboard extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render() {
12 | const state = this.state;
13 | return (
14 |
15 |
16 |
17 |
18 | { this.props.children }
19 |
20 |
21 | );
22 | }
23 | }
24 |
25 | export default Dashboard;
26 |
--------------------------------------------------------------------------------
/frontend/components/headers/dashboard_header_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { logout } from '../../actions/session_actions';
3 | import DashboardHeader from './dashboard_header';
4 | import { selectAllProjects } from '../../reducers/selector';
5 | import { clearErrors } from '../../actions/error_actions';
6 | import { receiveNewHeaderType } from '../../actions/header_actions';
7 |
8 | const mapStateToProps = ( state ) => {
9 | return {
10 | currentUser: state.session.currentUser,
11 | headerType: state.headerInfo.headerType,
12 | tooltip: state.headerInfo.tooltip,
13 | projectList: state.projects.projectList,
14 | projectsAll: selectAllProjects(state),
15 | dragErrors: state.globalErrors.dragErrors,
16 | loading: state.loading,
17 | };
18 | };
19 |
20 | const mapDispatchToProps = (dispatch, ownProps) => {
21 | return {
22 | logout: () => dispatch(logout()),
23 | clearErrors: () => dispatch(clearErrors()),
24 | changeHeader: (headerType, tooltip) => dispatch(receiveNewHeaderType(headerType, tooltip))
25 | };
26 | };
27 |
28 | export default connect(
29 | mapStateToProps,
30 | mapDispatchToProps
31 | )(DashboardHeader);
32 |
--------------------------------------------------------------------------------
/frontend/components/headers/errors_header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class ErrorHeader extends React.Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.state = {reload: false, ignore: false };
8 | this.reloadPage = this.reloadPage.bind(this);
9 | this.ignoreErrors = this.ignoreErrors.bind(this);
10 | }
11 |
12 | reloadPage(e) {
13 | this.setState({ reload: true });
14 | window.location.reload(true);
15 | }
16 |
17 | ignoreErrors() {
18 | this.props.clearErrors();
19 | }
20 |
21 | render() {
22 | return(
23 |
24 | Something went wrong. Your changes were not saved. Please reload the page.
25 |
31 |
37 |
38 | );
39 | }
40 | }
41 |
42 | export default ErrorHeader;
43 |
--------------------------------------------------------------------------------
/frontend/components/headers/main_header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | class MainHeader extends React.Component {
5 |
6 | loggedIn(){
7 | return Boolean(this.props.currentUser);
8 | }
9 |
10 | navItems() {
11 | if (this.loggedIn()) {
12 | return (
13 |
14 |
15 | - Dashboard
16 | - { this.props.currentUser.username }
17 |
18 | );
19 | } else if (this.props.headerType === 'signup' || this.props.headerType === 'login') {
20 | return (
21 |
22 | - {this.linkToOtherSession()}
23 |
24 | );
25 | } else {
26 | return (
27 |
28 | - Sign up
29 | - Log in
30 |
31 | );
32 | }
33 | }
34 |
35 | linkToOtherSession() {
36 | if (this.props.headerType === 'login') {
37 | return (
38 |
39 | don't have an account?Sign Up
40 |
41 | );
42 | } else if (this.props.headerType === 'signup') {
43 | return (
44 |
45 | already have an account?Log In
46 |
47 | );
48 | }
49 | }
50 |
51 | render() {
52 | return (
53 |
54 |
55 |
56 |
57 | Pixel
59 |
62 |
63 |
64 | );
65 | }
66 | }
67 |
68 | export default MainHeader;
69 |
--------------------------------------------------------------------------------
/frontend/components/headers/main_header_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { logout } from '../../actions/session_actions';
3 | import MainHeader from './main_header.jsx';
4 |
5 | const mapStateToProps = ( store ) => {
6 | return {
7 | currentUser: store.session.currentUser,
8 | headerType: store.headerInfo.headerType,
9 | };
10 | };
11 |
12 | const mapDispatchToProps = (dispatch, ownProps) => {
13 | return { logout: () => dispatch(logout()) };
14 | };
15 |
16 | export default connect(
17 | mapStateToProps,
18 | mapDispatchToProps
19 | )(MainHeader);
20 |
--------------------------------------------------------------------------------
/frontend/components/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MainHeaderContainer from './headers/main_header_container';
3 | import SplashPageContainer from './splash_page/splash_page_container';
4 |
5 | const Main = ({ children }) => {
6 | return (
7 |
8 |
9 |
10 |
11 | { children }
12 |
13 |
14 | );
15 | };
16 |
17 | export default Main;
18 |
--------------------------------------------------------------------------------
/frontend/components/pixels/comments.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner2 } from '../spinners/spinners';
3 | import ConfirmModal from './confirm_modal';
4 | import CommentList from './comments_list';
5 |
6 | class Comments extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = { body: "" };
10 |
11 | this.handleChange = this.handleChange.bind(this);
12 | this.handleDelete = this.handleDelete.bind(this);
13 | this.handleSubmit = this.handleSubmit.bind(this);
14 | }
15 |
16 | handleChange(e) {
17 | e.preventDefault();
18 | const newBody = e.currentTarget.value;
19 | this.setState({ body: newBody });
20 | }
21 |
22 | handleDelete(e) {
23 | e.preventDefault();
24 | e.currentTarget.disabled = true;
25 | const idx = parseInt(e.currentTarget.value);
26 | const comment = this.props.pixelList[this.props.pixelId].comments[idx];
27 | if (comment.user === this.props.currentUser) {
28 | this.props.deleteComment(comment.id);
29 | }
30 | }
31 |
32 | handleSubmit(e) {
33 | e.preventDefault();
34 | const newComment = this.state;
35 | newComment.pixel_id = this.props.pixelId;
36 | this.props.createComment(newComment).then(
37 | () => {
38 | this.setState({ body: "" });
39 | }
40 | );
41 | }
42 |
43 | render() {
44 | let className = "";
45 | if (this.props.errors.body) {
46 | className = "errored-item";
47 | }
48 |
49 | if (this.props.loading) {
50 | return (
51 |
54 | );
55 | }
56 | return (
57 |
58 | Activity
59 |
62 |
70 |
71 | );
72 | }
73 | }
74 |
75 | export default Comments;
76 |
--------------------------------------------------------------------------------
/frontend/components/pixels/comments_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Comments from './comments';
3 | import {
4 | createComment,
5 | updateComment,
6 | deleteComment,
7 | } from '../../actions/pixel_actions';
8 |
9 | const mapStateToProps = (state) => {
10 | return {
11 | pixelList: state.pixels.pixelList,
12 | errors: state.pixels.errors,
13 | loading: state.loading.commentsLoading,
14 | currentUser: state.session.currentUser.username,
15 | };
16 | };
17 |
18 | const mapDispatchToProps = (dispatch) => {
19 |
20 | return {
21 | createComment: (comment) => dispatch(createComment(comment)),
22 | updateComment: (comment) => dispatch(updateComment(comment)),
23 | deleteComment: (commentId) => dispatch(deleteComment(commentId)),
24 | };
25 | };
26 |
27 | export default connect(mapStateToProps, mapDispatchToProps)(Comments);
28 |
--------------------------------------------------------------------------------
/frontend/components/pixels/comments_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ConfirmModal from './confirm_modal';
3 |
4 | const CommentList = (props) => {
5 | const buttonContent = delete_forever;
6 | const mapped = props.comments.map((comment, idx) => {
7 | return (
8 |
9 | {comment.user}
10 | {comment.created_at}
11 | {comment.body}
12 |
13 |
20 |
21 |
22 | );
23 | });
24 | return (
25 |
28 | );
29 | };
30 |
31 | export default CommentList;
32 |
--------------------------------------------------------------------------------
/frontend/components/pixels/confirm_modal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Modal from 'react-modal';
3 |
4 | //buttonClass, buttonContent, message, callback, buttonActive: required props
5 | //buttonValue: optional prop
6 |
7 | class ConfirmModal extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = { confirmOpen: false };
11 |
12 | this.closeForm = this.closeForm.bind(this);
13 | this.openForm = this.openForm.bind(this);
14 | this.handleClick = this.handleClick.bind(this);
15 | }
16 |
17 | closeForm() {
18 | this.setState({ confirmOpen: false });
19 | }
20 |
21 | openForm() {
22 | if(this.props.buttonActive)
23 | this.setState({ confirmOpen: true });
24 | }
25 |
26 | handleClick(e) {
27 | if(this.props.buttonActive) {
28 | e.preventDefault();
29 | this.props.callback(e);
30 | this.closeForm();
31 | }
32 | }
33 |
34 | formStyling() {
35 | return {
36 | overlay : {
37 | position: 'fixed',
38 | top: 0,
39 | left: 0,
40 | bottom: 0,
41 | right: 0,
42 | background: 'rgba(255, 255, 255, 0.75)',
43 | zIndex: 10,
44 | },
45 | content : {
46 | position: 'fixed',
47 | top: '15%',
48 | left: '50%',
49 | transform: 'translateX(-50%)',
50 | padding: '0',
51 | border: '0',
52 | borderRadius: '3px',
53 | background: '#F6F6F6',
54 | zIndex: 11,
55 | width: '260px',
56 | height: '160px',
57 | boxShadow: '1px 1px 4px #333',
58 | overflow: 'hidden',
59 | },
60 | };
61 | }
62 |
63 | render() {
64 | let buttonValue = "";
65 | if (this.props.buttonValue !== undefined) {
66 | buttonValue = this.props.buttonValue;
67 | }
68 |
69 | return (
70 |
89 | );
90 | }
91 | }
92 |
93 | export default ConfirmModal;
94 |
--------------------------------------------------------------------------------
/frontend/components/pixels/drop_area.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropTarget } from 'react-dnd';
3 | import { ItemTypes } from '../../modules/dnd_item_types';
4 |
5 | const pixelTarget = {
6 | drop(props, monitor, component) {
7 | const pixel = monitor.getItem();
8 |
9 | if (pixel.icebox) {
10 | const newPixel = {
11 | id: pixel.id,
12 | pixel_ord:(props.ords.maxUnstarted + 1),
13 | icebox: false
14 | };
15 | pixel.startLoading();
16 | props.updatePixel(
17 | pixel.pixelId,
18 | newPixel
19 | ).then(
20 | () => pixel.finishLoading(),
21 | () => {
22 | pixel.finishLoading();
23 | props.dragError();
24 | }
25 | );
26 | } else if (pixel.pixelState === 'Unstarted') {
27 | const newPixel = {
28 | id: pixel.id,
29 | pixel_ord:(props.ords.maxIcebox + 1),
30 | icebox: true,
31 | };
32 | pixel.startLoading();
33 | props.updatePixel(
34 | pixel.pixelId,
35 | newPixel
36 | ).then(
37 | () => pixel.finishLoading(),
38 | () => {
39 | pixel.finishLoading();
40 | props.dragError();
41 | }
42 | );
43 | }
44 | },
45 | canDrop(props, monitor, component) {
46 | const item = monitor.getItem();
47 | if (props.areaType === "Current/Backlog") {
48 | return item.icebox === true;
49 | }
50 | if (props.areaType === "Icebox") {
51 | return (!item.icebox && item.pixelState === 'Unstarted');
52 | }
53 | return false;
54 | }
55 | };
56 |
57 | function collect(connect, monitor) {
58 | return {
59 | connectDropTarget: connect.dropTarget(),
60 | isOver: monitor.isOver(),
61 | canDrop: monitor.canDrop(),
62 | };
63 | }
64 |
65 | class DropArea extends React.Component {
66 |
67 | render() {
68 | if (this.props.areaType === 'Done') {
69 | return null;
70 | }
71 |
72 | let areaClass = 'drop-area';
73 | if (this.props.canDrop && this.props.isOver) {
74 | areaClass += ' hovered-container';
75 | }
76 |
77 | return this.props.connectDropTarget(
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | export default DropTarget(ItemTypes.PIXEL, pixelTarget, collect)(DropArea);
85 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_form_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import PixelForm from './pixel_form';
3 | import {
4 | createPixel,
5 | updatePixel,
6 | removePixel,
7 | resetPixelErrors,
8 | createTask,
9 | receiveTasks,
10 | loadingSinglePixel,
11 | finishLoadingSinglePixel,
12 | } from '../../actions/pixel_actions';
13 | import { hideColumn } from '../../actions/sidebar_actions';
14 | import { dragAndDropError } from '../../actions/error_actions';
15 |
16 |
17 | const mapStateToProps = (state) => {
18 | return {
19 | errors: state.pixels.errors,
20 | loading: state.loading.individualPixelsLoading,
21 | tasks: state.pixels.tasks,
22 | recentPixelId: state.pixels.recentPixelId,
23 | ords: state.pixels.ords,
24 | };
25 | };
26 |
27 | const mapDispatchToProps = (dispatch) => {
28 | return {
29 | createPixel: (projectId, pixel) => dispatch(createPixel(projectId, pixel)),
30 | updatePixel: (pixelId, pixel) => dispatch(updatePixel(pixelId, pixel)),
31 | removePixel: (pixelId) => dispatch(removePixel(pixelId)),
32 | resetPixelErrors: () => dispatch(resetPixelErrors()),
33 | hideForm:() => dispatch(hideColumn('newPixel')),
34 | createTask: (task) => dispatch(createTask(task)),
35 | receiveTasks: (tasks) => dispatch(receiveTasks(tasks)),
36 | startLoading: (pixelId) => dispatch(loadingSinglePixel(pixelId)),
37 | finishLoading: (pixelId) => dispatch(finishLoadingSinglePixel(pixelId)),
38 | dragError: () => dispatch(dragAndDropError()),
39 | };
40 | };
41 |
42 | export default connect(mapStateToProps, mapDispatchToProps)(PixelForm);
43 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_form_helper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const StateButton = (props) => {
4 | if (props.name === 'none' || props.id === "") {
5 | return null;
6 | }
7 | const className = `next-state-button drop-down-button ${props.name}-button`;
8 | return (
9 |
10 | {props.name}
11 |
12 | );
13 | };
14 |
15 | export const OpenButton = (props) => {
16 | if (props.id === '') {
17 | return null;
18 | }
19 | return (
20 |
21 | keyboard_arrow_down
22 |
23 | );
24 | };
25 |
26 | export const LoadingSpinner = (props) => {
27 | if (props.id !== "" && props.loading[props.id]) {
28 | return ;
29 | }
30 | if (props.id === "" && props.loading['new']){
31 | return ;
32 | }
33 | return null;
34 | };
35 |
36 | export const RejectButton = (props) => {
37 | if (props.state === 'Delivered'){
38 | return (
39 | Reject
42 |
43 | );
44 | }
45 | return null;
46 | };
47 |
48 | export const Comments = (props) => {
49 | if (props.id === "") {
50 | return null;
51 | }
52 | return (
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PixelPanelContainer from './pixel_panel_container';
3 | import { DragDropContext } from 'react-dnd';
4 | import HTML5Backend from 'react-dnd-html5-backend';
5 |
6 | class PixelList extends React.Component {
7 | componentDidMount() {
8 | let maxIcebox = 0;
9 | let maxBacklog = 0;
10 | let maxDone = 0;
11 | let maxUnstarted = 0;
12 | this.props.pixels.forEach((pixel)=> {
13 | if (pixel.icebox) {
14 | maxIcebox = pixel.pixel_ord;
15 | } else if (!pixel.icebox && pixel.state !== 'Accepted') {
16 | if (pixel.state === 'Unstarted') {
17 | maxUnstarted = pixel.pixel_ord;
18 | } else {
19 | maxBacklog = pixel.pixel_ord;
20 | }
21 | maxBacklog = pixel.pixel_ord;
22 | } else if (pixel.state === 'Accepted') {
23 | maxDone = pixel.pixel_ord;
24 | }
25 | });
26 | this.props.updateMaxOrds(maxIcebox, maxBacklog, maxDone, maxUnstarted);
27 | }
28 |
29 | componentWillReceiveProps(nextProps) {
30 | if (nextProps.pixelList !== this.props.pixelList) {
31 | let maxIcebox = 0;
32 | let maxBacklog = 0;
33 | let maxDone = 0;
34 | let maxUnstarted = 0;
35 | nextProps.pixels.forEach((pixel)=> {
36 | if (pixel.icebox) {
37 | maxIcebox = pixel.pixel_ord;
38 | } else if (!pixel.icebox && pixel.state !== 'Accepted') {
39 | if (pixel.state === 'Unstarted') {
40 | maxUnstarted = pixel.pixel_ord;
41 | } else {
42 | maxBacklog = pixel.pixel_ord;
43 | }
44 | maxBacklog = pixel.pixel_ord;
45 | } else if (pixel.state === 'Accepted') {
46 | maxDone = pixel.pixel_ord;
47 | }
48 | });
49 | this.props.updateMaxOrds(maxIcebox, maxBacklog, maxDone, maxUnstarted);
50 | }
51 | }
52 |
53 | render() {
54 | const panels = [];
55 | if ( this.props.sidebar.done ) {
56 | panels.push(
57 |
61 | );
62 | }
63 | if (this.props.sidebar.current) {
64 | panels.push(
65 |
70 | );
71 | }
72 | if (this.props.sidebar.icebox) {
73 | panels.push(
74 |
79 | );
80 | }
81 |
82 | return (
83 |
86 | );
87 | }
88 | }
89 |
90 | export default DragDropContext(HTML5Backend)(PixelList);
91 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_list_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import PixelList from './pixel_list';
3 | import { fetchPixels, updateMaxOrds } from '../../actions/pixel_actions';
4 | import { selectAllPixels } from '../../reducers/selector';
5 |
6 |
7 | const mapStateToProps = (state) => {
8 | return {
9 | pixelList: state.pixels.pixelList,
10 | errors: state.pixels.errors,
11 | sidebar: state.sidebar,
12 | pixels: selectAllPixels(state),
13 | };
14 | };
15 |
16 | const mapDispatchToProps = (dispatch) => {
17 |
18 | return {
19 | fetchPixels: (projectId) => dispatch(fetchPixels(projectId)),
20 | updateMaxOrds: (maxIcebox, maxBacklog, maxDone, maxUnstarted) => dispatch(updateMaxOrds(maxIcebox, maxBacklog, maxDone, maxUnstarted)),
21 | };
22 | };
23 |
24 | export default connect(mapStateToProps, mapDispatchToProps)(PixelList);
25 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_list_drop_area.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { DropTarget } from 'react-dnd';
3 | import { ItemTypes } from '../../modules/dnd_item_types';
4 |
5 | const pixelTarget = {
6 | drop(props, monitor, component) {
7 | const item = monitor.getItem();
8 | if (item.pixelId === props.pixelId) {
9 | return;
10 | }
11 |
12 | function updateOrder(icebox, callback) {
13 | const pixels = [];
14 | let index = 1;
15 | let doneIndex = 1;
16 | props.allPixels.forEach((pixel) => {
17 | if (callback(pixel)) {
18 | if (pixel.id === props.pixelId) {
19 | const currentPixel = {};
20 | currentPixel.id = item.pixelId;
21 | currentPixel.icebox = icebox;
22 | currentPixel.pixel_ord = index;
23 | index += 1;
24 | pixels.push(currentPixel);
25 | }
26 | const newPixel = {};
27 | newPixel.id = pixel.id;
28 | newPixel.icebox = icebox;
29 | newPixel.pixel_ord = index;
30 | index += 1;
31 | pixels.push(newPixel);
32 | }
33 | if (pixel.state === 'Accepted') {
34 | const newPixel = {};
35 | newPixel.id = pixel.id;
36 | newPixel.icebox = false;
37 | newPixel.pixel_ord = doneIndex;
38 | doneIndex += 1;
39 | pixels.push(newPixel);
40 | }
41 | });
42 | props.updateMassPixels(pixels);
43 | item.startLoading();
44 | const newPixels = {};
45 | pixels.forEach((pixel) => {
46 | newPixels[pixel.id] = { icebox: icebox, pixel_ord: pixel.pixel_ord };
47 | });
48 | props.massUpdatePixel(newPixels).then(
49 | () => item.finishLoading(),
50 | () => {
51 | item.finishLoading();
52 | props.dragError();
53 | }
54 |
55 | );
56 | }
57 |
58 | if (props.icebox) {
59 | const callback = (pixel) => {
60 | return (pixel.icebox && pixel.id !== item.pixelId);
61 | };
62 | updateOrder(true, callback);
63 | }
64 |
65 | if (props.pixelState === 'Unstarted') {
66 | const callback = (pixel) => {
67 | return (!pixel.icebox && pixel.state === 'Unstarted' && pixel.id !== item.pixelId);
68 | };
69 | updateOrder(false, callback);
70 | }
71 |
72 | if (props.pixelState !== 'Accepted') {
73 | const callback = (pixel) => {
74 | return (pixel.state !== 'Unstarted' && pixel.state !== 'Accepted' && pixel.id !== item.pixelId);
75 | };
76 | updateOrder(false, callback);
77 | }
78 |
79 | },
80 | canDrop(props, monitor, component) {
81 | const item = monitor.getItem();
82 | if (item.pixelState === props.pixelState) {
83 | return true;
84 | }
85 | if (item.pixelState !== 'Unstarted' && props.pixelState !== 'Unstarted') {
86 | return true;
87 | }
88 | return false;
89 | }
90 | };
91 |
92 | function collect(connect, monitor) {
93 | return {
94 | connectDropTarget: connect.dropTarget(),
95 | isOver: monitor.isOver(),
96 | canDrop: monitor.canDrop(),
97 | item: monitor.getItem(),
98 | };
99 | }
100 |
101 | class PixelListItemDrop extends React.Component {
102 |
103 | render() {
104 | let areaClass = 'drop-area-list-item';
105 | if (this.props.isOver && this.props.canDrop) {
106 | areaClass += ' hovered-list-item';
107 | }
108 |
109 | return this.props.connectDropTarget(
110 |
111 | {this.props.children}
112 |
113 | );
114 | }
115 | }
116 |
117 | export default DropTarget(
118 | ItemTypes.PIXEL,
119 | pixelTarget, collect
120 | )(PixelListItemDrop);
121 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_list_drop_area_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import PixelListDropArea from './pixel_list_drop_area';
3 | import {
4 | massUpdatePixels,
5 | updateMassPixels,
6 | } from '../../actions/pixel_actions';
7 | import { selectAllPixels } from '../../reducers/selector';
8 | import { dragAndDropError } from '../../actions/error_actions';
9 |
10 | const mapStateToProps = (state) => {
11 | return {
12 | errors: state.pixels.errors,
13 | ords: state.pixels.ords,
14 | allPixels: selectAllPixels(state),
15 | };
16 | };
17 |
18 | const mapDispatchToProps = (dispatch, ownProps) => {
19 | return {
20 | massUpdatePixel: (pixels) => dispatch(massUpdatePixels(pixels)),
21 | updateMassPixels: (pixels) => dispatch(updateMassPixels(pixels)),
22 | dragError: () => dispatch(dragAndDropError()),
23 | };
24 | };
25 |
26 | export default connect(mapStateToProps, mapDispatchToProps)(PixelListDropArea);
27 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_list_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PixelFormContainer from './pixel_form_container';
3 | import { newPixelState } from '../../util/pixel_state_util.js';
4 | import { DragSource } from 'react-dnd';
5 | import { ItemTypes } from '../../modules/dnd_item_types';
6 | import PixelListItemDropContainer from './pixel_list_drop_area_container';
7 | import PixelSummary from './pixel_summary';
8 |
9 | const pixelSource = {
10 | beginDrag(props, monitor, component) {
11 | return {
12 | pixelId: props.pixelId,
13 | pixelOrd: props.pixelList[props.pixelId].pixel_ord,
14 | icebox: props.pixelList[props.pixelId].icebox,
15 | pixelState: props.pixelList[props.pixelId].state,
16 | startLoading: props.startLoading,
17 | finishLoading: props.finishLoading,
18 | };
19 | },
20 | canDrag(props) {
21 | return props.pixelList[props.pixelId].state !== 'Accepted';
22 | },
23 | };
24 |
25 | function collect(connect, monitor) {
26 | return {
27 | connectDragSource: connect.dragSource(),
28 | connectDragPreview: connect.dragPreview(),
29 | isDragging: monitor.isDragging()
30 | };
31 | }
32 |
33 | class PixelListItem extends React.Component {
34 | constructor(props) {
35 | super(props);
36 | this.state = { opened: false, hover: false };
37 | this.handleClick = this.handleClick.bind(this);
38 | this.handleUpdateState = this.handleUpdateState.bind(this);
39 | }
40 |
41 |
42 | handleClick(e) {
43 | if (this.state.opened) {
44 | this.setState({ opened: false });
45 | } else {
46 | this.setState({ opened: true });
47 | }
48 | }
49 |
50 | handleUpdateState(pixel, newState = null) {
51 | return (e) => {
52 | const pixelId = pixel.id;
53 | const curState = pixel.state;
54 | if (newState === null) {
55 | newState = newPixelState(curState);
56 | }
57 | const nextState = newState;
58 | let newOrd = pixel.pixel_ord;
59 | if (pixel.icebox) {
60 | newOrd = this.props.ords.maxBacklog + 1;
61 | }
62 | if (curState === 'Unstarted' && nextState !== 'Unstarted') {
63 | newOrd = this.props.ords.maxBacklog + 1;
64 | }
65 | if (nextState === 'Accepted') {
66 | newOrd = this.props.ords.maxDone + 1;
67 | }
68 | if (nextState === 'Unstarted') {
69 | newOrd = this.props.ords.maxUnstarted + 1;
70 | }
71 | this.props.startLoading();
72 | this.props.updatePixel(pixelId, { state: nextState, icebox: false, pixel_ord: newOrd })
73 | .then(
74 | () => this.props.finishLoading(),
75 | () => {
76 | this.props.dragError();
77 | this.props.finishLoading();
78 | }
79 | );
80 | };
81 | }
82 |
83 | render() {
84 | const pixel = this.props.pixelList[this.props.pixelId];
85 | if(!pixel) return null;
86 | const { connectDragSource, isDragging } = this.props;
87 | if(this.state.opened) {
88 | return (
89 |
95 | );
96 | }
97 | if (pixel.state !== 'Accepted') {
98 | return connectDragSource(
99 |
111 | );
112 | }
113 | return connectDragSource(
114 |
121 | );
122 | }
123 | }
124 |
125 | export default DragSource(ItemTypes.PIXEL, pixelSource, collect)(PixelListItem);
126 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_list_item_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import PixelListItem from './pixel_list_item';
3 | import {
4 | updatePixel,
5 | loadingSinglePixel,
6 | finishLoadingSinglePixel,
7 | } from '../../actions/pixel_actions';
8 | import { dragAndDropError } from '../../actions/error_actions';
9 |
10 | const mapStateToProps = (state) => {
11 | return {
12 | pixelList: state.pixels.pixelList,
13 | errors: state.pixels.errors,
14 | loading: state.loading.individualPixelsLoading,
15 | ords: state.pixels.ords,
16 | };
17 | };
18 |
19 | const mapDispatchToProps = (dispatch, ownProps) => {
20 | const loadingPixelId = ownProps.pixelId;
21 | return {
22 | updatePixel: (pixelId, pixel) => dispatch(updatePixel(pixelId, pixel)),
23 | startLoading: () => dispatch(loadingSinglePixel(loadingPixelId)),
24 | finishLoading: () => dispatch(finishLoadingSinglePixel(loadingPixelId)),
25 | dragError: () => dispatch(dragAndDropError()),
26 | };
27 | };
28 |
29 | export default connect(mapStateToProps, mapDispatchToProps)(PixelListItem);
30 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_panel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PixelListItemContainer from './pixel_list_item_container';
3 | import PixelFormContainer from './pixel_form_container';
4 | import { Spinner } from '../spinners/spinners';
5 | import DropArea from './drop_area';
6 |
7 | class PixelPanel extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.listLength = 0;
11 | this.handleClick = this.handleClick.bind(this);
12 | }
13 |
14 | pixelSummary() {
15 | let mapPixels = [];
16 | const unStartedPixels = [];
17 | const startedPixels = [];
18 | let maxOrd = 0;
19 | this.props.pixels.forEach((pixel)=> {
20 | if (this.props.panelName === 'Icebox'){
21 | if (pixel.icebox) {
22 | mapPixels.push();
23 | maxOrd = pixel.pixel_ord;
24 | }
25 | } else if (this.props.panelName === 'Current/Backlog') {
26 | if ( !pixel.icebox && pixel.state !== 'Accepted' ) {
27 | if (pixel.state === 'Unstarted') {
28 | unStartedPixels.push();
29 | } else {
30 | startedPixels.push();
31 | }
32 | }
33 | } else if (this.props.panelName === 'Done') {
34 | if (pixel.state === 'Accepted'){
35 | mapPixels.push();
36 | }
37 | }
38 | });
39 | if (unStartedPixels.length > 0 || startedPixels.length > 0) {
40 | mapPixels = startedPixels.concat(unStartedPixels);
41 | }
42 | this.listLength = maxOrd;
43 | return (
44 |
47 | );
48 | }
49 |
50 | handleClick(e) {
51 | this.props.hideColumn(this.props.sidebarName);
52 | }
53 |
54 | renderPixelForm() {
55 | if (this.props.panelName === 'Icebox' && this.props.sidebar.newPixel) {
56 | return (
57 |
61 | );
62 | } else {
63 | return null;
64 | }
65 | }
66 |
67 | render() {
68 | const dropArea = (
69 |
75 | );
76 | const id = this.props.panelName.substring(0,3);
77 | return (
78 |
79 |
80 |
81 | { this.props.panelName }
82 |
83 | {this.renderPixelForm()}
84 |
85 | {this.pixelSummary()}
86 |
87 | { dropArea }
88 |
89 | );
90 | }
91 | }
92 |
93 | export default PixelPanel;
94 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_panel_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import PixelPanel from './pixel_panel';
3 | import { selectAllPixels } from '../../reducers/selector';
4 | import { hideColumn } from '../../actions/sidebar_actions';
5 | import { updatePixel } from '../../actions/pixel_actions';
6 | import { dragAndDropError } from '../../actions/error_actions';
7 |
8 | const mapStateToProps = (state) => {
9 | return {
10 | pixelList: state.pixels.pixelList,
11 | errors: state.pixels.errors,
12 | loading: state.loading.pixelsLoading,
13 | pixels: selectAllPixels(state),
14 | sidebar: state.sidebar,
15 | ords: state.pixels.ords,
16 | };
17 | };
18 |
19 | const mapDispatchToProps = (dispatch) => {
20 | return {
21 | hideColumn: (columnName) => dispatch(hideColumn(columnName)),
22 | updatePixel: (pixelId, pixel) => dispatch(updatePixel(pixelId, pixel)),
23 | dragError: () => dispatch(dragAndDropError()),
24 | };
25 | };
26 |
27 | export default connect(mapStateToProps, mapDispatchToProps)(PixelPanel);
28 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_summary.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { buttonName } from '../../util/pixel_state_util.js';
3 | import {
4 | Category,
5 | SummaryComments,
6 | SummaryTasks,
7 | StateButton,
8 | RejectButton,
9 | } from './pixel_summary_helper';
10 |
11 |
12 | const PixelSummary = (props) => {
13 | let className = `pixel-list-item-summary ${props.pixel.state}-item`;
14 | if (props.pixel.icebox) className += ' icebox-pixel';
15 | if (props.isDragging) className += ' dragging';
16 | if (props.pixel.category === 'Release') className += ' release-pixel';
17 |
18 | return(
19 |
20 |
21 |
22 | keyboard_arrow_right
23 |
24 |
27 | {props.pixel.points}
28 | chat_bubble_outline
29 |
30 | {props.pixel.title}
31 |
32 |
DESCRIPTION
33 |
{props.pixel.description}
34 |
35 | ACTIVITY
36 |
37 | TASKS
38 |
39 |
47 |
48 | {props.pixel.title}
49 |
53 |
56 |
57 | );
58 | };
59 |
60 | export default PixelSummary;
61 |
--------------------------------------------------------------------------------
/frontend/components/pixels/pixel_summary_helper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner5 } from '../spinners/spinners';
3 |
4 | export const Category = (props) => {
5 | if (props.loading) {
6 | return ();
7 | }
8 | if (props.category === "Feature") {
9 | return (star_rate);
10 | }
11 | if (props.category === "Bug") {
12 | return (bug_report);
13 | }
14 | if (props.category === "Chore") {
15 | return (build);
16 | }
17 | if (props.category === "Release") {
18 | return (flag);
19 | }
20 | };
21 |
22 | export const SummaryComments = (props) => {
23 | const mapComments = props.comments.map((comment, idx) => {
24 | return (
25 |
26 | {comment.user}
27 | {comment.created_at}
28 | {comment.body}
29 |
30 | );
31 | });
32 | return (
33 |
36 | );
37 | };
38 |
39 | export const SummaryTasks = (props) => {
40 | const mapTasks = props.tasks.map((task, idx) => {
41 | let taskClass = "";
42 | if (task.complete) taskClass = "task-complete";
43 | return (
44 |
45 | {idx + 1}. {task.body}
46 |
47 | );
48 | });
49 | return(
50 |
51 | {mapTasks}
52 |
53 | );
54 | };
55 |
56 | export const StateButton = (props) => {
57 | if (props.name === 'none') {
58 | return null;
59 | }
60 | const className = `next-state-button ${props.name}-button`;
61 | return (
62 |
63 | {props.name}
64 |
65 | );
66 | };
67 |
68 | export const RejectButton = (props) => {
69 | if (props.pixel.state === 'Delivered') {
70 | return (
71 | Reject
74 | );
75 | }
76 | return null;
77 | };
78 |
--------------------------------------------------------------------------------
/frontend/components/pixels/task_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spinner3 } from '../spinners/spinners';
3 |
4 | const TaskForm = (props) => {
5 | if (props.loading) {
6 | return (
7 |
10 | );
11 | }
12 | return (
13 |
25 | );
26 | };
27 |
28 | export default TaskForm;
29 |
--------------------------------------------------------------------------------
/frontend/components/pixels/task_list_checkbox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CheckBox = (props) => {
4 |
5 | if (props.active) {
6 | return (
7 |
12 | );
13 | } else {
14 | return (
15 |
19 | );
20 | }
21 |
22 | };
23 |
24 | export default CheckBox;
25 |
--------------------------------------------------------------------------------
/frontend/components/pixels/task_list_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CheckBox from './task_list_checkbox';
3 |
4 | const TaskForm = (props) => {
5 | return(
6 |
24 | );
25 | };
26 |
27 | export default TaskForm;
28 |
--------------------------------------------------------------------------------
/frontend/components/pixels/task_list_item_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { updateTask, deleteTask } from '../../actions/pixel_actions';
3 | import TaskListItem from './task_list_item';
4 |
5 | const mapStateToProps = (state) => {
6 | return {
7 | errors: state.pixels.errors,
8 | loading: state.loading.tasksLoading,
9 | };
10 | };
11 |
12 | const mapDispatchToProps = (dispatch) => {
13 | return {
14 | updateTask: (task) => dispatch(updateTask(task)),
15 | deleteTask: (taskId) => dispatch(deleteTask(taskId)),
16 | };
17 | };
18 |
19 | export default connect(mapStateToProps, mapDispatchToProps)(TaskListItem);
20 |
--------------------------------------------------------------------------------
/frontend/components/pixels/tasks_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Tasks from './tasks';
3 | import {
4 | createTask,
5 | receiveTasks,
6 | } from '../../actions/pixel_actions';
7 |
8 | const mapStateToProps = (state) => {
9 | return {
10 | pixelList: state.pixels.pixelList,
11 | errors: state.pixels.errors,
12 | loading: state.loading.tasksLoading,
13 | };
14 | };
15 |
16 | const mapDispatchToProps = (dispatch) => {
17 |
18 | return {
19 | createTask: (task) => dispatch(createTask(task)),
20 | receiveTasks: (tasks) => dispatch(receiveTasks(tasks)),
21 | };
22 | };
23 |
24 | export default connect(mapStateToProps, mapDispatchToProps)(Tasks);
25 |
--------------------------------------------------------------------------------
/frontend/components/project_detail/filter_list_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const FilterListItem = (props) => {
4 | const spanClassName = `sidebar-icon ${props.filterName}-icon`;
5 | let liClassName = `${props.filterName}-item`;
6 | if (props.sidebar) liClassName += ' selected-filter';
7 | return (
8 |
12 |
13 | {props.filterContent}
14 |
15 | );
16 | };
17 |
18 | export default FilterListItem;
19 |
--------------------------------------------------------------------------------
/frontend/components/project_detail/private_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PrivatePage = () => {
4 | return (
5 |
6 | Sorry, we couldn't find that project
7 | It may be private, or you haven't been added as a member
8 |
9 | );
10 | };
11 |
12 | export default PrivatePage;
13 |
--------------------------------------------------------------------------------
/frontend/components/project_detail/project_detail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DashBoardHeaderContainer from '../headers/dashboard_header_container';
3 | import SidebarContainer from './sidebar_container';
4 | import { RainbowSpinner } from '../spinners/spinners';
5 | import PrivatePage from './private_page';
6 | import PixelListContainer from '../pixels/pixel_list_container';
7 |
8 | class ProjectDetail extends React.Component {
9 | componentDidMount() {
10 | this.props.changeHeader('project detail');
11 | this.props.fetchPixels(this.props.params.projectId);
12 | if (!this.props.projectsList[this.props.params.projectId])
13 | this.props.fetchProject(this.props.params.projectId);
14 | }
15 |
16 | componentWillReceiveProps(nextProps) {
17 | if (this.props.params.projectId !== nextProps.params.projectId) {
18 | this.props.resetProjectErrors();
19 | this.props.resetPixels();
20 | this.props.fetchPixels(nextProps.params.projectId);
21 | if (!this.props.projectsList[nextProps.params.projectId])
22 | this.props.fetchProject(nextProps.params.projectId);
23 | }
24 | }
25 |
26 | componentWillUnmount() {
27 | this.props.resetProjectErrors();
28 | this.props.resetPixels();
29 | this.props.changeHeader('default');
30 | }
31 |
32 | renderStories() {
33 | if (this.props.loading || this.props.pixelsLoading) {
34 | return (
35 |
36 | );
37 | }
38 | if (this.props.errors) {
39 | if (this.props.errors[0] === 'Project Not Found or Private') {
40 | return ();
41 | }
42 | }
43 | return (
44 |
48 | );
49 | }
50 |
51 | render() {
52 | return (
53 |
54 |
55 | {this.renderStories()}
56 |
57 | );
58 | }
59 | }
60 |
61 | export default ProjectDetail;
62 |
--------------------------------------------------------------------------------
/frontend/components/project_detail/project_detail_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { selectAllProjects } from '../../reducers/selector';
3 | import { fetchOneProject, resetProjectErrors } from '../../actions/project_actions';
4 | import { fetchPixels } from '../../actions/pixel_actions';
5 | import { receiveNewHeaderType } from '../../actions/header_actions';
6 | import { resetPixels } from '../../actions/pixel_actions';
7 | import ProjectDetail from './project_detail';
8 |
9 | const mapStateToProps = (state) => {
10 | return {
11 | projectsList: state.projects.projectList,
12 | projectsAll: selectAllProjects(state),
13 | errors: state.projects.errors,
14 | loading: state.loading.projectsLoading,
15 | pixelsLoading: state.loading.pixelsLoading,
16 | };
17 | };
18 |
19 | const mapDispatchToProps = (dispatch, ownProps) => {
20 |
21 | return {
22 | fetchProject: (projectId) => dispatch(fetchOneProject(projectId)),
23 | changeHeader: (headerType) => dispatch(receiveNewHeaderType(headerType)),
24 | resetProjectErrors: () => dispatch(resetProjectErrors()),
25 | fetchPixels: (projectId) => dispatch(fetchPixels(projectId)),
26 | resetPixels: () => dispatch(resetPixels()),
27 | };
28 | };
29 |
30 | export default connect(mapStateToProps, mapDispatchToProps)(ProjectDetail);
31 |
--------------------------------------------------------------------------------
/frontend/components/project_detail/sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MembersContainer from '../projects/members_container';
3 | import FilterListItem from './filter_list_item';
4 |
5 | class Sidebar extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = { shrink: false };
10 | this.handleClick = this.handleClick.bind(this);
11 | this.handleFilter = this.handleFilter.bind(this);
12 | }
13 |
14 | componentDidMount() {
15 | this.props.resetView();
16 | }
17 |
18 | componentWillUnmount() {
19 | this.props.resetView();
20 | }
21 |
22 | handleClick(e) {
23 | if (this.state.shrink) {
24 | this.setState({ shrink: false });
25 | } else {
26 | this.setState({ shrink: true });
27 | }
28 | }
29 |
30 | handleFilter(filterName) {
31 | return (e) => {
32 | const columnName = filterName;
33 | if (filterName === 'newPixel') {
34 | this.props.showColumn('icebox');
35 | }
36 | if (this.props.sidebar[columnName]) {
37 | this.props.hideColumn(columnName);
38 | } else {
39 | this.props.showColumn(columnName);
40 | }
41 | };
42 | }
43 |
44 | countMemberNumbers() {
45 | let projectMembers = 0;
46 | const curProject = this.props.projectList[this.props.projectId];
47 | if (curProject) {
48 | projectMembers += curProject.members.length;
49 | }
50 | return projectMembers;
51 | }
52 |
53 | render() {
54 | let sideBarClass = 'side-bar';
55 | if(this.state.shrink) {
56 | sideBarClass += ' shrink';
57 | }
58 | return(
59 |
92 | );
93 | }
94 | }
95 |
96 | export default Sidebar;
97 |
--------------------------------------------------------------------------------
/frontend/components/project_detail/sidebar_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { showColumn, hideColumn, resetView }
3 | from '../../actions/sidebar_actions';
4 | import Sidebar from './sidebar';
5 |
6 | const mapStateToProps = (state) => {
7 | return {
8 | projectList: state.projects.projectList,
9 | sidebar: state.sidebar,
10 | };
11 | };
12 |
13 | const mapDispatchToProps = (dispatch) => {
14 | return {
15 | showColumn: (columnName) => dispatch(showColumn(columnName)),
16 | hideColumn: (columnName) => dispatch(hideColumn(columnName)),
17 | resetView: () => dispatch(resetView()),
18 | };
19 | };
20 |
21 | export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);
22 |
--------------------------------------------------------------------------------
/frontend/components/projects/member_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RainbowSpinner } from '../spinners/spinners';
3 | import ConfirmModal from '../pixels/confirm_modal';
4 |
5 | const MemberList = (props) => {
6 | if (props.loading) {
7 | return (
8 |
9 |
10 |
11 | );
12 | }
13 | let members = [];
14 | if (props.project) members = props.project.members;
15 | const membersMapped = members.map((member) => {
16 | if (member.member_name === props.currentUsername){
17 | return (
18 |
19 | {member.member_name}
20 | {member.member_email}
21 |
22 | );
23 | }
24 | return (
25 |
26 | {member.member_name}
27 | {member.member_email}
28 |
35 |
36 | );
37 | });
38 | return (
39 |
40 | Current Members
41 | {membersMapped}
42 |
43 | );
44 | };
45 |
46 | export default MemberList;
47 |
--------------------------------------------------------------------------------
/frontend/components/projects/members.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Modal from 'react-modal';
3 | import MemberList from './member_list';
4 |
5 | class Members extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = { formOpen: false, username: "" };
10 |
11 | this.closeForm = this.closeForm.bind(this);
12 | this.openForm = this.openForm.bind(this);
13 | this.handleNameInput = this.handleNameInput.bind(this);
14 | this.handleSubmit = this.handleSubmit.bind(this);
15 | this.handleRemove = this.handleRemove.bind(this);
16 | }
17 |
18 | closeForm(e) {
19 | e.preventDefault();
20 | this.props.resetProjectErrors();
21 | this.setState({ formOpen: false });
22 | }
23 |
24 | openForm() {
25 | this.setState({ formOpen: true });
26 | }
27 |
28 | handleNameInput(e) {
29 | const newName = e.currentTarget.value;
30 | this.setState({username: newName });
31 | }
32 |
33 | handleSubmit(e){
34 | e.preventDefault();
35 | const projectMember = {
36 | username: this.state.username,
37 | project_id: this.props.projectId,
38 | };
39 | this.props.createProjectMember(projectMember);
40 | this.setState({ username: "" });
41 | }
42 |
43 | handleRemove(e){
44 | e.preventDefault();
45 | this.props.deleteProjectMember(e.currentTarget.value);
46 | }
47 |
48 | render() {
49 | const errors = this.props.errors.map((error, idx)=>({error}));
50 |
51 | return (
52 |
53 |
58 |
59 |
60 | Members
61 |
66 |
75 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
89 | formStyling() {
90 | return {
91 | overlay : {
92 | position: 'fixed',
93 | top: 0,
94 | left: 0,
95 | bottom: 0,
96 | right: 0,
97 | background: 'rgba(255, 255, 255, 0.75)',
98 | zIndex: 10,
99 | },
100 | content : {
101 | position: 'fixed',
102 | top: '50%',
103 | left: '50%',
104 | transform: 'translate(-50%, -50%)',
105 | padding: '0',
106 | border: '0',
107 | borderRadius: '3px',
108 | background: '#F6F6F6',
109 | zIndex: 11,
110 | width: '480px',
111 | height: '520px',
112 | boxShadow: '1px 1px 4px #333',
113 | overflow: 'hidden',
114 | },
115 | };
116 | }
117 |
118 | }
119 |
120 | export default Members;
121 |
--------------------------------------------------------------------------------
/frontend/components/projects/members_container.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import {
3 | createProjectMember,
4 | deleteProjectMember,
5 | resetProjectErrors,
6 | } from '../../actions/project_actions';
7 | import Members from './members';
8 |
9 | const mapStateToProps = (state) => {
10 | return {
11 | projectList: state.projects.projectList,
12 | errors: state.projects.errors,
13 | currentUsername: state.session.currentUser.username,
14 | loading: state.loading.membersLoading,
15 | };
16 | };
17 |
18 | const mapDispatchToProps = (dispatch) => {
19 | return {
20 | createProjectMember: (projectMember) => dispatch(createProjectMember(projectMember)),
21 | deleteProjectMember: (projectMemberId) => dispatch(deleteProjectMember(projectMemberId)),
22 | resetProjectErrors: () => dispatch(resetProjectErrors()),
23 | };
24 | };
25 |
26 | export default connect(
27 | mapStateToProps,
28 | mapDispatchToProps
29 | )(Members);
30 |
--------------------------------------------------------------------------------
/frontend/components/projects/project_form_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { createProject, resetProjectErrors } from '../../actions/project_actions';
3 | import ProjectForm from './project_form';
4 |
5 | const mapStateToProps = ( state ) => {
6 | return {
7 | errors: state.projects.errors,
8 | };
9 | };
10 |
11 | const mapDispatchToProps = (dispatch, ownProps) => {
12 | return {
13 | createProject: (project) => dispatch(createProject(project)),
14 | resetProjectErrors: () => dispatch(resetProjectErrors()),
15 | };
16 | };
17 |
18 | export default connect(
19 | mapStateToProps,
20 | mapDispatchToProps
21 | )(ProjectForm);
22 |
--------------------------------------------------------------------------------
/frontend/components/projects/project_list.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProjectListItem from './project_list_item';
3 |
4 |
5 | class ProjectList extends React.Component {
6 | render() {
7 | const fullList = this.props.projects.map((project) => {
8 | return ();
9 | });
10 |
11 | return (
12 |
17 | );
18 | }
19 | }
20 |
21 | export default ProjectList;
22 |
--------------------------------------------------------------------------------
/frontend/components/projects/project_list_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import MembersContainer from './members_container';
4 |
5 | const ProjectListItem = ({ project }) => {
6 | let className = 'project-detail group';
7 | if (project.privacy) {
8 | className += ' private-project';
9 | }
10 | const projectUrl = `/projects/${project.id}`;
11 |
12 | return (
13 |
14 | {project.name}
15 |
16 |
17 | );
18 | };
19 |
20 | export default ProjectListItem;
21 |
--------------------------------------------------------------------------------
/frontend/components/projects/projects.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProjectList from './project_list';
3 | import ProjectFormContainer from './project_form_container';
4 | import { RainbowSpinner } from '../spinners/spinners';
5 |
6 | class Projects extends React.Component {
7 |
8 | componentDidMount() {
9 | this.props.fetchProjects();
10 | }
11 |
12 | renderProjects(){
13 | if (this.props.loading) {
14 | return (
15 |
16 | );
17 | } else {
18 | return (
19 |
20 | );
21 | }
22 | }
23 |
24 | render() {
25 | return (
26 |
27 |
28 | My Projects
29 |
30 |
31 | {this.renderProjects()}
32 |
33 | );
34 | }
35 | }
36 |
37 | export default Projects;
38 |
--------------------------------------------------------------------------------
/frontend/components/projects/projects_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchProjects } from '../../actions/project_actions';
3 | import { selectAllProjects } from '../../reducers/selector';
4 | import Projects from './projects';
5 |
6 | const mapStateToProps = ( state ) => {
7 | return {
8 | projectList: selectAllProjects(state),
9 | projects: state.projects,
10 | loading: state.loading.projectsLoading,
11 | };
12 | };
13 |
14 | const mapDispatchToProps = (dispatch, ownProps) => {
15 | return {
16 | fetchProjects: () => dispatch(fetchProjects()),
17 | };
18 | };
19 |
20 | export default connect(
21 | mapStateToProps,
22 | mapDispatchToProps
23 | )(Projects);
24 |
--------------------------------------------------------------------------------
/frontend/components/root.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { Router, Route, IndexRoute, hashHistory } from 'react-router';
4 | import Main from './main';
5 | import SessionFormContainer from './sessions/session_form_container';
6 | import Dashboard from './dashboard';
7 | import ProjectDetailContainer from './project_detail/project_detail_container';
8 |
9 | const Root = ({ store }) => {
10 |
11 | function _redirectIfLoggedIn(nextState, replace) {
12 | if (store.getState().session.currentUser) {
13 | replace('/');
14 | }
15 | }
16 |
17 | function _redirectUnlessLoggedIn(nextState, replace) {
18 | if (!store.getState().session.currentUser) {
19 | replace('/');
20 | }
21 | }
22 |
23 | return (
24 |
25 |
26 |
27 |
31 |
35 |
36 |
40 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default Root;
50 |
--------------------------------------------------------------------------------
/frontend/components/sessions/session_form_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import SessionForm from './session_form';
3 | import { login, signup, resetErrors } from '../../actions/session_actions';
4 | import { receiveNewHeaderType } from '../../actions/header_actions';
5 |
6 | const mapStateToProps = function (state) {
7 | return {
8 | loggedIn: (state.session.currentUser !== null),
9 | errors: state.session.errors,
10 | };
11 | };
12 |
13 | const mapDispatchToProps = function (dispatch, ownProps) {
14 | const formType = ownProps.location.pathname.slice(1);
15 | let actionFunction = null;
16 | if (formType === 'login') {
17 | actionFunction = login;
18 | } else if (formType === 'signup') {
19 | actionFunction = signup;
20 | }
21 | return {
22 | processForm: (user) => dispatch(actionFunction(user)),
23 | login: (user) => dispatch(login(user)),
24 | resetErrors: () => dispatch(resetErrors()),
25 | receiveNewHeaderType: (headerType) => dispatch(receiveNewHeaderType(headerType)),
26 | formType,
27 | };
28 | };
29 |
30 | export default connect(
31 | mapStateToProps,
32 | mapDispatchToProps
33 | )(SessionForm);
34 |
--------------------------------------------------------------------------------
/frontend/components/spinners/spinners.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const RainbowSpinner = () => {
4 | return (
5 |
6 | );
7 | };
8 |
9 |
10 | export const Spinner = () => {
11 | return (
12 | Loading...
13 | );
14 | };
15 |
16 |
17 | export const Spinner2 = () => {
18 | return (
19 | Loading...
20 | );
21 | };
22 |
23 | export const Spinner3 = () => {
24 | return (
25 | Loading...
26 | );
27 | };
28 |
29 | export const Spinner4 = () => {
30 | return (
31 | Loading...
32 | );
33 | };
34 |
35 | export const Spinner5 = () => {
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/frontend/components/splash_page/splash_page.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 |
4 | class SplashPage extends React.Component {
5 |
6 | renderButton() {
7 | let buttonClass = "splash-get-started-button";
8 | if (this.props.headerType === 'login' || this.props.headerType === 'signup'){
9 | buttonClass += " fade";
10 | }
11 | if (this.props.user) {
12 | return (Dashboard);
13 | } else {
14 | return (Get Started!);
15 | }
16 | }
17 |
18 | render() {
19 | let textClass = "splash-page-text";
20 | let imgClass = "";
21 | if (this.props.headerType === 'login' || this.props.headerType === 'signup') {
22 | textClass += " fade";
23 | imgClass += "blur";
24 | }
25 | return (
26 |
27 |
28 |
29 |
Pixel perfect project management
30 | {this.renderButton()}
31 |
32 |
40 |
41 | );
42 | }
43 | }
44 |
45 | export default SplashPage;
46 |
--------------------------------------------------------------------------------
/frontend/components/splash_page/splash_page_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import SplashPage from './splash_page';
3 |
4 | const mapStateToProps = ( state ) => {
5 | return {
6 | headerType: state.headerInfo.headerType,
7 | user: state.session.currentUser,
8 | };
9 | };
10 |
11 | export default connect(
12 | mapStateToProps
13 | )(SplashPage);
14 |
--------------------------------------------------------------------------------
/frontend/modules/dnd_item_types.js:
--------------------------------------------------------------------------------
1 | export const ItemTypes = {
2 | PIXEL: 'pixel',
3 | };
4 |
--------------------------------------------------------------------------------
/frontend/pixel_tracker.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 | import Modal from 'react-modal';
6 |
7 | document.addEventListener('DOMContentLoaded', () => {
8 | let preloadedState = {};
9 | if (window.currentUser) {
10 | preloadedState = { session: { currentUser: window.currentUser } };
11 | }
12 | const store = configureStore(preloadedState);
13 | const root = document.getElementById('root');
14 | Modal.setAppElement(document.body);
15 | ReactDOM.render(, root);
16 | });
17 |
--------------------------------------------------------------------------------
/frontend/reducers/error_reducer.js:
--------------------------------------------------------------------------------
1 | import { DRAG_ERROR, CLEAR_ERRORS } from '../actions/error_actions';
2 |
3 | const defaultState = { dragErrors: false };
4 |
5 | const ErrorReducer = (state = defaultState, action) => {
6 | Object.freeze(state);
7 | switch (action.type) {
8 | case DRAG_ERROR:
9 | return { dragErrors: true };
10 | case CLEAR_ERRORS:
11 | return defaultState;
12 | default:
13 | return state;
14 | }
15 | };
16 |
17 | export default ErrorReducer;
18 |
--------------------------------------------------------------------------------
/frontend/reducers/header_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_NEW_HEADER_TYPE } from '../actions/header_actions';
2 |
3 | const defaultState = { headerType: "default", tooltip: false };
4 |
5 | const HeaderReducer = (state = defaultState, action) => {
6 | Object.freeze(state);
7 | switch (action.type) {
8 | case RECEIVE_NEW_HEADER_TYPE:
9 | const newState = Object.assign({}, state);
10 | newState.headerType = action.headerType;
11 | if (action.tooltip) newState.tooltip = true;
12 | return newState;
13 | default:
14 | return state;
15 | }
16 | };
17 |
18 | export default HeaderReducer;
19 |
--------------------------------------------------------------------------------
/frontend/reducers/loading_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_PROJECTS,
3 | RECEIVE_ONE_PROJECT,
4 | RECEIVE_PROJECT_ERRORS,
5 | LOADING_PROJECTS,
6 | LOADING_PROJECT_MEMBERS,
7 | } from '../actions/project_actions';
8 |
9 | import {
10 | RECEIVE_ALL_PIXELS,
11 | RECEIVE_PIXEL_DETAIL,
12 | RECEIVE_PIXEL_ERRORS,
13 | LOADING_PIXELS,
14 | DELETE_PIXEL,
15 | LOADING_TASKS,
16 | LOADING_COMMENTS,
17 | LOAD_SINGLE_PIXEL,
18 | LOADING_COMPLETE_SINGLE_PIXEL,
19 | } from '../actions/pixel_actions';
20 |
21 | const defaultState = {
22 | membersLoading: false,
23 | projectsLoading: false,
24 | pixelsLoading: false,
25 | tasksLoading: false,
26 | commentsLoading: false,
27 | individualPixelsLoading: {},
28 | };
29 |
30 | export default (state = defaultState, action) => {
31 | switch (action.type) {
32 | case RECEIVE_PROJECTS:
33 | case RECEIVE_ALL_PIXELS:
34 | case RECEIVE_ONE_PROJECT:
35 | case RECEIVE_PROJECT_ERRORS:
36 | return defaultState;
37 | case RECEIVE_PIXEL_ERRORS:
38 | case RECEIVE_PIXEL_DETAIL:
39 | case DELETE_PIXEL:
40 | const clearState = Object.assign({}, defaultState);
41 | clearState.individualPixelsLoading = state.individualPixelsLoading;
42 | return clearState;
43 | case LOAD_SINGLE_PIXEL:
44 | const newState = (Object.assign({}, state));
45 | const pixelsLoading = Object.assign({}, state.individualPixelsLoading);
46 | pixelsLoading[action.pixelId] = true;
47 | newState.individualPixelsLoading = pixelsLoading;
48 | return newState;
49 | case LOADING_COMPLETE_SINGLE_PIXEL:
50 | const updatedState = (Object.assign({}, state));
51 | const updatedPixelsLoading = Object.assign({}, state.individualPixelsLoading);
52 | updatedPixelsLoading[action.pixelId] = false;
53 | updatedState.individualPixelsLoading = updatedPixelsLoading;
54 | return updatedState;
55 | case LOADING_PROJECTS:
56 | return Object.assign({}, state, { projectsLoading: true });
57 | case LOADING_PROJECT_MEMBERS:
58 | return Object.assign({}, state, { membersLoading: true });
59 | case LOADING_PIXELS:
60 | return Object.assign({}, state, { pixelsLoading: true });
61 | case LOADING_TASKS:
62 | return Object.assign({}, state, { tasksLoading: true });
63 | case LOADING_COMMENTS:
64 | return Object.assign({}, state, { commentsLoading: true });
65 | default:
66 | return state;
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/frontend/reducers/pixel_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_PIXELS,
3 | RECEIVE_PIXEL_DETAIL,
4 | RECEIVE_PIXEL_ERRORS,
5 | DELETE_PIXEL,
6 | RESET_PIXELS,
7 | RECEIVE_TASKS,
8 | UPDATE_ORDS,
9 | UPDATE_PIXELS,
10 | } from '../actions/pixel_actions';
11 |
12 | const defaultState = {
13 | pixelList: {},
14 | errors: {},
15 | tasks: [],
16 | ords: { maxIcebox: 0, maxBacklog: 0, maxDone: 0, maxUnstarted: 0 },
17 | };
18 |
19 | export default (state = defaultState, action) => {
20 | Object.freeze(state);
21 | let newState = Object.assign({}, state);
22 | switch (action.type) {
23 | case RECEIVE_ALL_PIXELS:
24 | newState.errors = {};
25 | newState.pixelList = action.pixels;
26 | return newState;
27 | case RECEIVE_PIXEL_DETAIL:
28 | const curPixelList = Object.assign({}, newState.pixelList);
29 |
30 | curPixelList[action.pixel.id] = action.pixel;
31 | newState.pixelList = curPixelList;
32 | newState.errors = {};
33 | return newState;
34 | case RECEIVE_PIXEL_ERRORS:
35 | newState.errors = action.errors;
36 | return newState;
37 | case RESET_PIXELS:
38 | return defaultState;
39 | case DELETE_PIXEL:
40 | const deletedPixelList = Object.assign({}, newState.pixelList);
41 | delete deletedPixelList[action.pixelId];
42 | newState.pixelList = deletedPixelList;
43 | return newState;
44 | case RECEIVE_TASKS:
45 | newState.tasks = action.tasks;
46 | return newState;
47 | case UPDATE_ORDS:
48 | const newOrds = Object.assign({}, newState.ords);
49 | newOrds.maxIcebox = action.maxIcebox;
50 | newOrds.maxBacklog = action.maxBacklog;
51 | newOrds.maxDone = action.maxDone;
52 | newOrds.maxUnstarted = action.maxUnstarted;
53 | newState.ords = newOrds;
54 | return newState;
55 | case UPDATE_PIXELS:
56 | const updatedPixelList = Object.assign({}, newState.pixelList);
57 | action.pixels.forEach((pixel) => {
58 | const updatedPixel = Object.assign({}, updatedPixelList[pixel.id]);
59 | updatedPixel.icebox = pixel.icebox;
60 | updatedPixel.pixel_ord = pixel.pixel_ord;
61 | updatedPixelList[pixel.id] = updatedPixel;
62 | });
63 | newState.pixelList = updatedPixelList;
64 | newState.errors = {};
65 | return newState;
66 | default:
67 | return state;
68 | }
69 | };
70 |
--------------------------------------------------------------------------------
/frontend/reducers/project_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_PROJECTS, RECEIVE_ONE_PROJECT, RECEIVE_PROJECT_ERRORS }
2 | from '../actions/project_actions';
3 |
4 | const defaultState = { errors: [], projectList: {} };
5 |
6 | const ProjectReducer = (state = defaultState, action) => {
7 | Object.freeze(state);
8 | let newState = Object.assign({}, state);
9 | switch (action.type) {
10 | case RECEIVE_PROJECTS:
11 | newState.projectList = action.projects;
12 | newState.errors = [];
13 | return newState;
14 | case RECEIVE_ONE_PROJECT:
15 | const addedProjectList = Object.assign({}, state.projectList);
16 | addedProjectList[action.project.id] = action.project;
17 | newState.projectList = addedProjectList;
18 | newState.errors = [];
19 | return newState;
20 | case RECEIVE_PROJECT_ERRORS:
21 | newState.errors = action.errors;
22 | return newState;
23 | default:
24 | return state;
25 | }
26 | };
27 |
28 | export default ProjectReducer;
29 |
--------------------------------------------------------------------------------
/frontend/reducers/root_reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import SessionReducer from './session_reducer';
3 | import HeaderReducer from './header_reducer';
4 | import ProjectReducer from './project_reducer';
5 | import LoadingReducer from './loading_reducer';
6 | import PixelReducer from './pixel_reducer';
7 | import SidebarReducer from './sidebar_reducer';
8 | import ErrorReducer from './error_reducer';
9 |
10 | export default combineReducers({
11 | session: SessionReducer,
12 | headerInfo: HeaderReducer,
13 | projects: ProjectReducer,
14 | loading: LoadingReducer,
15 | pixels: PixelReducer,
16 | sidebar: SidebarReducer,
17 | globalErrors: ErrorReducer,
18 | });
19 |
--------------------------------------------------------------------------------
/frontend/reducers/selector.js:
--------------------------------------------------------------------------------
1 | import { values } from 'lodash';
2 |
3 | export const selectAllProjects = (state) => {
4 | const projectList = state.projects.projectList;
5 | const projectListArray = values(projectList);
6 | projectListArray.sort((a, b) => {
7 | if (a.updated_at > b.updated_at ) {
8 | return -1;
9 | }
10 | if (a.updated_at < b.updated_at) {
11 | return 1;
12 | }
13 | return 0;
14 | });
15 | return projectListArray;
16 | };
17 |
18 | export const selectAllPixels = (state) => {
19 | const pixelList = state.pixels.pixelList;
20 | const pixelListArray = values(pixelList);
21 | pixelListArray.sort((a, b) => {
22 | if (a.pixel_ord > b.pixel_ord ) {
23 | return 1;
24 | }
25 | if (a.pixel_ord < b.pixel_ord) {
26 | return -1;
27 | }
28 | return 0;
29 | });
30 | return pixelListArray;
31 | };
32 |
--------------------------------------------------------------------------------
/frontend/reducers/session_reducer.js:
--------------------------------------------------------------------------------
1 | import { RECEIVE_CURRENT_USER, RECEIVE_ERRORS }
2 | from '../actions/session_actions';
3 |
4 | const defaultState = { currentUser: null, errors: [] };
5 |
6 | const SessionReducer = (state = defaultState, action) => {
7 | Object.freeze(state);
8 | const newState = Object.assign({}, state);
9 | switch (action.type) {
10 | case RECEIVE_CURRENT_USER:
11 | newState.currentUser = action.currentUser;
12 | newState.errors = [];
13 | return newState;
14 | case RECEIVE_ERRORS:
15 | newState.errors = action.errors;
16 | return newState;
17 | default:
18 | return state;
19 | }
20 | };
21 |
22 | export default SessionReducer;
23 |
--------------------------------------------------------------------------------
/frontend/reducers/sidebar_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | SHOW_COLUMN,
3 | HIDE_COLUMN,
4 | RESET_VIEW,
5 | } from '../actions/sidebar_actions';
6 |
7 | const defaultState = { icebox: true, done: true, current: true, newPixel: false };
8 |
9 | const SidebarReducer = (state = defaultState, action) => {
10 | Object.freeze(state);
11 | const newState = Object.assign({}, state);
12 | switch (action.type) {
13 | case SHOW_COLUMN:
14 | newState[action.columnName] = true;
15 | return newState;
16 | case HIDE_COLUMN:
17 | newState[action.columnName] = false;
18 | return newState;
19 | case RESET_VIEW:
20 | return defaultState;
21 | default:
22 | return state;
23 | }
24 | };
25 |
26 | export default SidebarReducer;
27 |
--------------------------------------------------------------------------------
/frontend/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import RootReducer from '../reducers/root_reducer';
4 |
5 | const configureStore = (preloadedState = {}) => {
6 | return createStore(
7 | RootReducer,
8 | preloadedState,
9 | applyMiddleware(thunk)
10 | );
11 | };
12 |
13 | export default configureStore;
14 |
--------------------------------------------------------------------------------
/frontend/util/comments_api_util.js:
--------------------------------------------------------------------------------
1 | export const createComment = (comment) => {
2 | return $.ajax({
3 | url: 'api/comments/',
4 | method: 'POST',
5 | data: { comment },
6 | });
7 | };
8 |
9 | export const updateComment = (comment) => {
10 | return $.ajax({
11 | url: `api/comments/${comment.id}`,
12 | method: 'PATCH',
13 | data: { comment },
14 | });
15 | };
16 |
17 | export const deleteComment = (commentId) => {
18 | return $.ajax({
19 | url: `api/comments/${commentId}`,
20 | method: 'DELETE',
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/util/pixel_state_util.js:
--------------------------------------------------------------------------------
1 | export const newPixelState = (curState) => {
2 | switch (curState) {
3 | case 'Unstarted':
4 | return 'Started';
5 | case 'Started':
6 | return 'Finished';
7 | case 'Finished':
8 | return 'Delivered';
9 | case 'Delivered':
10 | return 'Accepted';
11 | case 'Rejected':
12 | return 'Unstarted';
13 | default:
14 | return curState;
15 | }
16 | };
17 |
18 | export const buttonName = (curState) => {
19 | switch (curState) {
20 | case 'Unstarted':
21 | return 'Start';
22 | case 'Started':
23 | return 'Finish';
24 | case 'Finished':
25 | return 'Deliver';
26 | case 'Delivered':
27 | return 'Accept';
28 | case 'Rejected':
29 | return 'Restart';
30 | default:
31 | return 'none';
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/frontend/util/pixels_api_util.js:
--------------------------------------------------------------------------------
1 | export const fetchAllPixels = (projectId) => {
2 | return $.ajax({
3 | url: `api/projects/${projectId}/pixels`,
4 | method: 'GET',
5 | });
6 | };
7 |
8 | export const fetchPixelDetail = (pixelId) => {
9 | return $.ajax({
10 | url: `api/pixels/${pixelId}`,
11 | method: 'GET',
12 | });
13 | };
14 |
15 | export const createPixel = (projectId, pixel) => {
16 | return $.ajax({
17 | url: `api/projects/${projectId}/pixels`,
18 | method: 'POST',
19 | data: { pixel },
20 | });
21 | };
22 |
23 | export const updatePixel = (pixelId, pixel) => {
24 | return $.ajax({
25 | url: `api/pixels/${pixelId}`,
26 | method: 'PATCH',
27 | data: { pixel }
28 | });
29 | };
30 |
31 | export const deletePixel = (pixelId) => {
32 | return $.ajax({
33 | url: `api/pixels/${pixelId}`,
34 | method: 'DELETE',
35 | });
36 | };
37 |
38 | export const massUpdatePixels = (pixels) => {
39 | return $.ajax({
40 | url: 'api/mass_update_pixels',
41 | method: 'PATCH',
42 | data: { pixels },
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/frontend/util/projects_api_util.js:
--------------------------------------------------------------------------------
1 | export const newProject = (project) => {
2 | return $.ajax({
3 | url: '/api/projects/',
4 | method: 'POST',
5 | data: { project },
6 | });
7 | };
8 |
9 | export const fetchProjects = () => {
10 | return $.ajax({
11 | url: '/api/projects/',
12 | method: 'GET',
13 | });
14 | };
15 |
16 | export const fetchOneProject = (projectId) => {
17 | return $.ajax({
18 | url: `/api/projects/${projectId}`,
19 | method: 'GET',
20 | });
21 | };
22 |
23 | export const newProjectMember = ( projectMember ) => {
24 | return $.ajax({
25 | url: '/api/project_members/',
26 | method: 'POST',
27 | data: { project_member: projectMember },
28 | });
29 | };
30 |
31 | export const destroyProjectMember = (projectMemberId) => {
32 | return $.ajax({
33 | url: `/api/project_members/${projectMemberId}`,
34 | method: 'DELETE',
35 | });
36 | };
37 |
--------------------------------------------------------------------------------
/frontend/util/session_api_util.js:
--------------------------------------------------------------------------------
1 | export const signup = (user) => {
2 | return $.ajax({
3 | url: 'api/users',
4 | method: 'POST',
5 | data: { user },
6 | });
7 | };
8 |
9 | export const login = (user) => {
10 | return $.ajax({
11 | url: 'api/session',
12 | method: 'POST',
13 | data: { user },
14 | });
15 | };
16 |
17 | export const logout = () => {
18 | return $.ajax({
19 | url: 'api/session',
20 | method: 'DELETE',
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/util/tasks_api_util.js:
--------------------------------------------------------------------------------
1 | export const createTask = (task) => {
2 | return $.ajax({
3 | url: 'api/tasks/',
4 | method: 'POST',
5 | data: { task },
6 | });
7 | };
8 |
9 | export const updateTask = (task) => {
10 | return $.ajax({
11 | url: `api/tasks/${task.id}`,
12 | method: 'PATCH',
13 | data: { task },
14 | });
15 | };
16 |
17 | export const deleteTask = (taskId) => {
18 | return $.ajax({
19 | url: `api/tasks/${taskId}`,
20 | method: 'DELETE',
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/lib/tasks/.keep
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/log/.keep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pixel_tracker",
3 | "version": "1.0.0",
4 | "description": "Visit the docs folder for more info: [docs](/docs)",
5 | "main": "index.js",
6 | "directories": {
7 | "doc": "docs",
8 | "test": "test"
9 | },
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1",
12 | "postinstall": "./node_modules/.bin/webpack"
13 | },
14 | "engines": {
15 | "node": "6.2.2",
16 | "npm": "3.9.5"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/DireCorgi/pixelTracker.git"
21 | },
22 | "keywords": [],
23 | "author": "",
24 | "license": "ISC",
25 | "bugs": {
26 | "url": "https://github.com/DireCorgi/pixelTracker/issues"
27 | },
28 | "homepage": "https://github.com/DireCorgi/pixelTracker#readme",
29 | "dependencies": {
30 | "babel-core": "^6.18.2",
31 | "babel-loader": "^6.2.8",
32 | "babel-preset-es2015": "^6.18.0",
33 | "babel-preset-react": "^6.16.0",
34 | "lodash": "^4.17.2",
35 | "react": "^15.4.1",
36 | "react-dnd": "^2.1.4",
37 | "react-dnd-html5-backend": "^2.1.2",
38 | "react-dom": "^15.4.1",
39 | "react-joyride": "^1.6.0",
40 | "react-modal": "^1.6.1",
41 | "react-redux": "^4.4.6",
42 | "react-router": "^3.0.0",
43 | "redux": "^3.6.0",
44 | "redux-thunk": "^2.1.0",
45 | "webpack": "^1.13.3"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/public/favicon.ico
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/controllers/.keep
--------------------------------------------------------------------------------
/test/controllers/api/comments_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Api::CommentsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/api/pixels_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Api::PixelsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/api/project_members_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Api::ProjectMembersControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/api/projects_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Api::ProjectsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/api/sessions_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Api::SessionsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/api/tasks_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class Api::TasksControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/controllers/comments_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CommentsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/static_pages_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/fixtures/.keep
--------------------------------------------------------------------------------
/test/fixtures/comments.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: comments
4 | #
5 | # id :integer not null, primary key
6 | # body :text not null
7 | # pixel_id :integer not null
8 | # user_id :integer not null
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 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/fixtures/files/.keep
--------------------------------------------------------------------------------
/test/fixtures/pixels.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: pixels
4 | #
5 | # id :integer not null, primary key
6 | # pixel_ord :integer not null
7 | # state :string not null
8 | # title :string not null
9 | # category :string not null
10 | # description :text
11 | # points :integer not null
12 | # project_id :integer not null
13 | # requester_id :integer not null
14 | # created_at :datetime not null
15 | # updated_at :datetime not null
16 | # icebox :boolean default(TRUE), not null
17 | #
18 |
19 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
20 |
21 | # This model initially had no columns defined. If you add columns to the
22 | # model remove the '{}' from the fixture names and add the columns immediately
23 | # below each fixture, per the syntax in the comments below
24 | #
25 | one: {}
26 | # column: value
27 | #
28 | two: {}
29 | # column: value
30 |
--------------------------------------------------------------------------------
/test/fixtures/project_members.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_members
4 | #
5 | # id :integer not null, primary key
6 | # project_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 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
13 |
14 | # This model initially had no columns defined. If you add columns to the
15 | # model remove the '{}' from the fixture names and add the columns immediately
16 | # below each fixture, per the syntax in the comments below
17 | #
18 | one: {}
19 | # column: value
20 | #
21 | two: {}
22 | # column: value
23 |
--------------------------------------------------------------------------------
/test/fixtures/projects.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: projects
4 | #
5 | # id :integer not null, primary key
6 | # name :string not null
7 | # private :boolean not null
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 |
12 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
13 |
14 | # This model initially had no columns defined. If you add columns to the
15 | # model remove the '{}' from the fixture names and add the columns immediately
16 | # below each fixture, per the syntax in the comments below
17 | #
18 | one: {}
19 | # column: value
20 | #
21 | two: {}
22 | # column: value
23 |
--------------------------------------------------------------------------------
/test/fixtures/tasks.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: tasks
4 | #
5 | # id :integer not null, primary key
6 | # body :text not null
7 | # complete :boolean default(FALSE), not null
8 | # pixel_id :integer not null
9 | # task_ord :integer not null
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | #
13 |
14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
15 |
16 | # This model initially had no columns defined. If you add columns to the
17 | # model remove the '{}' from the fixture names and add the columns immediately
18 | # below each fixture, per the syntax in the comments below
19 | #
20 | one: {}
21 | # column: value
22 | #
23 | two: {}
24 | # column: value
25 |
--------------------------------------------------------------------------------
/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 not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | # email :string
12 | #
13 |
14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
15 |
16 | # This model initially had no columns defined. If you add columns to the
17 | # model remove the '{}' from the fixture names and add the columns immediately
18 | # below each fixture, per the syntax in the comments below
19 | #
20 | one: {}
21 | # column: value
22 | #
23 | two: {}
24 | # column: value
25 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/helpers/.keep
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/test/models/.keep
--------------------------------------------------------------------------------
/test/models/comment_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: comments
4 | #
5 | # id :integer not null, primary key
6 | # body :text not null
7 | # pixel_id :integer not null
8 | # user_id :integer not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 |
13 | require 'test_helper'
14 |
15 | class CommentTest < ActiveSupport::TestCase
16 | # test "the truth" do
17 | # assert true
18 | # end
19 | end
20 |
--------------------------------------------------------------------------------
/test/models/pixel_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: pixels
4 | #
5 | # id :integer not null, primary key
6 | # pixel_ord :integer not null
7 | # state :string not null
8 | # title :string not null
9 | # category :string not null
10 | # description :text
11 | # points :integer not null
12 | # project_id :integer not null
13 | # requester_id :integer not null
14 | # created_at :datetime not null
15 | # updated_at :datetime not null
16 | # icebox :boolean default(TRUE), not null
17 | #
18 |
19 | require 'test_helper'
20 |
21 | class PixelTest < ActiveSupport::TestCase
22 | # test "the truth" do
23 | # assert true
24 | # end
25 | end
26 |
--------------------------------------------------------------------------------
/test/models/project_member_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: project_members
4 | #
5 | # id :integer not null, primary key
6 | # project_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 | require 'test_helper'
13 |
14 | class ProjectMemberTest < ActiveSupport::TestCase
15 | # test "the truth" do
16 | # assert true
17 | # end
18 | end
19 |
--------------------------------------------------------------------------------
/test/models/project_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: projects
4 | #
5 | # id :integer not null, primary key
6 | # name :string not null
7 | # private :boolean not null
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 |
12 | require 'test_helper'
13 |
14 | class ProjectTest < ActiveSupport::TestCase
15 | # test "the truth" do
16 | # assert true
17 | # end
18 | end
19 |
--------------------------------------------------------------------------------
/test/models/task_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: tasks
4 | #
5 | # id :integer not null, primary key
6 | # body :text not null
7 | # complete :boolean default(FALSE), not null
8 | # pixel_id :integer not null
9 | # task_ord :integer not null
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | #
13 |
14 | require 'test_helper'
15 |
16 | class TaskTest < ActiveSupport::TestCase
17 | # test "the truth" do
18 | # assert true
19 | # end
20 | end
21 |
--------------------------------------------------------------------------------
/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 not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | # email :string
12 | #
13 |
14 | require 'test_helper'
15 |
16 | class UserTest < ActiveSupport::TestCase
17 | # test "the truth" do
18 | # assert true
19 | # end
20 | end
21 |
--------------------------------------------------------------------------------
/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 order.
7 | fixtures :all
8 |
9 | # Add more helper methods to be used by all tests here...
10 | end
11 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/tmp/.keep
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/vendor/assets/javascripts/.keep
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DireCorgi/PixelTracker/cc6ff3befafc0824a3bd49d2144f28a789dcb91b/vendor/assets/stylesheets/.keep
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require("path");
3 |
4 | module.exports = {
5 | context: __dirname,
6 | entry: './frontend/pixel_tracker.jsx',
7 | output: {
8 | path: path.join(__dirname, 'app', 'assets', 'javascripts'),
9 | filename: "bundle.js"
10 | },
11 | module: {
12 | loaders: [
13 | {
14 | test: [/\.jsx?$/, /\.js?$/],
15 | exclude: /(node_modules|bower_components)/,
16 | loader: 'babel',
17 | query: {
18 | presets: ['es2015', 'react']
19 | }
20 | }
21 | ]
22 | },
23 | devtool: 'source-maps',
24 | resolve: {
25 | extensions: ["", ".js", ".jsx" ]
26 | },
27 | plugins: [
28 | new webpack.DefinePlugin({
29 | 'process.env':{
30 | 'NODE_ENV': JSON.stringify('production')
31 | }
32 | }),
33 | new webpack.optimize.UglifyJsPlugin({
34 | compress:{
35 | warnings: false
36 | }
37 | })
38 | ],
39 | };
40 |
--------------------------------------------------------------------------------