├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README-DEV.md ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── gifs │ │ ├── auth-gif.gif │ │ ├── channel-gif.gif │ │ ├── live-chat-gif.gif │ │ ├── login-gif.gif │ │ ├── new-message-gif.gif │ │ ├── replies-gif.gif │ │ └── responsive-gif.gif │ ├── images │ │ ├── .DS_Store │ │ ├── .keep │ │ ├── Hamilton.jpg │ │ ├── detailed-wh.png │ │ ├── favicon.ico │ │ ├── george.jpg │ │ ├── hoover.jpg │ │ ├── jfk.jpeg │ │ ├── lake.jpeg │ │ ├── lincoln.jpeg │ │ ├── obama.jpeg │ │ ├── rwb-stars2.jpg │ │ ├── teddy.jpg │ │ ├── yosemite.jpg │ │ └── yosemitesmall.jpg │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ └── channels │ │ │ └── .keep │ └── stylesheets │ │ ├── application.css │ │ ├── channel_list.css.scss │ │ ├── homepage.css.scss │ │ ├── landing_page.css.scss │ │ ├── leftcol.css.scss │ │ ├── reset.css │ │ ├── selected_input.css.scss │ │ └── selected_messages.css.scss ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ ├── channels_controller.rb │ │ ├── memberships_controller.rb │ │ ├── messages_controller.rb │ │ ├── replies_controller.rb │ │ ├── sessions_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ └── static_pages_controller.rb ├── helpers │ └── application_helper.rb ├── jobs │ └── application_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── channel.rb │ ├── concerns │ │ └── .keep │ ├── membership.rb │ ├── message.rb │ ├── reply.rb │ └── user.rb └── views │ ├── api │ ├── channels │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── memberships │ │ └── show.json.jbuilder │ ├── messages │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── replies │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ └── users │ │ ├── _user.json.jbuilder │ │ ├── index.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 │ ├── pusher.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20170620135604_create_messages.rb │ ├── 20170620140138_create_users.rb │ ├── 20170620140145_create_channels.rb │ ├── 20170620140209_create_memberships.rb │ ├── 20170621210151_remove_email_from_users.rb │ ├── 20170627144802_add_attachment_avatar_to_users.rb │ ├── 20170627203533_add_users_to_channels.rb │ └── 20171019204157_create_replies.rb ├── schema.rb └── seeds.rb ├── docs ├── api-endpoints.md ├── component-hierarchy.md ├── sample_state.md ├── schema.md └── wireframes │ ├── direct_message_form.JPG │ ├── homepage.JPG │ ├── landing_page.JPG │ ├── new_channel_form.JPG │ └── session_form.JPG ├── frontend ├── actions │ ├── channel_actions.js │ ├── current_message_actions.js │ ├── membership_actions.js │ ├── message_actions.js │ ├── modal_actions.js │ ├── reply_actions.js │ ├── session_actions.js │ └── user_actions.js ├── components │ ├── App.jsx │ ├── greeting │ │ ├── greeting.jsx │ │ └── greeting_container.jsx │ ├── homepage │ │ ├── homepage.jsx │ │ ├── leftcol │ │ │ ├── channel_scroller │ │ │ │ ├── channel_scroller.jsx │ │ │ │ ├── channels │ │ │ │ │ ├── channel_list.jsx │ │ │ │ │ ├── channel_list_container.jsx │ │ │ │ │ ├── channel_list_item.jsx │ │ │ │ │ ├── channel_list_item_container.jsx │ │ │ │ │ ├── direct_message_item.jsx │ │ │ │ │ ├── direct_message_item_container.jsx │ │ │ │ │ ├── direct_message_list.jsx │ │ │ │ │ ├── direct_message_list_container.jsx │ │ │ │ │ ├── new_channel_form.jsx │ │ │ │ │ ├── new_channel_form_container.jsx │ │ │ │ │ ├── new_direct_message_form.jsx │ │ │ │ │ └── new_direct_message_form_container.jsx │ │ │ │ └── searches │ │ │ │ │ ├── search_list.jsx │ │ │ │ │ ├── search_list_container.jsx │ │ │ │ │ ├── search_list_item.jsx │ │ │ │ │ ├── selected_list.jsx │ │ │ │ │ └── unselected_list.jsx │ │ │ ├── header │ │ │ │ ├── col_header.jsx │ │ │ │ └── col_header_container.js │ │ │ └── leftcol.jsx │ │ └── selected_channel │ │ │ ├── selected_channel.jsx │ │ │ ├── selected_channel_container.jsx │ │ │ ├── selected_header │ │ │ ├── selected_header.jsx │ │ │ └── selected_header_container.jsx │ │ │ ├── selected_input │ │ │ ├── selected_input.jsx │ │ │ └── selected_input_container.jsx │ │ │ └── selected_messages │ │ │ ├── message_replies.jsx │ │ │ ├── message_reply_input.jsx │ │ │ ├── message_reply_item.jsx │ │ │ ├── selected_message_item.jsx │ │ │ ├── selected_message_item_container.jsx │ │ │ ├── selected_message_list.jsx │ │ │ └── selected_message_list_container.jsx │ ├── landing_page │ │ ├── landing_page.jsx │ │ ├── navbar │ │ │ ├── navbar.jsx │ │ │ └── navbar_container.js │ │ └── session_form │ │ │ ├── session_form.jsx │ │ │ └── session_form_container.js │ └── root.jsx ├── entry.jsx ├── reducers │ ├── channel_reducer.js │ ├── current_message_reducer.js │ ├── message_reducer.js │ ├── modal_reducer.js │ ├── replies_reducer.js │ ├── root_reducer.js │ ├── session_reducer.js │ └── user_reducer.js ├── store │ └── store.js └── util │ ├── channel_api_util.js │ ├── membership_api_util.js │ ├── message_api_util.js │ ├── reply_api_util.js │ ├── route_util.js │ ├── session_api_util.js │ └── user_api_util.js ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── package-lock.json ├── 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 ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ └── replies.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ └── reply_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.js.map 9 | bundle.js 10 | .DS_Store 11 | 12 | # Ignore all logfiles and tempfiles. 13 | node_modules/ 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore Byebug command history file. 20 | .byebug_history 21 | 22 | # Ignore application configuration 23 | /config/application.yml 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | gem 'pusher' 9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 10 | gem 'rails', '~> 5.0.3' 11 | # Use postgresql as the database for Active Record 12 | gem 'pg', '~> 0.18' 13 | # Use Puma as the app server 14 | gem 'puma', '~> 3.0' 15 | # Use SCSS for stylesheets 16 | gem 'sass-rails', '~> 5.0' 17 | # Use Uglifier as compressor for JavaScript assets 18 | gem 'uglifier', '>= 1.3.0' 19 | # Use CoffeeScript for .coffee assets and views 20 | gem 'coffee-rails', '~> 4.2' 21 | # See https://github.com/rails/execjs#readme for more supported runtimes 22 | # gem 'therubyracer', platforms: :ruby 23 | gem 'rails_12factor' 24 | # Use jquery as the JavaScript library 25 | gem 'jquery-rails' 26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 27 | gem 'jbuilder', '~> 2.5' 28 | # Use Redis adapter to run Action Cable in production 29 | # gem 'redis', '~> 3.0' 30 | # Use ActiveModel has_secure_password 31 | gem 'bcrypt', '~> 3.1.7' 32 | 33 | gem 'figaro' 34 | 35 | gem 'aws-sdk', '>= 2.0' 36 | 37 | gem 'paperclip','~> 5.0.0beta1' 38 | # Use Capistrano for deployment 39 | # gem 'capistrano-rails', group: :development 40 | 41 | group :development, :test do 42 | # Call 'byebug' anywhere in the code to stop execution and get a console 43 | gem 'byebug', platform: :mri 44 | end 45 | 46 | group :development do 47 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 48 | gem 'web-console', '>= 3.3.0' 49 | gem 'listen', '~> 3.0.5' 50 | gem 'annotate' 51 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 52 | gem 'spring' 53 | gem 'spring-watcher-listen', '~> 2.0.0' 54 | end 55 | 56 | gem 'pry-rails' 57 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 58 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 59 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.0.4) 5 | actionpack (= 5.0.4) 6 | nio4r (>= 1.2, < 3.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.0.4) 9 | actionpack (= 5.0.4) 10 | actionview (= 5.0.4) 11 | activejob (= 5.0.4) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.0.4) 15 | actionview (= 5.0.4) 16 | activesupport (= 5.0.4) 17 | rack (~> 2.0) 18 | rack-test (~> 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.0.4) 22 | activesupport (= 5.0.4) 23 | builder (~> 3.1) 24 | erubis (~> 2.7.0) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | activejob (5.0.4) 28 | activesupport (= 5.0.4) 29 | globalid (>= 0.3.6) 30 | activemodel (5.0.4) 31 | activesupport (= 5.0.4) 32 | activerecord (5.0.4) 33 | activemodel (= 5.0.4) 34 | activesupport (= 5.0.4) 35 | arel (~> 7.0) 36 | activesupport (5.0.4) 37 | concurrent-ruby (~> 1.0, >= 1.0.2) 38 | i18n (~> 0.7) 39 | minitest (~> 5.1) 40 | tzinfo (~> 1.1) 41 | annotate (2.6.5) 42 | activerecord (>= 2.3.0) 43 | rake (>= 0.8.7) 44 | arel (7.1.4) 45 | aws-sdk (2.10.0) 46 | aws-sdk-resources (= 2.10.0) 47 | aws-sdk-core (2.10.0) 48 | aws-sigv4 (~> 1.0) 49 | jmespath (~> 1.0) 50 | aws-sdk-resources (2.10.0) 51 | aws-sdk-core (= 2.10.0) 52 | aws-sigv4 (1.0.0) 53 | bcrypt (3.1.11) 54 | bindex (0.5.0) 55 | builder (3.2.3) 56 | byebug (9.0.6) 57 | climate_control (0.2.0) 58 | cocaine (0.5.8) 59 | climate_control (>= 0.0.3, < 1.0) 60 | coderay (1.1.1) 61 | coffee-rails (4.2.2) 62 | coffee-script (>= 2.2.0) 63 | railties (>= 4.0.0) 64 | coffee-script (2.4.1) 65 | coffee-script-source 66 | execjs 67 | coffee-script-source (1.12.2) 68 | concurrent-ruby (1.0.5) 69 | erubis (2.7.0) 70 | execjs (2.7.0) 71 | ffi (1.9.18) 72 | figaro (1.1.1) 73 | thor (~> 0.14) 74 | globalid (0.4.0) 75 | activesupport (>= 4.2.0) 76 | httpclient (2.8.3) 77 | i18n (0.8.4) 78 | jbuilder (2.7.0) 79 | activesupport (>= 4.2.0) 80 | multi_json (>= 1.2) 81 | jmespath (1.3.1) 82 | jquery-rails (4.3.1) 83 | rails-dom-testing (>= 1, < 3) 84 | railties (>= 4.2.0) 85 | thor (>= 0.14, < 2.0) 86 | listen (3.0.8) 87 | rb-fsevent (~> 0.9, >= 0.9.4) 88 | rb-inotify (~> 0.9, >= 0.9.7) 89 | loofah (2.0.3) 90 | nokogiri (>= 1.5.9) 91 | mail (2.6.6) 92 | mime-types (>= 1.16, < 4) 93 | method_source (0.8.2) 94 | mime-types (3.1) 95 | mime-types-data (~> 3.2015) 96 | mime-types-data (3.2016.0521) 97 | mimemagic (0.3.2) 98 | mini_portile2 (2.2.0) 99 | minitest (5.10.2) 100 | multi_json (1.12.1) 101 | nio4r (2.1.0) 102 | nokogiri (1.8.0) 103 | mini_portile2 (~> 2.2.0) 104 | paperclip (5.0.0) 105 | activemodel (>= 4.2.0) 106 | activesupport (>= 4.2.0) 107 | cocaine (~> 0.5.5) 108 | mime-types 109 | mimemagic (~> 0.3.0) 110 | pg (0.21.0) 111 | pry (0.10.4) 112 | coderay (~> 1.1.0) 113 | method_source (~> 0.8.1) 114 | slop (~> 3.4) 115 | pry-rails (0.3.6) 116 | pry (>= 0.10.4) 117 | puma (3.9.1) 118 | pusher (1.3.1) 119 | httpclient (~> 2.7) 120 | multi_json (~> 1.0) 121 | pusher-signature (~> 0.1.8) 122 | pusher-signature (0.1.8) 123 | rack (2.0.3) 124 | rack-test (0.6.3) 125 | rack (>= 1.0) 126 | rails (5.0.4) 127 | actioncable (= 5.0.4) 128 | actionmailer (= 5.0.4) 129 | actionpack (= 5.0.4) 130 | actionview (= 5.0.4) 131 | activejob (= 5.0.4) 132 | activemodel (= 5.0.4) 133 | activerecord (= 5.0.4) 134 | activesupport (= 5.0.4) 135 | bundler (>= 1.3.0, < 2.0) 136 | railties (= 5.0.4) 137 | sprockets-rails (>= 2.0.0) 138 | rails-dom-testing (2.0.3) 139 | activesupport (>= 4.2.0) 140 | nokogiri (>= 1.6) 141 | rails-html-sanitizer (1.0.3) 142 | loofah (~> 2.0) 143 | rails_12factor (0.0.3) 144 | rails_serve_static_assets 145 | rails_stdout_logging 146 | rails_serve_static_assets (0.0.5) 147 | rails_stdout_logging (0.0.5) 148 | railties (5.0.4) 149 | actionpack (= 5.0.4) 150 | activesupport (= 5.0.4) 151 | method_source 152 | rake (>= 0.8.7) 153 | thor (>= 0.18.1, < 2.0) 154 | rake (12.0.0) 155 | rb-fsevent (0.9.8) 156 | rb-inotify (0.9.10) 157 | ffi (>= 0.5.0, < 2) 158 | sass (3.4.24) 159 | sass-rails (5.0.6) 160 | railties (>= 4.0.0, < 6) 161 | sass (~> 3.1) 162 | sprockets (>= 2.8, < 4.0) 163 | sprockets-rails (>= 2.0, < 4.0) 164 | tilt (>= 1.1, < 3) 165 | slop (3.6.0) 166 | spring (2.0.2) 167 | activesupport (>= 4.2) 168 | spring-watcher-listen (2.0.1) 169 | listen (>= 2.7, < 4.0) 170 | spring (>= 1.2, < 3.0) 171 | sprockets (3.7.1) 172 | concurrent-ruby (~> 1.0) 173 | rack (> 1, < 3) 174 | sprockets-rails (3.2.0) 175 | actionpack (>= 4.0) 176 | activesupport (>= 4.0) 177 | sprockets (>= 3.0.0) 178 | thor (0.19.4) 179 | thread_safe (0.3.6) 180 | tilt (2.0.7) 181 | tzinfo (1.2.3) 182 | thread_safe (~> 0.1) 183 | uglifier (3.2.0) 184 | execjs (>= 0.3.0, < 3) 185 | web-console (3.5.1) 186 | actionview (>= 5.0) 187 | activemodel (>= 5.0) 188 | bindex (>= 0.4.0) 189 | railties (>= 5.0) 190 | websocket-driver (0.6.5) 191 | websocket-extensions (>= 0.1.0) 192 | websocket-extensions (0.1.2) 193 | 194 | PLATFORMS 195 | ruby 196 | 197 | DEPENDENCIES 198 | annotate 199 | aws-sdk (>= 2.0) 200 | bcrypt (~> 3.1.7) 201 | byebug 202 | coffee-rails (~> 4.2) 203 | figaro 204 | jbuilder (~> 2.5) 205 | jquery-rails 206 | listen (~> 3.0.5) 207 | paperclip (~> 5.0.0beta1) 208 | pg (~> 0.18) 209 | pry-rails 210 | puma (~> 3.0) 211 | pusher 212 | rails (~> 5.0.3) 213 | rails_12factor 214 | sass-rails (~> 5.0) 215 | spring 216 | spring-watcher-listen (~> 2.0.0) 217 | tzinfo-data 218 | uglifier (>= 1.3.0) 219 | web-console (>= 3.3.0) 220 | 221 | BUNDLED WITH 222 | 1.15.1 223 | -------------------------------------------------------------------------------- /README-DEV.md: -------------------------------------------------------------------------------- 1 | # Slacker Development 2 | 3 | ## Minimum Viable Product 4 | Slacker is a web application using Ruby on Rails and React/Redux. By the end of Week 9, this app will, at a minimum, satisfy the following criteria with smooth, bug-free navigation, adequate seed data and sufficient CSS styling: 5 | 6 | - Channels 7 | - Direct Message 8 | - Live chat 9 | - Teams or multi-person DM 10 | - Bonus: Search Messages 11 | - Bonus: Notifications 12 | 13 | ## Proposal 14 | - MVP 15 | - Wireframes 16 | - React Components 17 | - Static State 18 | - DB Schema 19 | - API Endpoints 20 | - Timeline 21 | 22 | 23 | ## Implementation Timeline 24 | 25 | ### Phase 1: Backend setup and Front End User Authentication (2 days) 26 | 27 | **Objectives:** Create a functioning rails project as well as front end authentication. 28 | 29 | ### Phase 2: Login Page (1 day) 30 | 31 | **Objectives:** Create and style a login page. 32 | 33 | ### Phase 3: Channel Model, API, and components (1.5 days) 34 | 35 | **Objectives:** Channels can be created, read, 36 | edited, and destroyed through the API. The channels will be created on a separate page. 37 | 38 | ### Phase 4: Styling Channels (0.5 days) 39 | 40 | **Objectives:** Style channels on the homepage. 41 | 42 | ### Phase 5: Messages (1.5 days) 43 | 44 | **Objectives:** Fill in the channels with messages that can also be created, read, edited, and destroyed through the API. 45 | 46 | ### Phase 6: Styling Messages (0.5 days) 47 | 48 | **Objectives:** Style messages on the homepage. 49 | 50 | ### Phase 7: Direct Messages (1.5 days) 51 | 52 | **Objectives:** Add private and direct messaging. 53 | 54 | ### Phase 8: Style Direct Message (0.5 days) 55 | 56 | **Objectives:** Style direct messaging on the homepage. 57 | 58 | ### Phase 9: Multi-Person Direct Messages (1.5 days) 59 | 60 | **Objectives:** Creation of direct messages to multiple recipients; a channel is created with those people as members. 61 | 62 | ### Phase 10: Style Complete Homepage (0.5 days) 63 | 64 | **Objectives:** Ensure the styling on the homepage is seamless. 65 | 66 | ## Phase 11: Due Diligence (1 day) 67 | 68 | **Objectives:** Ensure the application is seamless. 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 52 | 53 | # Slacker 54 | 55 | [LIVE](https://fullstackslacker.herokuapp.com/) 56 | 59 | 60 | 61 | Slacker is a web application inspired by a well-known communication app, Slack. The application is meant to have the same comfortable feel and functionality of Slack with a few personal touches to make the application more fun. 62 | 63 | ## Features and implementation 64 | 65 | Slacker's UI and single-page architecture were built with React.js and Ruby on Rails. Messaging data is stored in a Postgres database, while AWS hosts all user images. 66 | 67 | In addition, the app also utilizes the following technologies: 68 | 69 | Redux 70 | BCrypt 71 | Pusher 72 | Figaro 73 | Paperclip 74 | jBuilder 75 | react-modal 76 | 77 | To provide most sensible explanation of what Slick is capable of, its features will be described in the following order: 78 | 1. [Users](#users) 79 | 2. [Channels and Subscriptions](#channels-and-subscriptions) 80 | 3. [Messages](#messages) 81 | 4. [Live Chat](#live-chat) 82 | 5. [Replies](#replies) 83 | 6. [Responsiveness](#responsiveness) 84 | 85 | 86 | ## Users 87 |
88 | login 89 | auth 90 |
91 | 92 | Slacker allows users to Sign Up and Log in using intuitive modals on the splash page. The application has front end and back end authorization to safely store the password for old and new users. On the backend, the password is put through a hashing function and stored in the database under 'password_digest'. When a user then goes to sign in, the password is put through the same hashing function (BCrpyt) and if the two password_digests are equal the user will be logged in. Upon logging in, the users session_token is attached to the browser so that the user can refresh the page and not have to tediously log in another time. 93 | 94 | ## Channels and Subscriptions 95 |
96 | new-channel 97 |
98 | 99 | On Slacker, the demo accounts (George Washington and Theodore Roosevelt) begin with memberships to several channels in addition to the ability to create more. Because users have many channels and channels have many users I employed a through table called 'Memberships' to handle the relationship of channels and users. Upon creating a channel, the selected users will have a 'Membership' created with that specific channel. 100 | 101 | There are two types of channels in Slacker - public Public Channels and Direct Messages. Both are created in the left column of the application. Upon clicking the plus sign, a modal will appear showing the names of every user in the database. The user can click the the names of the users they would like in the new channel or direct message. 102 | 103 | 104 | ## Messages 105 |
106 | new-message 107 |
108 | 109 | Messages are created when the user submits content in the most bottom part of middle column. Upon submitting, the message is immediately displayed and the bottom of the chat will come into screen. Messages are stored in the database and only called upon the clicking of the channel they belong to. This keeps the initial rendering of the application quick. 110 | 111 | 112 | ## Live Chat 113 |
114 | chatting 115 |
116 | 117 | Live chat is available using the pusher api. When a message is sent and validated on the on the backend, pusher's 'trigger' method is called with the event 'message_published' and the specific channel's id with which it was saved. The channels, which listening for 'message_published', will fetch the new message when this event is received. 118 | 119 | Using the publish and subscribe model users enjoy live group chat in all channels where they have a membership. 120 | 121 | 122 | ## Replies 123 |
124 | replies 125 |
126 | 127 | In addition to responding to messages in the channel, users can directly respond to messages in the reply thread. When messages are sent to the frontend using jBuilder, they hold their replies as an object in the state. This allows for fast look up on the frontend. 128 | 129 | 130 | ## Responsiveness 131 |
132 | responsive 133 |
134 | 135 | The width of browser determines how long, how big, and which of the elements will appear on the application. By using media queries, the messages and replies adjust so that the content of each can be seen in any circumstance. 136 | 137 | ## Future Directions for the Project 138 | 139 | As for future directions, following are desired features for Slick: 140 | 141 | 1. Persisting emoticon data 142 | 2. Notifications 143 | -------------------------------------------------------------------------------- /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/gifs/auth-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/auth-gif.gif -------------------------------------------------------------------------------- /app/assets/gifs/channel-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/channel-gif.gif -------------------------------------------------------------------------------- /app/assets/gifs/live-chat-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/live-chat-gif.gif -------------------------------------------------------------------------------- /app/assets/gifs/login-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/login-gif.gif -------------------------------------------------------------------------------- /app/assets/gifs/new-message-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/new-message-gif.gif -------------------------------------------------------------------------------- /app/assets/gifs/replies-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/replies-gif.gif -------------------------------------------------------------------------------- /app/assets/gifs/responsive-gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/gifs/responsive-gif.gif -------------------------------------------------------------------------------- /app/assets/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/.DS_Store -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/Hamilton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/Hamilton.jpg -------------------------------------------------------------------------------- /app/assets/images/detailed-wh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/detailed-wh.png -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/george.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/george.jpg -------------------------------------------------------------------------------- /app/assets/images/hoover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/hoover.jpg -------------------------------------------------------------------------------- /app/assets/images/jfk.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/jfk.jpeg -------------------------------------------------------------------------------- /app/assets/images/lake.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/lake.jpeg -------------------------------------------------------------------------------- /app/assets/images/lincoln.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/lincoln.jpeg -------------------------------------------------------------------------------- /app/assets/images/obama.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/obama.jpeg -------------------------------------------------------------------------------- /app/assets/images/rwb-stars2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/rwb-stars2.jpg -------------------------------------------------------------------------------- /app/assets/images/teddy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/teddy.jpg -------------------------------------------------------------------------------- /app/assets/images/yosemite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/yosemite.jpg -------------------------------------------------------------------------------- /app/assets/images/yosemitesmall.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/images/yosemitesmall.jpg -------------------------------------------------------------------------------- /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/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /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_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/channel_list.css.scss: -------------------------------------------------------------------------------- 1 | .fa-plus-circle:hover { 2 | color: white !important; 3 | } 4 | 5 | .direct-message-space-holder { 6 | height: 80px; 7 | width: 400px; 8 | } 9 | 10 | .channel-space-holder { 11 | height: 20px; 12 | } 13 | 14 | .channel-list { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | .new-channel-form { 20 | justify-content: center; 21 | display: flex; 22 | flex-direction: column; 23 | width: 500px; 24 | font-size: 24px; 25 | border: none; 26 | outline: none; 27 | font-family: 'Nunito', sans-serif; 28 | overflow: hidden; 29 | } 30 | 31 | .channel-form-input { 32 | color: #eee; 33 | width: 480px; 34 | height: 48px; 35 | border-radius: 10px; 36 | border: 2px solid; 37 | padding: 5px; 38 | margin-top: 16px; 39 | font-size: 26px; 40 | padding-left: 10px; 41 | outline: none; 42 | } 43 | 44 | .channel-form-input:focus{ 45 | color: black; 46 | } 47 | 48 | .channel-list-item a { 49 | display: block; 50 | border-bottom: none; 51 | text-decoration: none; 52 | color: inherit; 53 | size: inherit; 54 | padding-top: 3px; 55 | padding-bottom: 2px; 56 | text-overflow: ellipsis; 57 | height: 14px; 58 | width: -1px; 59 | padding-left: 8px; 60 | border-radius: 5px; 61 | } 62 | 63 | .selector { 64 | background-color: #4c9689 !important; 65 | color: white !important; 66 | border-radius: 5px !important; 67 | } 68 | 69 | 70 | .channel-list-item { 71 | margin-bottom: 10px; 72 | color: #9e8e9c; 73 | font-size: 14px; 74 | text-overflow: ellipsis; 75 | height: 19px; 76 | overflow: hidden; 77 | text-overflow: ellipsis; 78 | white-space: nowrap; 79 | overflow-x: hidden; 80 | width: 195px; 81 | 82 | a:hover { 83 | background-color: #231221; 84 | } 85 | } 86 | 87 | 88 | channel-list-item:hover{ 89 | color: #555459; 90 | } 91 | 92 | .open-channel { 93 | } 94 | 95 | .closed-channel { 96 | 97 | } 98 | 99 | .big-text-nav-bar { 100 | font-size: 13px; 101 | color: #ab9ba9; 102 | margin-left: 5px; 103 | color: #9e8e9c; 104 | } 105 | 106 | .channel-submit { 107 | margin-top: 10px; 108 | margin-right: 20px; 109 | background-color: #2ab27b; 110 | background-repeat: #2ab27b; 111 | border: none; 112 | overflow: hidden; 113 | outline: none; 114 | cursor: pointer; 115 | font-size: 26px; 116 | color: white; 117 | opacity: .99; 118 | color: white; 119 | width: 500px; 120 | justify-content: center; 121 | font-family: 'Nunito', sans-serif; 122 | height: 64px; 123 | border-radius: 7px; 124 | } 125 | 126 | .channel-and-form { 127 | display: flex; 128 | justify-content: space-between; 129 | align-items: center; 130 | } 131 | 132 | .channel-intro { 133 | position: relative; 134 | left: 38px; 135 | } 136 | 137 | .channel-label { 138 | font-size:14px; 139 | color: #9e8e9c; 140 | font-size: 14px; 141 | color: #9e8e9c; 142 | position: relative; 143 | left: 13px; 144 | margin-bottom: 8px; 145 | } 146 | 147 | .direct-message-label { 148 | position: relative; 149 | left: 9px; 150 | margin-bottom: 8px; 151 | } 152 | 153 | .direct-message-intro { 154 | position: relative; 155 | top: 68px; 156 | left: 35px; 157 | } 158 | 159 | .whole-direct-message-list { 160 | margin-top: 22px; 161 | } 162 | 163 | .button { 164 | padding-right: 11px; 165 | font-size: 16px; 166 | color: #9e8e9c; 167 | position: relative; 168 | bottom: 7px; 169 | } 170 | 171 | .button:hover { 172 | cursor: pointer; 173 | } 174 | 175 | .fa-circle { 176 | font-size: 12px; 177 | color: #38978d; 178 | padding-right: 5px; 179 | } 180 | 181 | .time-stamp { 182 | font-size: 11px; 183 | margin-left: 10px; 184 | font-weight: 200; 185 | 186 | } 187 | 188 | .selected-users-box { 189 | border-bottom: 1px solid; 190 | height: 110px; 191 | font-size: 18px; 192 | overflow-y: scroll; 193 | padding-bottom: 50px; 194 | margin-top: 30px; 195 | } 196 | 197 | .unselected-users-box { 198 | height: 110px; 199 | font-size: 18px; 200 | overflow-y: scroll; 201 | margin-top: 35px; 202 | margin-bottom: 20px; 203 | padding-bottom: 30px; 204 | } 205 | 206 | .selecting-users-label { 207 | margin-bottom: 10px; 208 | position: fixed; 209 | left: 193px; 210 | margin-top: 5px; 211 | top: 130px; 212 | } 213 | 214 | .unselecting-users-label { 215 | margin-bottom: 10px; 216 | position: fixed; 217 | left: 184px; 218 | margin-top: 5px; 219 | top: 318px; 220 | } 221 | 222 | .search-list-item { 223 | font-size: 16px; 224 | display: flex; 225 | position: relative; 226 | bottom: 5px; 227 | left: 17%; 228 | padding-bottom: 4px; 229 | border-radius: 20px; 230 | } 231 | 232 | .search-list-item:hover { 233 | cursor: pointer; 234 | // background: #f5f5ef; 235 | font-weight:bolder; 236 | } 237 | 238 | .search-list-item img { 239 | height: 40px !important; 240 | width: 40px !important; 241 | position: relative; 242 | top: 15px; 243 | } 244 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage.css.scss: -------------------------------------------------------------------------------- 1 | body { 2 | overflow-x: hidden; 3 | overflow-y: hidden; 4 | } 5 | 6 | 7 | .homepage { 8 | display: flex; 9 | font-family: 'Nunito', sans-serif; 10 | } 11 | 12 | 13 | .left-col { 14 | display: flex; 15 | flex-direction: column; 16 | height: auto; 17 | background-color: #4d394b; 18 | color: gray; 19 | width: 220px; 20 | height: 100vh; 21 | position: fixed; 22 | } 23 | // display: flex; 24 | // flex-direction: column; 25 | // height: auto; 26 | // background-color: #4d394b; 27 | // color: gray; 28 | // width: 220px; 29 | // height: 100vh; 30 | 31 | .col-header { 32 | height: 60px; 33 | } 34 | 35 | .selected-header { 36 | height: 61px; 37 | position: absolute; 38 | position: absolute; 39 | left: 220px; 40 | top: 0px; 41 | width: 100%; 42 | height: 61px; 43 | background: white; 44 | border-bottom: 1px solid; 45 | z-index: 1; 46 | } 47 | 48 | .selected-channel { 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: flex-end; 52 | width: 100%; 53 | } 54 | 55 | .header-message-list { 56 | justify-content: space-between; 57 | } 58 | 59 | .selected-channel-header { 60 | } 61 | 62 | .selected-header-text { 63 | padding-top: 20px; 64 | padding-left: 5px; 65 | } 66 | -------------------------------------------------------------------------------- /app/assets/stylesheets/landing_page.css.scss: -------------------------------------------------------------------------------- 1 | #landing-text { 2 | display: flex; 3 | margin-top: 551px; 4 | margin-left: 25px; 5 | font-size: 56px; 6 | color: black; 7 | font-family: 'Nunito', sans-serif; 8 | z-index: 3; 9 | position: fixed; 10 | bottom: 40px; 11 | left: 40px; 12 | } 13 | 14 | 15 | 16 | .landing-block { 17 | display: flex; 18 | flex-direction: row; 19 | font-family: 'Merriweather', serif; 20 | position: relative; 21 | top: 100px; 22 | left: 50px; 23 | max-width: 1000px; 24 | } 25 | 26 | .overall-description { 27 | // font-size: 32px; 28 | color: #b92e2e; 29 | position: relative; 30 | bottom: 15px; 31 | // left: 50px; 32 | font-family: 'Poiret One', cursive; 33 | font-size: 28px; 34 | } 35 | 36 | 37 | #rwb-background { 38 | background-image: image-url("rwb-stars2.jpg"); 39 | // height: 100vh; 40 | // width: 100vw; 41 | width: 100%; 42 | height: 100%; 43 | opacity: .040; 44 | position:absolute; 45 | } 46 | 47 | #background-div { 48 | background-image: image-url("detailed-wh.png"); 49 | background-size: 500px 350px; 50 | font-family: 'Nunito', sans-serif; 51 | // height: 100vh; 52 | background-repeat: no-repeat; 53 | height: 440px; 54 | width: 900px; 55 | left: 69px; 56 | font-family: 'Nunito', sans-serif; 57 | background-repeat:no-repeat; 58 | height: 440px; 59 | width: 900px; 60 | opacity: 1 !important; 61 | } 62 | 63 | 64 | .landing-intro { 65 | display: flex; 66 | flex-direction: column; 67 | font-size: 20px; 68 | // z-index 3; 69 | opacity: 1; 70 | width: 450px; 71 | position: relative; 72 | top: 74px; 73 | left: 10px; 74 | } 75 | 76 | .landing-intro .intro-name { 77 | font-size: 62px; 78 | color: #b92e2e; 79 | } 80 | 81 | .landing-intro .intro-description { 82 | font-size: 22px; 83 | color: #272992; 84 | margin-top: 20px; 85 | line-height: 30px; 86 | } 87 | 88 | html { 89 | height: 100vh; 90 | } 91 | 92 | 93 | .nav { 94 | display: flex; 95 | flex-direction: row-reverse; 96 | border-bottom: solid; 97 | background-color: #1e297b; 98 | // opacity: .5; 99 | } 100 | 101 | .auth { 102 | margin: 10px; 103 | background-color: Transparent; 104 | background-repeat:no-repeat; 105 | border: none; 106 | overflow: hidden; 107 | cursor:pointer; 108 | font-size: 24px; 109 | font-family: 'Nunito', sans-serif; 110 | color: white; 111 | opacity: .99; 112 | } 113 | 114 | .login-form-container { 115 | font-size: 18px; 116 | } 117 | 118 | .submit { 119 | margin-top: 10px; 120 | margin-right: 20px; 121 | background-color: #2ab27b; 122 | background-repeat: #2ab27b; 123 | border: none; 124 | overflow: hidden; 125 | outline: none; 126 | cursor: pointer; 127 | font-size: 18px; 128 | color: white; 129 | opacity: .99; 130 | color: white; 131 | width: 407px; 132 | justify-content: center; 133 | font-family: 'Nunito', sans-serif; 134 | height: 38px; 135 | } 136 | 137 | .auth-input { 138 | width: 400px; 139 | height: 30px; 140 | margin-top: 15px; 141 | color: lightgray; 142 | font-size: 18px; 143 | font-family: 'Nunito', sans-serif; 144 | } 145 | 146 | .auth-input:focus{ 147 | color: black; 148 | } 149 | 150 | .entry-message { 151 | font-size: 14px; 152 | font-family: 'Nunito', sans-serif; 153 | } 154 | 155 | .entry-greeting { 156 | margin-bottom: 6px; 157 | font-size: 24px; 158 | font-family: 'Nunito', sans-serif; 159 | } 160 | 161 | .whole-greeting { 162 | justify-content: center; 163 | text-align: center; 164 | } 165 | -------------------------------------------------------------------------------- /app/assets/stylesheets/leftcol.css.scss: -------------------------------------------------------------------------------- 1 | 2 | .col-header h4 { 3 | color: white; 4 | font-size: 28px; 5 | font-style: oblique; 6 | } 7 | 8 | .col-header { 9 | display: flex; 10 | flex-direction: row; 11 | margin-bottom: 10px; 12 | position: relative; 13 | left: 13px; 14 | top: 6px; 15 | } 16 | 17 | .current-user-greeting { 18 | font-size: 13px; 19 | color: #a291a0; 20 | display: flex; 21 | } 22 | 23 | .logout { 24 | outline: none; 25 | border: 1px solid; 26 | background: #4d394b; 27 | color: #aa9aa8; 28 | border-radius: 5px; 29 | font-family: 'Nunito', sans-serif; 30 | font-size: 14px; 31 | height: 25px; 32 | align-items: center; 33 | position: relative; 34 | left: 22px; 35 | top: 3px; 36 | justify-content: center; 37 | padding-bottom: 6px; 38 | } 39 | 40 | .fa-sign-out:hover { 41 | cursor: pointer; 42 | font-size: 20px; 43 | 44 | } 45 | 46 | .fa-sign-out { 47 | color: white; 48 | position: fixed; 49 | left: 197px; 50 | top: 13px; 51 | } 52 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | -------------------------------------------------------------------------------- /app/assets/stylesheets/selected_input.css.scss: -------------------------------------------------------------------------------- 1 | .selected-input-div { 2 | display: flex; 3 | min-height: 21px; 4 | box-sizing: border-box; 5 | cursor: text; 6 | line-height: 1.42; 7 | outline: 0; 8 | overflow-y: auto; 9 | padding: 12px 15px; 10 | tab-size: 4; 11 | -moz-tab-size: 4; 12 | text-align: left; 13 | white-space: pre-wrap; 14 | word-wrap: break-word; 15 | padding-bottom: 20px; 16 | padding-top: 0px; 17 | overflow: hidden; 18 | position: absolute; 19 | z-index: 3; 20 | bottom: -9px; 21 | left: 224px; 22 | width: 78%; 23 | } 24 | // display: flex; 25 | // min-height: 21px; 26 | // box-sizing: border-box; 27 | // cursor: text; 28 | // line-height: 1.42; 29 | // outline: 0; 30 | // overflow-y: auto; 31 | // padding: 12px 15px; 32 | // tab-size: 4; 33 | // -moz-tab-size: 4; 34 | // text-align: left; 35 | // white-space: pre-wrap; 36 | // word-wrap: break-word; 37 | // padding-bottom: 20px; 38 | // padding-top: 0px; 39 | // overflow:hidden; 40 | 41 | .selected-input-form { 42 | height: 40px; 43 | width: 100%; 44 | border: none; 45 | input{ 46 | &:focus { 47 | border-color: #bbbdbf; 48 | outline: none; 49 | color: black 50 | } 51 | padding-left: 12px; 52 | width: 100%; 53 | height: 40px; 54 | border: 2px solid #E0E0E0; 55 | border-radius: 8px; 56 | font-size: 14px; 57 | font-family: 'Nunito', sans-serif; 58 | color: #a0a0a0 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/assets/stylesheets/selected_messages.css.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* ------- REPLY Input CSS */ 4 | 5 | .reply-input-div { 6 | display: flex; 7 | min-height: 21px; 8 | box-sizing: border-box; 9 | cursor: text; 10 | line-height: 1.42; 11 | outline: 0; 12 | overflow-y: scroll; 13 | padding: 12px 15px; 14 | /* overflow-x: scroll; */ 15 | tab-size: 4; 16 | -moz-tab-size: 4; 17 | text-align: left; 18 | white-space: pre-wrap; 19 | word-wrap: break-word; 20 | padding-bottom: 20px; 21 | padding-top: 0px; 22 | overflow: hidden; 23 | position: absolute; 24 | /* z-index: 3; */ 25 | width: 93%; 26 | bottom: 10px; 27 | } 28 | 29 | .reply-input-form { 30 | height: 40px; 31 | width: 98%; 32 | border: none; 33 | input{ 34 | &:focus { 35 | border-color: #bbbdbf; 36 | outline: none; 37 | color: black 38 | } 39 | padding-left: 12px; 40 | width: 100%; 41 | height: 40px; 42 | border: 2px solid #E0E0E0; 43 | border-radius: 8px; 44 | font-size: 14px; 45 | font-family: 'Nunito', sans-serif; 46 | color: #a0a0a0 47 | } 48 | } 49 | 50 | /* ------- end REPLY Input CSS */ 51 | 52 | 53 | 54 | 55 | /* ------- REPLY ITEM CSS */ 56 | 57 | @media only screen and (max-width: 870px) { 58 | .reply-item { 59 | width: 110px !important; 60 | // background: red; 61 | font-size: 12px; 62 | } 63 | 64 | .reply-author-name { 65 | font-size: 0px !important; 66 | // background: yellow; 67 | } 68 | 69 | .selected-message-item { 70 | width: 35% !important; 71 | } 72 | 73 | .all-replies { 74 | height: 55% !important; 75 | width: 110px !important; 76 | } 77 | 78 | .original-avatar { 79 | margin-left: 8px !important; 80 | } 81 | 82 | .original-thread { 83 | font-size: 12px !important; 84 | } 85 | 86 | .reply-content { 87 | width: 55px !important; 88 | } 89 | } 90 | 91 | 92 | .reply-item { 93 | width: 1% !important; 94 | padding-left: 8px !important; 95 | display: flex; 96 | flex-direction: row; 97 | padding-top: 4px; 98 | border-left: 3px solid transparent; 99 | } 100 | 101 | .reply-item:hover { 102 | // background: #f9f9f9; 103 | border-left: 3px solid #aaacad; 104 | } 105 | 106 | .reply-pic { 107 | width: 30px; 108 | height: 30px; 109 | border-radius: 15px; 110 | margin-right: 5px; 111 | } 112 | 113 | .reply-item-div { 114 | display: flex; 115 | flex-direction: column; 116 | } 117 | 118 | .reply-author-name { 119 | font-size: 12px; 120 | font-weight: bold; 121 | } 122 | 123 | 124 | .reply-time-stamp { 125 | font-size: 8px; 126 | color: gray; 127 | } 128 | 129 | .reply-content { 130 | width: 160px; 131 | position: relative; 132 | bottom: 14px; 133 | left: 3px; 134 | 135 | } 136 | 137 | /* END of Reply item CSS */ 138 | 139 | .all-replies { 140 | overflow-y: scroll; 141 | height: 63%; 142 | max-height: 376px; 143 | } 144 | 145 | @media only screen and (max-height: 640px) { 146 | .all-replies { 147 | height: 59% 148 | } 149 | } 150 | 151 | 152 | 153 | .small-reply-avatar { 154 | height: 20px; 155 | width: 20px; 156 | border-radius: 10px; 157 | } 158 | 159 | .reply-label { 160 | color: black; 161 | padding-left: 13px; 162 | } 163 | 164 | .reply-box { 165 | display: none; 166 | padding-top: 3px; 167 | padding-bottom: 3px; 168 | padding-left: 5px; 169 | border: 1px solid black; 170 | border-radius: 30px; 171 | width: 75px; 172 | position: relative; 173 | bottom: 5px; 174 | } 175 | 176 | .reply-thread-value { 177 | position: relative; 178 | left: 3%; 179 | font-size: 30px; 180 | color: #e2d6d6; 181 | margin-top: 10px; 182 | } 183 | 184 | @media only screen and (min-width: 960px) { 185 | .reply-thread-value { 186 | left: 10%; 187 | } 188 | } 189 | 190 | @media only screen and (min-width: 1050px) { 191 | .reply-thread-value { 192 | left: 13%; 193 | } 194 | } 195 | 196 | @media only screen and (min-width: 1100px) { 197 | .reply-thread-value { 198 | left: 16%; 199 | } 200 | } 201 | 202 | 203 | 204 | .original-avatar { 205 | height: 35px; 206 | width: 35px; 207 | margin-right: 5px; 208 | border-radius: 15px; 209 | margin-left: 20px; 210 | } 211 | 212 | 213 | .original-thread { 214 | font-weight: 100; 215 | border-bottom: solid 2px #4d3a4b; 216 | padding-bottom: 20px; 217 | padding-top: 5px; 218 | padding-left: 16px; 219 | font-weight: 100; 220 | font-size: 16px; 221 | display: flex; 222 | justify-content: space-between; 223 | padding-top: 16px; 224 | background: #f3eeee; 225 | border-radius: 8px; 226 | margin-bottom: 12px; 227 | } 228 | 229 | .reply-box:hover { 230 | cursor: pointer; 231 | } 232 | 233 | .replies-label { 234 | position: relative; 235 | bottom: 3px; 236 | } 237 | 238 | .replies-box { 239 | display: block; 240 | padding-top: 3px; 241 | // padding-bottom: 3px; 242 | padding-left: 5px; 243 | border: 1px solid gray; 244 | border-radius: 30px; 245 | width: 125px; 246 | position: relative; 247 | bottom: 14px; 248 | } 249 | 250 | .one-reply-box { 251 | display: block; 252 | padding-top: 3px; 253 | // padding-bottom: 3px; 254 | padding-left: 5px; 255 | border: 1px solid gray; 256 | border-radius: 30px; 257 | width: 90px; 258 | position: relative; 259 | bottom: 14px; 260 | } 261 | 262 | .one-reply-box:hover { 263 | cursor: pointer; 264 | } 265 | 266 | 267 | .replies-box:hover { 268 | cursor: pointer; 269 | } 270 | 271 | .selected-message-item:hover .reply-box { 272 | display: block; 273 | } 274 | 275 | .original-message-container { 276 | align-items: center; 277 | } 278 | 279 | .big { 280 | position: absolute; 281 | top: 69px; 282 | height: 77%; 283 | width: 400px; 284 | right: 15px; 285 | width: 23%; 286 | border-radius: 18px; 287 | border: solid 5px #4d3a4b; 288 | background: white; 289 | } 290 | 291 | @media only screen and (max-width: 870px) { 292 | .big { 293 | width: 130px; 294 | } 295 | } 296 | 297 | .selected-message-item { 298 | background-color: white; 299 | /* height: 46px; */ 300 | margin-left: 27px; 301 | margin-top: 5px; 302 | display: flex; 303 | flex-direction: row; 304 | width: 47%; 305 | 306 | -moz-transition: padding-bottom .15s ease-in; 307 | -o-transition: padding-bottom .15s ease-in; 308 | -webkit-transition: padding-bottom .15s ease-in; 309 | transition: padding-bottom .15s ease-in; 310 | } 311 | 312 | .selected-message-item:hover { 313 | background: #f9f9f9; 314 | padding-bottom:10px; 315 | } 316 | 317 | 318 | 319 | .reply-offer { 320 | font-size: 10px; 321 | } 322 | 323 | .selected-message-item-div:hover .reply-box { 324 | display: block; 325 | } 326 | 327 | .selected-message-list { 328 | background-color: white; 329 | height: 45px; 330 | } 331 | 332 | .unordered-message-list { 333 | overflow: scroll; 334 | max-height: calc(100% - 124px); 335 | white-space: pre-wrap; 336 | padding-top: 63px; 337 | position: absolute; 338 | top: 14px; 339 | left: 217px; 340 | /* bottom: 00px; */ 341 | width: 100%; 342 | } 343 | 344 | 345 | // overflow: scroll; 346 | // max-height: 100%; 347 | // white-space: pre-wrap; 348 | // padding-top: 63px; 349 | 350 | .authorPic { 351 | height: 40px !important; 352 | width: 40px !important; 353 | border-radius: 3px; 354 | margin-right: 7px; 355 | } 356 | 357 | .search-list-item img { 358 | height: 40px !important; 359 | width: 40px !important; 360 | } 361 | 362 | .selected-message-item-div { 363 | display: flex; 364 | flex-direction: column; 365 | } 366 | 367 | .message-author-name { 368 | font-size: 12px; 369 | font-weight: bold; 370 | font-family: sans-serif; 371 | display: flex; 372 | flex-direction: row; 373 | } 374 | 375 | 376 | .message-content { 377 | font-size: 14px; 378 | font-style: oblique; 379 | margin-bottom: 0px; 380 | position: relative; 381 | bottom: 22px; 382 | margin-top: 10px; 383 | } 384 | -------------------------------------------------------------------------------- /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/channels_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ChannelsController < ApplicationController 2 | 3 | def index 4 | user_id = current_user.id 5 | user = User.find_by(id: user_id) 6 | @channels = user.channels 7 | render "api/channels/index" 8 | end 9 | 10 | def create 11 | @channel = Channel.new(name: params[:channel][:name], 12 | private:params[:channel][:secret]) 13 | user_ids = params[:user_ids] 14 | if @channel.save 15 | user_ids.each { |user_id| Membership.create(channel_id: @channel.id, user_id: user_id) } 16 | render "api/channels/show" 17 | else 18 | render json: @channel.errors.full_messages, status: 422 19 | end 20 | end 21 | 22 | def show 23 | 24 | @channel = Channel.find_by(id: channel_params[:channel]) 25 | if @channel 26 | 27 | render "api/channels/show" 28 | else 29 | render json: @channel.errors.full_messages, status: 422 30 | end 31 | end 32 | 33 | def destroy 34 | 35 | end 36 | 37 | 38 | def channel_params 39 | params.require(:channel).permit(:name, :secret) 40 | # , :user_ids, :channel_id) 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/api/memberships_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::MembershipsController < ApplicationController 2 | 3 | def create 4 | 5 | @channel = Channel.create(name: params[:channel][:name], 6 | private:params[:channel][:secret]) 7 | arr = params[:user_ids].split(",") 8 | 9 | arr.each do |user_id| 10 | Membership.create(user_id: user_id, channel_id: @channel.id) 11 | end 12 | render "api/memberships/show" 13 | end 14 | 15 | 16 | 17 | 18 | 19 | end 20 | 21 | # user_id: user_id, channel_id: @channel.id) 22 | -------------------------------------------------------------------------------- /app/controllers/api/messages_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::MessagesController < ApplicationController 2 | 3 | def index 4 | channel = Channel.find_by(id: params[:channel]) 5 | @messages = channel.messages 6 | render "api/messages/index" 7 | end 8 | 9 | def show 10 | @message = Message.find(params[:id]) 11 | end 12 | 13 | def create 14 | @message = Message.new(message_params) 15 | channel = @message.channel 16 | if @message.save 17 | Pusher.trigger('channel_' + channel.id.to_s, 'message_published', {}) 18 | render "api/messages/show" 19 | else 20 | render json: @message.errors.full_messages, status: 422 21 | end 22 | 23 | end 24 | 25 | def update 26 | @message = Message.find_by(id: params[:id]) 27 | if @message.update_attributes(message_params) 28 | render "api/messages/show" 29 | else 30 | render json: @message.errors.full_messages, status: 422 31 | end 32 | end 33 | 34 | def destroy 35 | @message = Message.find_by(id: params[:message_id]) 36 | if @message 37 | @message.destroy 38 | render json: @message.id 39 | else 40 | render( 41 | json: ["Message not found"], 42 | status: 404 43 | ) 44 | end 45 | end 46 | 47 | def message_params 48 | params.require(:message).permit(:content, :user_id, :channel_id) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/controllers/api/replies_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::RepliesController < ApplicationController 2 | 3 | 4 | def create 5 | @reply = Reply.new(reply_params) 6 | @reply.user_id = current_user.id 7 | if @reply.save 8 | @message = Message.find(reply_params['message_id'].to_i) 9 | render "api/messages/show", status: 200 10 | else 11 | render json: ["can't be added"], status: 422 12 | end 13 | end 14 | 15 | 16 | def index 17 | message = Message.find_by(id: params[:message_id]) 18 | @replies = message.replies 19 | render "api/replies/index" 20 | end 21 | 22 | 23 | def reply_params 24 | params.require(:reply).permit(:content, :message_id, :current_user_avatar) 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials( 5 | params[:user][:username], 6 | params[:user][:password] 7 | ) 8 | 9 | if @user 10 | login(@user) 11 | render "api/users/show" 12 | else 13 | 14 | render( 15 | json: ["Invalid username/password combination"], 16 | status: 401 17 | ) 18 | end 19 | end 20 | 21 | def destroy 22 | @user = current_user 23 | if @user 24 | logout 25 | render "api/users/show" 26 | else 27 | render( 28 | json: ["Nobody signed in"], 29 | status: 404 30 | ) 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | @user = User.new(user_params) 5 | 6 | if @user.save 7 | login(@user) 8 | render "api/users/show" 9 | else 10 | render json: @user.errors.full_messages, status: 422 11 | end 12 | end 13 | 14 | def index 15 | 16 | @users = User.all 17 | render "api/users/index" 18 | end 19 | 20 | private 21 | 22 | def user_params 23 | params.require(:user).permit(:username, :password) 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | 3 | ### UNCOMMENT!!! 4 | # protect_from_forgery with: :exception 5 | helper_method :current_user, :logged_in? 6 | 7 | private 8 | 9 | def current_user 10 | return nil unless session[:session_token] 11 | @current_user ||= User.find_by(session_token: session[:session_token]) 12 | end 13 | 14 | def logged_in? 15 | !!current_user 16 | end 17 | 18 | def login(user) 19 | user.reset_session_token! 20 | session[:session_token] = user.session_token 21 | @current_user = user 22 | end 23 | 24 | def logout 25 | current_user.reset_session_token! 26 | session[:session_token] = nil 27 | @current_user = nil 28 | end 29 | 30 | def require_logged_in 31 | render json: {base: ['invalid credentials']}, status: 401 if !current_user 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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/channel.rb: -------------------------------------------------------------------------------- 1 | class Channel < ApplicationRecord 2 | validates :name, presence: true 3 | has_many :memberships 4 | has_many :messages 5 | has_many :users, through: :memberships 6 | # has_many :notifications 7 | end 8 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/membership.rb: -------------------------------------------------------------------------------- 1 | class Membership < ApplicationRecord 2 | validates :user_id, :channel_id, presence: true 3 | belongs_to :user 4 | belongs_to :channel 5 | 6 | # validates uniqueness: { scope: [:user_id, :channel_id] } 7 | # validates :user_id, uniqueness: { scope: [:channel_id] } 8 | end 9 | -------------------------------------------------------------------------------- /app/models/message.rb: -------------------------------------------------------------------------------- 1 | class Message < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :channel 4 | has_many :replies 5 | 6 | # def replies 7 | # Reply.find_by(message_id: == self.id) 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/models/reply.rb: -------------------------------------------------------------------------------- 1 | class Reply < ApplicationRecord 2 | validates :user_id, :message_id, presence: true 3 | 4 | belongs_to :user 5 | belongs_to :message 6 | end 7 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | 3 | attr_reader :password 4 | 5 | validates :username, :password_digest, :session_token, presence: true 6 | validates :username, uniqueness: true 7 | validates :password, length: {minimum: 6}, allow_nil: :true 8 | 9 | after_initialize :ensure_session_token 10 | before_validation :ensure_session_token_uniqueness 11 | 12 | has_many :memberships 13 | has_many :channels, through: :memberships 14 | has_many :messages 15 | has_many :replies 16 | 17 | has_attached_file :avatar 18 | validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\Z/ 19 | 20 | 21 | def password= password 22 | self.password_digest = BCrypt::Password.create(password) 23 | @password = password 24 | end 25 | 26 | def self.find_by_credentials username, password 27 | user = User.find_by(username: username) 28 | return nil unless user 29 | user.password_is?(password) ? user : nil 30 | end 31 | 32 | def password_is? password 33 | BCrypt::Password.new(self.password_digest).is_password?(password) 34 | end 35 | 36 | def reset_session_token! 37 | self.session_token = new_session_token 38 | ensure_session_token_uniqueness 39 | self.save 40 | self.session_token 41 | end 42 | 43 | private 44 | 45 | def ensure_session_token 46 | self.session_token ||= new_session_token 47 | end 48 | 49 | def new_session_token 50 | SecureRandom.base64 51 | end 52 | 53 | def ensure_session_token_uniqueness 54 | while User.find_by(session_token: self.session_token) 55 | self.session_token = new_session_token 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /app/views/api/channels/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | # json.arra! @channels, :name, :private, :id 2 | 3 | @channels.each do |channel| 4 | json.set! channel.id do 5 | json.extract! channel, :name, :private, :id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/channels/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @channel, :name, :private, :id 2 | -------------------------------------------------------------------------------- /app/views/api/memberships/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @channel, :name, :private, :id 2 | -------------------------------------------------------------------------------- /app/views/api/messages/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @messages.each do |message| 2 | json.set! message.id do 3 | json.extract! message, :content, :user_id, :channel_id 4 | json.count message.replies.length 5 | 6 | json.replies message.replies.each do |reply| 7 | json.avatar reply.user.avatar 8 | json.reply_content reply.content 9 | json.reply_username reply.user.username 10 | 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/api/messages/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.extract! @message, :id, :content, :channel_id, :user_id 3 | json.count @message.replies.length 4 | json.authorPic @message.user.avatar 5 | json.message_id @message.id 6 | json.replies @message.replies.each do |reply| 7 | json.reply_content reply.content 8 | json.reply_username reply.user.username 9 | json.avatar reply.user.avatar 10 | end 11 | 12 | 13 | # @messages.each do |message| 14 | # json.set! message.id do 15 | # json.extract! message, :content, :user_id, :channel_id 16 | # json.count message.replies.length 17 | # 18 | # json.replies message.replies.each do |reply| 19 | # json.avatar reply.user.avatar 20 | # json.reply_content reply.content 21 | # json.reply_username reply.user.username 22 | # 23 | # end 24 | # end 25 | # end 26 | 27 | # @messages.each do |message| 28 | # json.set! message.id do 29 | # json.extract! message, :content, :user_id, :channel_id 30 | # end 31 | # end 32 | -------------------------------------------------------------------------------- /app/views/api/replies/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @replies.each do |reply| 2 | json.set! reply.id do 3 | json.extract! reply, :content, :user_id, :message_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/replies/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @reply, :id, :content, :user_id, :message_id 2 | 3 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :username 2 | 3 | json.avatar_url asset_path(user.avatar.url) 4 | -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @users.each do |user| 2 | json.set! user.id do 3 | json.extract! user, :username, :id 4 | # 5 | json.avatar_url asset_path(user.avatar.url) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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 | Slacker 5 | <%= csrf_meta_tags %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <%= stylesheet_link_tag 'application', media: 'all' %> 17 | <%= javascript_include_tag 'application' %> 18 | <%= favicon_link_tag 'favicon.ico' %> 19 | 20 | 21 | 22 | 23 | <%= yield %> 24 | 25 | 26 | -------------------------------------------------------------------------------- /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 | 14 |
15 | -------------------------------------------------------------------------------- /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 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /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 SlackClone 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 | 15 | 16 | 17 | 18 | config.paperclip_defaults = { 19 | :storage => :s3, 20 | :s3_credentials => { 21 | :bucket => ENV["s3_bucket"], 22 | :access_key_id => ENV["s3_access_key_id"], 23 | :secret_access_key => ENV["s3_secret_access_key"], 24 | s3_host_name: "s3-#{ENV['s3_region']}.amazonaws.com", 25 | :s3_region => ENV["s3_region"], 26 | :url => ":s3_host_name" 27 | } 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /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: SlackClone_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: SlackClone 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: SlackClone_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: SlackClone_production 84 | username: SlackClone 85 | password: <%= ENV['SLACKCLONE_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 = "SlackClone_#{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 Guide for Upgrading Ruby on Rails for more info on each option. 6 | 7 | Rails.application.config.raise_on_unfiltered_parameters = true 8 | 9 | # Enable per-form CSRF tokens. Previous versions had false. 10 | Rails.application.config.action_controller.per_form_csrf_tokens = true 11 | 12 | # Enable origin-checking CSRF mitigation. Previous versions had false. 13 | Rails.application.config.action_controller.forgery_protection_origin_check = true 14 | 15 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 16 | # Previous versions had false. 17 | ActiveSupport.to_time_preserves_timezone = true 18 | 19 | # Require `belongs_to` associations by default. Previous versions had false. 20 | Rails.application.config.active_record.belongs_to_required_by_default = true 21 | 22 | # Do not halt callback chains when a callback returns false. Previous versions had true. 23 | ActiveSupport.halt_callback_chains_on_return_false = false 24 | 25 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 26 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 27 | -------------------------------------------------------------------------------- /config/initializers/pusher.rb: -------------------------------------------------------------------------------- 1 | require 'pusher' 2 | 3 | Pusher.app_id = '360472' 4 | Pusher.key = '7ed0f023347152a1d1c7' 5 | Pusher.secret = 'b26e9542281ee8395734' 6 | Pusher.cluster = 'us2' 7 | Pusher.logger = Rails.logger 8 | Pusher.encrypted = true 9 | -------------------------------------------------------------------------------- /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: '_SlackClone_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 | namespace :api, defaults: {format: :json} do 3 | resources :channels, only: [:index, :show, :create, :update, :delete] 4 | resources :messages, only: [:create, :index, :update, :delete, :show] 5 | resources :users, only: [:create, :update, :delete, :index] 6 | resources :memberships, only: [:create] 7 | resource :session, only: [:create, :destroy] 8 | resources :replies, only: [:create, :destroy] 9 | end 10 | 11 | root "static_pages#root" 12 | end 13 | -------------------------------------------------------------------------------- /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: 1c1cff33cd7c58649f7ee1644edc2d133d063c600a1cdb0352de5b13ba92b0decbe91820a7c581e3b3835bf4ec3e3bd7c49ceb1d5d2ce2c7c49535d1dfa5cbeb 15 | 16 | test: 17 | secret_key_base: 6b19b41ae9e035f72afd2f9ae126a6a9930f8449c6d40431c073f904c6aa2055bbf8f302d687e3b1dd26f3d0eabaa25506b486a544385605d225a5aa21fd6498 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/20170620135604_create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMessages < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :messages do |t| 4 | t.integer :user_id, null: false 5 | t.integer :channel_id, null: false 6 | t.text :content, null: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | 12 | end 13 | 14 | 15 | # t.string :username 16 | # t.string :email 17 | # password_digest :string 18 | # session_token :string 19 | -------------------------------------------------------------------------------- /db/migrate/20170620140138_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 :email, null: false 6 | t.string :password_digest, null: false 7 | t.string :session_token, null: false 8 | 9 | t.timestamps 10 | 11 | end 12 | add_index :users, :username, unique: true 13 | add_index :users, :email, unique: true 14 | add_index :users, :session_token, unique: true 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20170620140145_create_channels.rb: -------------------------------------------------------------------------------- 1 | class CreateChannels < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :channels do |t| 4 | t.string :name, null: false 5 | t.boolean :private, default: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170620140209_create_memberships.rb: -------------------------------------------------------------------------------- 1 | class CreateMemberships < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :memberships do |t| 4 | t.integer :user_id, null: false 5 | t.integer :channel_id, null: false 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20170621210151_remove_email_from_users.rb: -------------------------------------------------------------------------------- 1 | class RemoveEmailFromUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :users, :email 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170627144802_add_attachment_avatar_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentAvatarToUsers < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.attachment :avatar 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :users, :avatar 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170627203533_add_users_to_channels.rb: -------------------------------------------------------------------------------- 1 | class AddUsersToChannels < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :channels, :users, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171019204157_create_replies.rb: -------------------------------------------------------------------------------- 1 | class CreateReplies < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :replies do |t| 4 | t.integer :user_id, null: false 5 | t.integer :message_id, null: false 6 | t.string :content, null: false 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /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: 20171019204157) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "channels", force: :cascade do |t| 19 | t.string "name", null: false 20 | t.boolean "private", default: false 21 | t.datetime "created_at", null: false 22 | t.datetime "updated_at", null: false 23 | t.string "users" 24 | end 25 | 26 | create_table "memberships", force: :cascade do |t| 27 | t.integer "user_id", null: false 28 | t.integer "channel_id", null: false 29 | t.datetime "created_at", null: false 30 | t.datetime "updated_at", null: false 31 | end 32 | 33 | create_table "messages", force: :cascade do |t| 34 | t.integer "user_id", null: false 35 | t.integer "channel_id", null: false 36 | t.text "content", null: false 37 | t.datetime "created_at", null: false 38 | t.datetime "updated_at", null: false 39 | end 40 | 41 | create_table "replies", force: :cascade do |t| 42 | t.integer "user_id", null: false 43 | t.integer "message_id", null: false 44 | t.string "content", null: false 45 | t.datetime "created_at", null: false 46 | t.datetime "updated_at", null: false 47 | end 48 | 49 | create_table "users", force: :cascade do |t| 50 | t.string "username", null: false 51 | t.string "password_digest", null: false 52 | t.string "session_token", null: false 53 | t.datetime "created_at", null: false 54 | t.datetime "updated_at", null: false 55 | t.string "avatar_file_name" 56 | t.string "avatar_content_type" 57 | t.integer "avatar_file_size" 58 | t.datetime "avatar_updated_at" 59 | t.index ["session_token"], name: "index_users_on_session_token", unique: true, using: :btree 60 | t.index ["username"], name: "index_users_on_username", unique: true, using: :btree 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | #### Root 6 | - `GET /` - load Slacker application 7 | 8 | ## JSON API 9 | 10 | ### Session 11 | - `POST /api/session` -log in 12 | - `DELETE /api/session` - log out 13 | 14 | ### Users 15 | - `GET /api/users` - retrieve all users 16 | - `GET /api/channel/:channel_id/users` - retrieve all users of a channel 17 | - `POST /api/users` - create a user 18 | - `PATCH /api/users/:id` - update a user 19 | - `DELETE /api/users/:id` - delete a user 20 | 21 | 22 | ### Channels 23 | - `GET /api/channels` - retrieve all channels 24 | - `POST /api/channels` - create a channel 25 | - `PATCH /api/channels/:id` - update a channel 26 | - `DELETE /api/channels/:id` - delete a channel 27 | - `GET /api/users/:user_id/channels` - retrieve channels for a user 28 | 29 | ### Messages 30 | - `GET /api/messages` - retrieve all messages 31 | - `GET /api/channels/:channel_id/messages` - retrieve all messages of a channel 32 | - `POST /api/channels/:channel_id/messages/` - create a message 33 | - `PATCH /api/messages/:id` - update a message 34 | - `DELETE /api/messages/:id` - delete a message 35 | 36 | 39 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | # Component Hierarchy: 2 | 3 | 4 | ### Before logging in: 5 | - LandingPageContainer 6 | - NavBarContainer 7 | * NavBar 8 | * SessionForm 9 | 10 | 11 | ## After logging in: 12 | - Home Container 13 | - NavColumn: 14 | - Header Container: 15 | + Header: 16 | * display username 17 | * display 'Slacker' 18 | - ChannelListContainer: 19 | - ChannelList 20 | - NewChannelFormContainer 21 | * NewChannelForm 22 | * ChannelListItem 23 | - DirectMessageListContainer: 24 | - DirectMessageFormContainer 25 | - DirectMessageForm 26 | - DirectMessagesList 27 | * DirectMessageListItem 28 | - MainChannelContainer 29 | - ChannelHeaderContainer 30 | - ChannelHeader 31 | - ChannelMessagesContainer 32 | + ChannelMessages 33 | + ChannelMessageItemContainer 34 | * ChannelMessageItem 35 | - NewMessageContainer 36 | - NewMessageForm 37 | 38 | ## Routes 39 | 40 | |Path | Component | 41 | |-------|-------------| 42 | | "/sign-up" | "SessionFormContainer" | 43 | | "/log-in" | "SessionFormContainer" | 44 | | "/home" | "HomeContainer" | 45 | | "/channels/:channelId" | "MainChannelContainer" | 46 | | "/channels/new" | "NewChannelFormContainer" | 47 | | "/dms/new" | "NewChannelFormContainer" | 48 | -------------------------------------------------------------------------------- /docs/sample_state.md: -------------------------------------------------------------------------------- 1 | { 2 | modal : { 3 | type: login 4 | } 5 | current_channel : { 6 | name: "MenOfHonor123" 7 | } 8 | session: { 9 | currentUser: { 10 | user: null, 11 | errors: [] 12 | } 13 | } 14 | channels: { 15 | 1: { 16 | name: "presidentsofusa" 17 | private: false 18 | } 19 | 2: { 20 | name: "gloryteam" 21 | private: true 22 | } 23 | } 24 | users: { 25 | 1: { 26 | id: 1, 27 | username: "teddy", 28 | } 29 | 2: { 30 | id: 2, 31 | username: "george", 32 | } 33 | } 34 | messages: { 35 | 1: { 36 | id: 32, 37 | user_id: 1, 38 | channel_id: 2, 39 | content: 'america" 40 | } 41 | 2: { 42 | id: 45, 43 | user_id: 1, 44 | channel_id: 1, 45 | content: "canada stinks" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | # DB Schema 2 | 3 | ## User 4 | 5 | | column | type | details | 6 | |-----------------|----------|---------------------| 7 | | username | `string` | `unique` `presence` `indexed` | 8 | | email | `string` | `unique` `presence` `indexed` | 9 | | avatar_url | `string` | `string` | 10 | | password_digest | `string` | `unique` `presence` | 11 | | session_token | `string` | `unique` `presence` `indexed` | 12 | - User Model Associations 13 | - **Has Many** 14 | - Messages 15 | - Channels through Memberships 16 | 17 | 18 | ## Channel 19 | 20 | | column | type | details | 21 | |-------------|-----------|---------------------| 22 | | name | `string` | `unique` `presence` | 23 | | private | `boolean` | `presence` `default: false`| 24 | - Channel Model Associations 25 | - **Has Many** 26 | - Messages 27 | - Users through Memberships 28 | 29 | ## Message 30 | 31 | | column | type | details | 32 | |------------|-----------|------------| 33 | | user_id | `integer` | `presence` | 34 | | channel_id | `integer` | `presence` | 35 | | content | `string` | | 36 | 37 | - Message Model Associations 38 | - **Belongs To** 39 | - User 40 | - Channel 41 | 42 | 43 | ## Membership 44 | 45 | | column | type | details | 46 | |------------|-----------|------------| 47 | | user_id | `integer` | `presence` | 48 | | channel_id | `integer` | `presence` | 49 | - Membership Model Associations 50 | - **Belongs To** 51 | - User 52 | - Channel 53 | -------------------------------------------------------------------------------- /docs/wireframes/direct_message_form.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/docs/wireframes/direct_message_form.JPG -------------------------------------------------------------------------------- /docs/wireframes/homepage.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/docs/wireframes/homepage.JPG -------------------------------------------------------------------------------- /docs/wireframes/landing_page.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/docs/wireframes/landing_page.JPG -------------------------------------------------------------------------------- /docs/wireframes/new_channel_form.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/docs/wireframes/new_channel_form.JPG -------------------------------------------------------------------------------- /docs/wireframes/session_form.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/docs/wireframes/session_form.JPG -------------------------------------------------------------------------------- /frontend/actions/channel_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_USER_CHANNELS = "RECEIVE_USER_CHANNELS"; 2 | export const RECEIVE_USER_CHANNEL = "RECEIVE_USER_CHANNEL"; 3 | 4 | import * as APIUtil from '../util/channel_api_util'; 5 | // import { receiveErrors } from './session_actions'; 6 | 7 | export const receiveUserChannels = (channels) => { 8 | return({ 9 | type: RECEIVE_USER_CHANNELS, 10 | channels 11 | }); 12 | }; 13 | 14 | export const receiveUserChannel = (channel) => { 15 | return({ 16 | type: RECEIVE_USER_CHANNEL, 17 | channel 18 | }); 19 | }; 20 | 21 | 22 | export const fetchUserChannels = user => dispatch => { 23 | APIUtil.fetchUserChannels(user) 24 | .then(channels => (dispatch(receiveUserChannels(channels)))); 25 | // err => (dispatch(receiveErrors(err.responseJSON)))); 26 | }; 27 | 28 | 29 | 30 | export const fetchCurrentChannel = (channel_id) => dispatch => { 31 | return APIUtil.fetchCurrentChannel(channel_id); 32 | }; 33 | 34 | 35 | // channel has channel name and users 36 | -------------------------------------------------------------------------------- /frontend/actions/current_message_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_CURRENT_MESSAGE = "RECEIVE_CURRENT_MESSAGE"; 2 | 3 | export const receiveCurrentMessage = (message) => { 4 | return({ 5 | type: RECEIVE_CURRENT_MESSAGE, 6 | message 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /frontend/actions/membership_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/membership_api_util'; 2 | import { receiveUserChannel } from './channel_actions'; 3 | 4 | export const createMembership = membership => dispatch => { 5 | 6 | return APIUtil.createMembership(membership) 7 | .then(channel => (dispatch(receiveUserChannel(channel)))); 8 | // err => (dispatch(receiveErrors(err.responseJSON)))); 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/actions/message_actions.js: -------------------------------------------------------------------------------- 1 | import { receiveCurrentMessage } from './current_message_actions' 2 | export const RECEIVE_SELECTED_MESSAGES = "RECEIVE_SELECTED_MESSAGES"; 3 | export const RECEIVE_SELECTED_MESSAGE = "RECEIVE_SELECTED_MESSAGE"; 4 | 5 | import * as APIUtil from '../util/message_api_util'; 6 | // import { receiveErrors } from './session_actions'; 7 | 8 | export const receiveSelectedMessages = (messages) => { 9 | return ({ 10 | type: RECEIVE_SELECTED_MESSAGES, 11 | messages 12 | }); 13 | }; 14 | 15 | export const receiveSelectedMessage = message => { 16 | return ({ 17 | type: RECEIVE_SELECTED_MESSAGE, 18 | message 19 | }); 20 | }; 21 | 22 | export const createMessageReply = reply => dispatch => { 23 | return APIUtil.createMessageReply(reply) 24 | .then( message => { 25 | dispatch(receiveSelectedMessage(message)); 26 | dispatch(receiveCurrentMessage(message)); 27 | } 28 | ); 29 | } 30 | 31 | export const fetchSelectedMessages = channel_id => dispatch => { 32 | return APIUtil.fetchSelectedMessages(channel_id) 33 | .then(messages => dispatch(receiveSelectedMessages(messages))); 34 | }; 35 | // err => (dispatch(receiveErrors(err.responseJSON)))); 36 | 37 | export const fetchSelectedMessage = message_id => dispatch => { 38 | return APIUtil.fetchSelectedMessage(message_id) 39 | .then(message => dispatch(receiveSelectedMessage(message))) 40 | }; 41 | 42 | export const createSelectedMessage = message => dispatch => { 43 | return APIUtil.createSelectedMessage(message) 44 | .then( message => dispatch(receiveSelectedMessage(message))); 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | 2 | export const LOGIN = 'LOGIN'; 3 | export const SIGNUP = 'SIGNUP'; 4 | 5 | export const signupForm = () => ({ 6 | type: SIGNUP 7 | }); 8 | 9 | export const loginForm = () => ({ 10 | type: LOGIN 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/actions/reply_actions.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/frontend/actions/reply_actions.js -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | 4 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 5 | export const RECEIVE_SESSION_ERRORS = 'RECEIVE_ERRORS'; 6 | 7 | export const receiveCurrentUser = currentUser => ({ 8 | type: RECEIVE_CURRENT_USER, 9 | currentUser 10 | }); 11 | 12 | export const receiveErrors = errors => ({ 13 | type: RECEIVE_SESSION_ERRORS, 14 | errors 15 | }); 16 | 17 | export const signup = user => dispatch => ( 18 | APIUtil.signup(user).then(user => ( 19 | dispatch(receiveCurrentUser(user)) 20 | ), err => ( 21 | dispatch(receiveErrors(err.responseJSON)) 22 | )) 23 | ); 24 | 25 | export const login = user => dispatch => ( 26 | APIUtil.login(user).then(user => ( 27 | dispatch(receiveCurrentUser(user)) 28 | ), err => ( 29 | dispatch(receiveErrors(err.responseJSON)) 30 | )) 31 | ); 32 | 33 | export const logout = () => dispatch => ( 34 | APIUtil.logout().then(user => ( 35 | dispatch(receiveCurrentUser(null)) 36 | )) 37 | ); 38 | -------------------------------------------------------------------------------- /frontend/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_ALL_USERS = 'RECEIVE_ALL_USERS'; 2 | import * as APIUtil from '../util/user_api_util'; 3 | 4 | 5 | export const receiveAllUsers = (users) => { 6 | return({ 7 | type: RECEIVE_ALL_USERS, 8 | users 9 | }); 10 | }; 11 | 12 | export const fetchAllUsers = () => dispatch => { 13 | APIUtil.fetchAllUsers() 14 | .then((users) => (dispatch(receiveAllUsers(users)))); 15 | // err => (dispatch(receiveErrors(err.responseJSON)))); 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { 4 | Route, 5 | Redirect, 6 | Switch, 7 | Link, 8 | HashRouter 9 | } from 'react-router-dom'; 10 | 11 | import LandingPage from './landing_page/landing_page'; 12 | import HomePage from './homepage/homepage'; 13 | import GreetingContainer from './greeting/greeting_container'; 14 | import SessionFormContainer from './landing_page/session_form/session_form_container'; 15 | import { AuthRoute, ProtectedRoute } from '../util/route_util'; 16 | import SelectedChannel from './homepage/selected_channel/selected_channel'; 17 | 18 | const App = () => ( 19 |
20 |
21 | 22 | 23 |
24 |
25 | ); 26 | 27 | export default App; 28 | // 29 | 30 | //
31 | //
32 | // 33 | //
34 | //
35 | 36 | 37 | 38 | // 39 | // 40 | // 41 | // 42 | 43 | // 44 | //

Slack

45 | // 46 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const sessionLinks = () => ( 5 | 10 | ); 11 | 12 | const personalGreeting = (currentUser, logout) => ( 13 |
14 |

Hi, {currentUser.username}!

15 | 16 |
17 | ); 18 | 19 | const Greeting = ({ currentUser, logout }) => ( 20 | currentUser ? personalGreeting(currentUser, logout) : sessionLinks() 21 | ); 22 | 23 | export default Greeting; 24 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import Greeting from './greeting'; 4 | 5 | const mapStateToProps = ({ session }) => ({ 6 | currentUser: session.currentUser 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | logout: () => dispatch(logout()) 11 | }); 12 | 13 | export default connect( 14 | mapStateToProps, 15 | mapDispatchToProps 16 | )(Greeting); 17 | -------------------------------------------------------------------------------- /frontend/components/homepage/homepage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | import LeftCol from './leftcol/leftcol'; 5 | import SelectedChannel from './selected_channel/selected_channel'; 6 | 7 | const Homepage = (props) => { 8 | 9 | const channel_id = props.match.params.id; 10 | return( 11 |
12 | 13 | 14 |
15 | );}; 16 | 17 | export default Homepage; 18 | 19 | // 20 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channel_scroller.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ChannelListContainer from './channels/channel_list_container'; 3 | // import DirectMessageList from './direct_messages/direct_message_list'; 4 | 5 | class ChannelScroller extends React.Component { 6 | render(){ 7 | return( 8 |
9 | 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default ChannelScroller; 16 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/channel_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'react-modal'; 3 | 4 | import ChannelListItem from './channel_list_item'; 5 | import NewChannelFormContainer from './new_channel_form_container'; 6 | import NewDirectMessageFormContainer from './new_direct_message_form_container'; 7 | import DirectMessageList from './direct_message_list'; 8 | 9 | class ChannelList extends React.Component { 10 | constructor(props){ 11 | super(props); 12 | 13 | 14 | this.state = { isOpen: false }; 15 | this.handleClick = this.handleClick.bind(this); 16 | this.openModal = this.openModal.bind(this); 17 | this.closeModal = this.closeModal.bind(this); 18 | this.handleModal = this.handleModal.bind(this); 19 | } 20 | 21 | closeModal(e) { 22 | if (this.state.isOpen){ 23 | $('.transform').toggleClass('transform-active'); 24 | } 25 | // this.props.clearErrors(); 26 | let that = this; 27 | setTimeout(function(){ 28 | that.setState({ isOpen: false }); 29 | }, 300); 30 | } 31 | 32 | openModal(){ 33 | this.setState({ isOpen: true }); 34 | } 35 | 36 | handleModal(e){ 37 | // 38 | this.openModal(); 39 | } 40 | 41 | componentWillMount(){ 42 | Modal.setAppElement('body'); 43 | } 44 | 45 | 46 | handleClick() { 47 | 48 | } 49 | 50 | 51 | componentDidMount(){ 52 | 53 | this.props.fetchUserChannels(this.props.currentUser); 54 | this.props.fetchAllUsers(); 55 | } 56 | 57 | render(){ 58 | const customStyles = { 59 | overlay : { 60 | position : 'fixed', 61 | top : 0, 62 | left : 0, 63 | right : 0, 64 | bottom : 0, 65 | backgroundColor : 'rgba(400, 400, 500, 1)', 66 | zIndex: 4 67 | }, 68 | content : { 69 | top : '50%', 70 | left : '50%', 71 | right : 'auto', 72 | bottom : 'auto', 73 | marginRight : '-50%', 74 | transform : 'translate(-50%, -50%)', 75 | outline : 'none', 76 | border : '0px', 77 | } 78 | }; 79 | 80 | let allChannels = this.props.channels.map( 81 | (channel,idx) => { 82 | if (!channel.private) { 83 | return ; 84 | } 85 | }); 86 | 87 | let allDirectMessages = this.props.channels.map( 88 | (channel, idx) => { 89 | if (!!channel.private) { 90 | return ; 91 | } 92 | } 93 | ); 94 | 95 | // let allDirectMessages = this.props.channels.map( 96 | // (channel,idx) => { 97 | // if (channel.private === false){ 98 | // ; 99 | // } 100 | // }); 101 | 102 | 103 | return( 104 |
105 |
106 |
107 | 112 | 113 | 114 | 115 | 116 | CHANNELS 117 | 118 | 119 | 122 | 123 |
124 |
125 | 126 |
    127 |
    128 | {allChannels} 129 |
    130 |
131 | 132 | 133 | 134 |
135 | ); 136 | } 137 | } 138 | 139 | export default ChannelList; 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | // 152 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/channel_list_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchUserChannels, receiveUserChannels } from '../../../../../actions/channel_actions'; 3 | import { fetchAllUsers } from '../../../../../actions/user_actions'; 4 | 5 | import ChannelList from './channel_list'; 6 | 7 | const mapStateToProps = (state) => { 8 | 9 | return ({ 10 | channels: Object.keys(state.channels) 11 | .map(function (key) { return state.channels[key]; }), 12 | currentUser: state.session.currentUser 13 | }); 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return ({ 18 | fetchUserChannels: (user) => dispatch(fetchUserChannels(user)), 19 | fetchAllUsers: () => dispatch(fetchAllUsers()) 20 | }); 21 | }; 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(ChannelList); 27 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/channel_list_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, withRouter } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import { receiveCurrentMessage } from '../../../../../actions/current_message_actions'; 5 | 6 | 7 | // const ChannelListItem = ({ channel }) => { 8 | // 9 | // let pretext; 10 | // if (channel.private) { 11 | // pretext = 12 | // } else { 13 | // pretext = "#"; 14 | // } 15 | // return( 16 | //
  • 17 | // 18 | // {pretext}{channel.name} 19 | // 20 | //
  • 21 | // ); 22 | // }; 23 | // 24 | // export default ChannelListItem; 25 | 26 | 27 | 28 | 29 | class ChannelListItem extends React.Component { 30 | 31 | constructor(props){ 32 | super(props) 33 | 34 | } 35 | 36 | render() { 37 | let channel = this.props.channel; 38 | let pretext; 39 | if (channel.private) { 40 | pretext = 41 | } else { 42 | pretext = "#"; 43 | } 44 | 45 | return ( 46 |
  • this.props.receiveCurrentMessage({})} className="channel-list-item"> 47 | 48 | {pretext}{channel.name} 49 | 50 |
  • 51 | ); 52 | } 53 | }; 54 | 55 | const mapStateToProps = (state) => { 56 | return({}); 57 | }; 58 | 59 | 60 | const mapDispatchToProps = (dispatch) => { 61 | return ({ 62 | receiveCurrentMessage: (message) => dispatch(receiveCurrentMessage(message)) 63 | }); 64 | }; 65 | 66 | export default withRouter(connect( 67 | mapStateToProps, 68 | mapDispatchToProps 69 | )(ChannelListItem)); 70 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/channel_list_item_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { fetchChannelMessages } from '../../../../../actions/channel_actions'; 4 | import { receiveCurrentMessage } from '../../../../../actions/current_message_actions'; 5 | import ChannelListItem from './new_channel_form'; 6 | 7 | const mapStateToProps = () => { 8 | return({ 9 | 10 | }); 11 | }; 12 | 13 | 14 | const mapDispatchToProps = () => { 15 | return ({ 16 | fetchChannelMessages: (channel) => dispatch(fetchChannelMessages(channel)), 17 | receiveCurrentMessage: (message) => dispatch(receiveCurrentMessage(message)) 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/direct_message_item.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/frontend/components/homepage/leftcol/channel_scroller/channels/direct_message_item.jsx -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/direct_message_item_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { fetchChannelMessages } from '../../../../../actions/channel_actions'; 4 | import DirectMessageItem from './direct_message_item'; 5 | 6 | const mapStateToProps = () => { 7 | return({ 8 | 9 | }); 10 | }; 11 | 12 | 13 | const mapDispatchToProps = () => { 14 | return ({ 15 | fetchChannelMessages: (channel) => dispatch(fetchChannelMessages(channel)) 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/direct_message_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Modal from 'react-modal'; 3 | 4 | import ChannelListItem from './channel_list_item'; 5 | import NewDirectMessageFormContainer from './new_direct_message_form_container'; 6 | 7 | 8 | class DirectMessageList extends React.Component { 9 | constructor(props){ 10 | super(props); 11 | 12 | 13 | this.state = { isOpen: false }; 14 | this.handleClick = this.handleClick.bind(this); 15 | this.openModal = this.openModal.bind(this); 16 | this.closeModal = this.closeModal.bind(this); 17 | this.handleModal = this.handleModal.bind(this); 18 | } 19 | 20 | closeModal(e) { 21 | if (this.state.isOpen){ 22 | $('.transform').toggleClass('transform-active'); 23 | } 24 | // this.props.clearErrors(); 25 | let that = this; 26 | setTimeout(function(){ 27 | that.setState({ isOpen: false }); 28 | }, 300); 29 | } 30 | 31 | openModal(){ 32 | this.setState({ isOpen: true }); 33 | } 34 | 35 | handleModal(e){ 36 | // 37 | this.openModal(); 38 | } 39 | 40 | componentWillMount(){ 41 | Modal.setAppElement('body'); 42 | } 43 | 44 | 45 | handleClick() { 46 | 47 | } 48 | 49 | 50 | componentDidMount(){ 51 | 52 | // this.props.fetchUserChannels(this.props.currentUser); 53 | // this.props.fetchAllUsers(); 54 | } 55 | 56 | render(){ 57 | const customStyles = { 58 | overlay : { 59 | position : 'fixed', 60 | top : 0, 61 | left : 0, 62 | right : 0, 63 | bottom : 0, 64 | backgroundColor : 'rgba(400, 400, 500, 1)', 65 | zIndex: 4 66 | }, 67 | content : { 68 | top : '40%', 69 | left : '50%', 70 | right : 'auto', 71 | bottom : 'auto', 72 | marginRight : '-50%', 73 | transform : 'translate(-50%, -50%)', 74 | outline : 'none', 75 | border : '0px' 76 | } 77 | }; 78 | 79 | let allDirectMessages = this.props.allDirectMessages; 80 | 81 | return( 82 |
    83 | 84 | 85 | 90 | 91 | 92 | 93 |
    94 |
      95 |
      96 | 97 | DIRECT MESSAGES 98 | 99 | 100 | 104 | 105 |
      106 |
      107 | {allDirectMessages} 108 |
      109 |
    110 |
    111 |
    112 | ); 113 | } 114 | } 115 | 116 | export default DirectMessageList; 117 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/direct_message_list_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { createChannel } from '../../../../../actions/channel_actions'; 4 | import { createMembership } from '../../../../../actions/membership_actions'; 5 | import NewChannelForm from './new_channel_form'; 6 | 7 | 8 | const mapStateToProps = (state) => { 9 | // 10 | return { 11 | users: Object.keys(state.users).map( (key) => state.users[key] ) 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return ({ 17 | createMembership: (membership) => dispatch(createMembership(membership)) 18 | }); 19 | }; 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(NewChannelForm); 25 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/new_channel_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | import SelectedList from '../searches/selected_list'; 4 | import UnselectedList from '../searches/unselected_list'; 5 | 6 | class NewChannelForm extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | name: "Channel Name", 12 | secret: false, 13 | selected:[], 14 | unselected: [...this.props.users] 15 | }; 16 | 17 | this.handleSubmit = this.handleSubmit.bind(this); 18 | this.empty = this.empty.bind(this); 19 | this.moveUser = this.moveUser.bind(this); 20 | } 21 | 22 | // empty (type) { 23 | // return (e) => { 24 | // e.preventDefault(); 25 | // if (e.currentTarget.value === "Channel Name" || 26 | // e.currentTarget.value === "Filter by username"){ 27 | // this.setState({[type]:''}); 28 | // } 29 | // }; 30 | // } 31 | 32 | empty (type) { 33 | return (e) => { 34 | e.preventDefault(); 35 | if (e.currentTarget.value === "Channel Name" || 36 | e.currentTarget.value === "Filter by username"){ 37 | this.setState({[type]:''}); 38 | } 39 | }; 40 | } 41 | 42 | update(field) { 43 | return e => this.setState({ 44 | [field]: e.currentTarget.value 45 | }); 46 | } 47 | 48 | handleSubmit(e) { 49 | e.preventDefault(); 50 | const user_ids = this.state.selected.map( (user) => user.id ); 51 | this.setState({selected: user_ids}, 52 | ()=>{this.props.createMembership(this.state)}) 53 | this.props.closeModal() 54 | } 55 | 56 | moveUser(origin, destination) { 57 | return (clicked_user) => { 58 | return () => { 59 | const oldOrigin = this.state[origin]; 60 | const oldDestination = this.state[destination]; 61 | 62 | if (origin){ 63 | const newOrigin = oldOrigin.filter( (user) => user.id !== clicked_user.id ); 64 | const newDestination = [...oldDestination, clicked_user]; 65 | 66 | this.setState({ 67 | [origin]: newOrigin, 68 | [destination]: newDestination, 69 | }); 70 | } 71 | } 72 | } 73 | } 74 | 75 | render(){ 76 | return( 77 |
    78 |
    79 |
    80 | Select users and create a channel name 81 |
    82 | 83 |
    84 | 90 |
    91 | 92 | 93 | 94 |
    95 |
    96 | SELECTED USERS 97 |
    98 | 100 |
    101 | 102 |
    103 |
    104 | UNSELECTED USERS 105 |
    106 | 108 |
    109 | 110 | 111 | 113 |
    114 | 115 |
    116 | ); 117 | } 118 | 119 | } 120 | 121 | 122 | export default NewChannelForm; 123 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/new_channel_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { createChannel } from '../../../../../actions/channel_actions'; 4 | import { createMembership } from '../../../../../actions/membership_actions'; 5 | import NewChannelForm from './new_channel_form'; 6 | 7 | 8 | const mapStateToProps = (state) => { 9 | // 10 | return { 11 | users: Object.keys(state.users).map( (key) => state.users[key] ) 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return ({ 17 | createMembership: (membership) => dispatch(createMembership(membership)) 18 | }); 19 | }; 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(NewChannelForm); 25 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/new_direct_message_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | import SelectedList from '../searches/selected_list'; 4 | import UnselectedList from '../searches/unselected_list'; 5 | 6 | 7 | class NewDirectMessageForm extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | name: "", 13 | secret: true, 14 | selected:[], 15 | unselected: [...this.props.users] 16 | }; 17 | 18 | this.handleSubmit = this.handleSubmit.bind(this); 19 | this.empty = this.empty.bind(this); 20 | } 21 | 22 | // empty (type) { 23 | // return (e) => { 24 | // e.preventDefault(); 25 | // if (e.currentTarget.value === "Channel Name" || 26 | // e.currentTarget.value === "Filter by username"){ 27 | // this.setState({[type]:''}); 28 | // } 29 | // }; 30 | // } 31 | 32 | empty (type) { 33 | return (e) => { 34 | e.preventDefault(); 35 | if (e.currentTarget.value === "Filter by username"){ 36 | this.setState({[type]:''}); 37 | } 38 | }; 39 | } 40 | 41 | update(field) { 42 | return e => this.setState({ 43 | [field]: e.currentTarget.value 44 | }); 45 | } 46 | 47 | handleSubmit(e) { 48 | e.preventDefault(); 49 | const name_array = this.state.selected.map( (user) => user.username ) 50 | const name_string = name_array.toString(); 51 | 52 | const user_ids = this.state.selected.map( (user) => user.id ) 53 | this.setState({name: name_string, selected: user_ids}, 54 | ()=>{this.props.createMembership(this.state)} 55 | ); 56 | 57 | this.props.closeModal() 58 | } 59 | 60 | moveUser(origin, destination) { 61 | return (clicked_user) => { 62 | return () => { 63 | const oldOrigin = this.state[origin]; 64 | const oldDestination = this.state[destination]; 65 | 66 | if (origin){ 67 | const newOrigin = oldOrigin.filter( (user) => user.id !== clicked_user.id ); 68 | const newDestination = [...oldDestination, clicked_user]; 69 | 70 | this.setState({ 71 | [origin]: newOrigin, 72 | [destination]: newDestination, 73 | }); 74 | } 75 | } 76 | } 77 | } 78 | 79 | 80 | render(){ 81 | return( 82 | 83 |
    84 |
    85 | Select users to be in your private channel 86 |
    87 |
    88 | 89 |
    90 |
    91 | 92 |
    93 |
    94 | SELECTED USERS 95 |
    96 | 98 |
    99 | 100 |
    101 |
    102 | UNSELECTED USERS 103 |
    104 | 106 |
    107 | 108 | 109 | 111 |
    112 | 113 |
    114 | ); 115 | } 116 | 117 | } 118 | 119 | 120 | export default NewDirectMessageForm; 121 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/channels/new_direct_message_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { createChannel } from '../../../../../actions/channel_actions'; 4 | import { createMembership } from '../../../../../actions/membership_actions'; 5 | import NewDirectMessageForm from './new_direct_message_form'; 6 | 7 | 8 | const mapStateToProps = (state) => { 9 | // 10 | return { 11 | users: Object.keys(state.users).map( (key) => state.users[key] ) 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return ({ 17 | createMembership: (membership) => dispatch(createMembership(membership)) 18 | }); 19 | }; 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(NewDirectMessageForm); 25 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/searches/search_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SelectedList from './selected_list'; 3 | import UnselectedList from './unselected_list'; 4 | 5 | class SearchList extends React.Component { 6 | 7 | constructor(props){ 8 | super(props); 9 | 10 | this.state = { 11 | selected:[], 12 | unselected: [...this.props.users] 13 | }; 14 | } 15 | 16 | // this.state = { 17 | // name: "Channel Name", 18 | // usernames: "Filter by username", 19 | // secret: false 20 | // }; 21 | 22 | moveUser(origin, destination, clicked_user) { 23 | const oldOrigin = this.state[origin]; 24 | const oldDestination = this.state[destination]; 25 | 26 | if (origin){ 27 | const newOrigin = oldOrigin.filter( (user) => user.id !== clicked_user.id ); 28 | const newDestination = [...oldDestination, clicked_user]; 29 | 30 | this.setState({ 31 | [origin]: newOrigin, 32 | [destination]: newDestination, 33 | }); 34 | } 35 | } 36 | 37 | render(){ 38 | return( 39 |
    40 | 42 | 44 |
    45 | ); 46 | } 47 | } 48 | 49 | export default SearchList; 50 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/searches/search_list_container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import SearchList from './search_list'; 4 | 5 | const mapStateToProps = (state) => { 6 | return ({ 7 | users: Object.keys(state.users).map(function (key) { return state.users[key]; }) 8 | }); 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return ({ 13 | 14 | }); 15 | }; 16 | 17 | export default connect ( 18 | mapStateToProps, 19 | null 20 | )(SearchList); 21 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/searches/search_list_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SearchListItem = ({user, moveUser}) => { 4 | 5 | return ( 6 |
    7 |
  • 8 | 9 | {user.username} 10 |
  • 11 |
    12 | ); 13 | }; 14 | 15 | export default SearchListItem; 16 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/searches/selected_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchListItem from './search_list_item'; 3 | 4 | class SelectedList extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | render(props){ 10 | const moveUser = this.props.moveUser; 11 | 12 | const allSelectedUsers = this.props.selected.map( 13 | user => 14 | ); 15 | 16 | return ( 17 |
      18 | {allSelectedUsers} 19 |
    20 | ); 21 | } 22 | } 23 | 24 | export default SelectedList; 25 | 26 | 27 | // let allDirectMessages = this.props.channels.map( 28 | // (channel, idx) => { 29 | // if (!!channel.private) { 30 | // return ; 31 | // } 32 | // } 33 | // ); 34 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/channel_scroller/searches/unselected_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchListItem from './search_list_item'; 3 | 4 | class UnselectedList extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | render(props){ 10 | let moveUser = this.props.moveUser; 11 | let allUnselectedUsers = this.props.unselected.map( 12 | function (user, index){ 13 | return( 14 | 15 | ); 16 | } 17 | ); 18 | 19 | return( 20 |
      21 | {allUnselectedUsers} 22 |
    23 | ); 24 | 25 | } 26 | } 27 | 28 | export default UnselectedList; 29 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/header/col_header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | class ColHeader extends React.Component { 5 | 6 | constructor (props) { 7 | super(props); 8 | 9 | this.handleClick = this.handleClick.bind(this); 10 | } 11 | 12 | handleClick(e){ 13 | e.preventDefault(); 14 | debugger 15 | this.props.logout(); 16 | this.props.receiveCurrentMessage({}); 17 | this.props.history.push('/'); 18 | debugger 19 | } 20 | render(){ 21 | // let channel_id = this.props.channel_id; 22 | // let currentChannel = this.props.fetchCurrentChannel(channel_id); 23 | 24 | let channel_name = this.props.channel_name.name; 25 | let current_user = this.props.current_user; 26 | return ( 27 |
    28 |
    29 |

    slacker

    30 |
    31 |

    {current_user}

    32 |
    33 |
    34 | 37 |
    38 | ); 39 | } 40 | } 41 | 42 | export default ColHeader; 43 | 44 | 45 | 46 | // 49 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/header/col_header_container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { logout } from '../../../../actions/session_actions'; 4 | import { fetchCurrentChannel } from '../../../../actions/channel_actions'; 5 | import { receiveCurrentMessage } from '../../../../actions/current_message_actions'; 6 | import { withRouter } from 'react-router-dom'; 7 | 8 | import ColHeader from './col_header'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | let channel_name = ""; 12 | let channel_id = ""; 13 | if (state.channels[parseInt(ownProps.match.params.id)]) { 14 | channel_id = parseInt(ownProps.match.params.id); 15 | channel_name = state.channels[channel_id]; 16 | } 17 | 18 | return ( 19 | { 20 | channel_name: channel_name, 21 | current_user: state.session.currentUser.username 22 | } 23 | ); 24 | }; 25 | 26 | const mapDispatchToProps = (dispatch, ownProps) => { 27 | return({ 28 | logout: () => dispatch(logout()), 29 | fetchCurrentChannel: (channel_name) => dispatch(fetchCurrentChannel(channel_name)), 30 | receiveCurrentMessage: (message) => dispatch(receiveCurrentMessage(message)) 31 | }); 32 | }; 33 | 34 | export default withRouter(connect( 35 | mapStateToProps, 36 | mapDispatchToProps 37 | )(ColHeader)); 38 | -------------------------------------------------------------------------------- /frontend/components/homepage/leftcol/leftcol.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ColHeaderContainer from './header/col_header_container'; 4 | import ChannelScroller from './channel_scroller/channel_scroller'; 5 | 6 | const LeftCol = (props) => { 7 | 8 | return ( 9 |
    10 | 11 | 12 |
    13 | ); 14 | }; 15 | 16 | export default LeftCol; 17 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_channel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SelectedHeaderContainer from './selected_header/selected_header_container'; 4 | import SelectedMessageListContainer from './selected_messages/selected_message_list_container'; 5 | import SelectedInputContainer from './selected_input/selected_input_container'; 6 | import MessageReplies from './selected_messages/message_replies'; 7 | 8 | const SelectedChannel = (props, ownProps) => { 9 | 10 | const channel_id = props.channel_id; 11 | 12 | return ( 13 |
    14 |
    15 | 16 |
    17 | 18 | 19 | 20 |
    21 | ); 22 | }; 23 | 24 | export default SelectedChannel; 25 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_channel_container.jsx: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_header/selected_header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class SelectedHeader extends React.Component { 4 | constructor (props) { 5 | super(props); 6 | 7 | } 8 | 9 | render(){ 10 | let pretext; 11 | if (this.props.channel_name.private) { 12 | pretext = "@"; 13 | } else { 14 | pretext = "#"; 15 | } 16 | let channel_name = this.props.channel_name.name; 17 | return ( 18 |
    19 |
    20 |

    {pretext}{channel_name}

    21 | 22 |
    23 |
    24 | ); 25 | } 26 | } 27 | 28 | export default SelectedHeader; 29 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_header/selected_header_container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { logout } from '../../../../actions/session_actions'; 4 | import { fetchCurrentChannel } from '../../../../actions/channel_actions'; 5 | import { withRouter } from 'react-router-dom'; 6 | 7 | import SelectedHeader from './selected_header'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | 11 | let channel_name = ""; 12 | let channel_id = ""; 13 | if (state.channels[parseInt(ownProps.match.params.id)]) { 14 | channel_id = parseInt(ownProps.match.params.id); 15 | channel_name = state.channels[channel_id]; 16 | 17 | } 18 | 19 | return ( 20 | { 21 | channel_name: channel_name 22 | } 23 | ); 24 | }; 25 | 26 | const mapDispatchToProps = (dispatch, ownProps) => { 27 | return({ 28 | fetchCurrentChannel: (channel_name) => dispatch(fetchCurrentChannel(channel_name)) 29 | }); 30 | }; 31 | 32 | export default withRouter(connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(SelectedHeader)); 36 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_input/selected_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class SelectedInput extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = { 7 | message: "Message" 8 | }; 9 | 10 | this.handleSubmit = this.handleSubmit.bind(this); 11 | this.empty = this.empty.bind(this); 12 | } 13 | 14 | 15 | 16 | empty(type) { 17 | return (e) => { 18 | e.preventDefault(); 19 | if (e.currentTarget.value === "Message"){ 20 | this.setState({[type]:''}); 21 | } 22 | }; 23 | } 24 | 25 | update(field) { 26 | return e => this.setState({ 27 | [field]: e.currentTarget.value 28 | }); 29 | } 30 | 31 | handleSubmit(e){ 32 | e.preventDefault(); 33 | const message_obj = {content: this.state.message, user_id: this.props.user_id, channel_id: this.props.channel_id}; 34 | this.props.createSelectedMessage(message_obj); 35 | // 36 | this.setState({message:''}); 37 | } 38 | 39 | 40 | 41 | render(){ 42 | 43 | return( 44 |
    45 |
    46 | 51 | 52 |
    53 |
    54 | ); 55 | } 56 | } 57 | // 58 | 59 | export default SelectedInput; 60 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_input/selected_input_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SelectedInput from './selected_input'; 3 | import { createSelectedMessage } from '../../../../actions/message_actions'; 4 | 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | return { 8 | user_id: state.session.currentUser.id, 9 | channel_id: parseInt(ownProps.channel_id) 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | 15 | return ({ 16 | createSelectedMessage: (message) => dispatch(createSelectedMessage(message)) 17 | }); 18 | }; 19 | 20 | export default connect( 21 | mapStateToProps, 22 | mapDispatchToProps 23 | )(SelectedInput); 24 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/message_replies.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import MessageReplyItem from './message_reply_item'; 4 | import MessageReplyInput from './message_reply_input'; 5 | 6 | 7 | class MessageReplies extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | } 12 | 13 | 14 | 15 | 16 | componentWillReceiveProps(nextProps) { 17 | } 18 | 19 | // shouldComponentUpdate(nextProps){ 20 | // 21 | // if (this.props.current_message.count > 0) { 22 | // return nextProps.current_message.replies.slice(-1)[0].reply_content != this.props.current_message.replies.slice(-1)[0].reply_content; 23 | // } else { 24 | // return true; 25 | // } 26 | // } 27 | 28 | 29 | render() { 30 | 31 | let current_user_id = this.props.current_user_id; 32 | let message_id = this.props.current_message.message_id; 33 | 34 | let originalMessage; 35 | if (this.props.current_message.content){ 36 | originalMessage = this.props.current_message.content; 37 | } else { 38 | originalMessage =
    Reply Thread
    ; 39 | } 40 | 41 | let originalAvatar; 42 | if ( this.props.current_message.authorPic ) { 43 | originalAvatar = 44 | } else { 45 | originalAvatar = ""; 46 | } 47 | 48 | // let originalAvatar = this.props.current_message.authorPic; 49 | 50 | let allReplies = []; 51 | let i = 0; 52 | while (i < this.props.current_message.count) { 53 | allReplies.push(); 54 | i += 1; 55 | } 56 | 57 | // if (this.props.current_message.count > 0) { 58 | // reply1 = 59 | // } 60 | 61 | let messageReplyInput; 62 | if ( this.props.current_message.message_id) { 63 | messageReplyInput = ; 64 | } else { 65 | messageReplyInput = ""; 66 | } 67 | 68 | return ( 69 |
    70 |
    71 |
    72 |
    73 |
    74 | {originalMessage} 75 |
    76 |
    77 |
    78 | {originalAvatar} 79 |
    80 |
    81 |
    82 | {allReplies} 83 |
    84 |
    85 | {messageReplyInput} 86 |
    87 |
    88 |
    89 | ); 90 | } 91 | 92 | } 93 | 94 | const mapStateToProps = (state) => { 95 | 96 | return ({ 97 | current_message: state.current_message, 98 | current_user: state.session.currentUser.id, 99 | }) 100 | 101 | }; 102 | 103 | // return ({ 104 | // channel: channelId, 105 | // messages: Object.keys(state.messages).map( 106 | // (key) => { 107 | // const message = state.messages[key]; 108 | // const author = state.users[message.user_id]; 109 | // return Object.assign(message, {authorName: author.username, authorPic: author.avatar_url}); 110 | // } 111 | // ) 112 | // }); 113 | 114 | const mapDispatchToProps = (dispatch) => { 115 | return({}); 116 | }; 117 | 118 | export default connect ( 119 | mapStateToProps, 120 | mapDispatchToProps 121 | )(MessageReplies); 122 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/message_reply_input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createMessageReply } from '../../../../actions/message_actions' 3 | import { connect } from 'react-redux'; 4 | 5 | class MessageReplyInput extends React.Component { 6 | constructor(props){ 7 | super(props); 8 | this.state = { 9 | reply: "Reply" 10 | }; 11 | 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | this.empty = this.empty.bind(this); 14 | } 15 | 16 | 17 | 18 | 19 | 20 | empty(type) { 21 | return (e) => { 22 | e.preventDefault(); 23 | if (e.currentTarget.value === "Reply"){ 24 | this.setState({[type]:''}); 25 | } 26 | }; 27 | } 28 | 29 | update(field) { 30 | return e => this.setState({ 31 | [field]: e.currentTarget.value 32 | }); 33 | } 34 | 35 | // handleSubmit(e){ 36 | // e.preventDefault(); 37 | // 38 | // this.setState(function(prevState, props){ 39 | // return {reply: ''} 40 | // }); 41 | // } 42 | 43 | handleSubmit(e){ 44 | e.preventDefault(); 45 | 46 | // this.setState({reply: "REPLACED"}, function () { 47 | // return console.log(this.state) 48 | // }); 49 | // this.setState({reply:"REPLACED"}, function () { 50 | // console.log(this.state.reply); 51 | // }); 52 | const reply_obj = {content: this.state.reply, message_id: this.props.message_id}; 53 | this.props.createMessageReply(reply_obj); 54 | this.setState( { reply:''} ); 55 | } 56 | 57 | 58 | 59 | render(){ 60 | 61 | return( 62 |
    63 |
    64 | 69 |
    70 |
    71 | ); 72 | } 73 | } 74 | // 75 | 76 | const mapStateToProps = (state, { channelId }) => { 77 | return ({}); 78 | }; 79 | 80 | 81 | const mapDispatchToProps = (dispatch) => { 82 | return({ 83 | createMessageReply: (reply) => { return dispatch(createMessageReply(reply))} 84 | }); 85 | }; 86 | 87 | export default connect ( 88 | mapStateToProps, 89 | mapDispatchToProps 90 | )(MessageReplyInput); 91 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/message_reply_item.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import moment from 'moment'; 4 | // import { receiveCurrentMessage } from '../../../../actions/current_message_actions'; 5 | import { connect } from 'react-redux'; 6 | 7 | class MessageReplyItem extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | 13 | render() { 14 | 15 | let reply = this.props.reply; 16 | const date = moment().format('MM/DD'); 17 | 18 | return( 19 |
  • 20 | 21 | 22 |
    23 |
    24 |
    {reply.reply_username}
    25 |
    26 |
    27 |
    28 | {reply.reply_content} 29 |
    30 |
    31 |
  • 32 | ); 33 | } 34 | 35 | } 36 | 37 | 38 | 39 | export default MessageReplyItem; 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | //
    {date}
    48 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/selected_message_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { receiveCurrentMessage } from '../../../../actions/current_message_actions'; 4 | import { connect } from 'react-redux'; 5 | 6 | 7 | // const SelectedMessageItem = (props) => { 8 | // 9 | // const date = moment().format('MM/DD'); 10 | // let message = props.message 11 | // let replyOffer; 12 | // 13 | // if (message.count == 1) { 14 | // replyOffer = 15 | //
    16 | // 17 | //  {message.count} Replies 18 | //
    19 | // } else if (message.count >= 2) { 20 | // replyOffer = 21 | //
    22 | // 23 | // 24 | //  {message.count} Replies 25 | //
    26 | // } else { 27 | // replyOffer = 28 | //
    29 | //
    Reply
    30 | //
    31 | // }; 32 | // 33 | // return( 34 | //
  • 35 | // 36 | //
    37 | // 38 | //
    39 | //
    {message.authorName}
    40 | //
    {date}
    41 | //
    42 | // 43 | //
    44 | // 45 | //
    46 | // {message.content} 47 | //
    48 | // 49 | // {replyOffer} 50 | // 51 | //
    52 | //
  • 53 | // ); 54 | // }; 55 | 56 | 57 | /* 58 | 1.) make the HOC select message itme work 59 | 2.) in props 60 | 3.) 61 | */ 62 | 63 | 64 | class SelectedMessageItem extends React.Component { 65 | constructor(props) { 66 | super(props); 67 | 68 | // this.handleClick = this.handleClick.bind(this); 69 | } 70 | 71 | handleClick(message) { 72 | this.props.receiveCurrentMessage(message); 73 | } 74 | 75 | render () { 76 | const date = moment().format('MM/DD'); 77 | let message = this.props.message 78 | let replyOffer; 79 | 80 | if (message.count == 1) { 81 | replyOffer = 82 |
    { this.props.receiveCurrentMessage(message)}}> 83 | 84 |  {message.count} Reply 85 |
    86 | } else if (message.count >= 2) { 87 | replyOffer = 88 |
    { this.props.receiveCurrentMessage(message)}}> 89 | 90 | 91 |  {message.count} Replies 92 |
    93 | } else { 94 | replyOffer = 95 |
    { this.props.receiveCurrentMessage(message)}}> 96 |
    Reply
    97 |
    98 | }; 99 | return( 100 |
  • 101 | 102 |
    103 | 104 |
    105 |
    {message.authorName}
    106 |
    {date}
    107 |
    108 | 109 |
    110 | 111 |
    112 | {message.content} 113 |
    114 | 115 | {replyOffer} 116 | 117 |
    118 |
  • 119 | ); 120 | } 121 | } 122 | 123 | 124 | 125 | const mapStateToProps = (state) => { 126 | return ({}); 127 | }; 128 | 129 | 130 | const mapDispatchToProps = (dispatch) => { 131 | return({ 132 | receiveCurrentMessage: (message) => dispatch(receiveCurrentMessage(message)) 133 | }); 134 | }; 135 | 136 | 137 | 138 | // 139 | // }; 140 | 141 | export default connect ( 142 | mapStateToProps, 143 | mapDispatchToProps 144 | )(SelectedMessageItem); 145 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/selected_message_item_container.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/frontend/components/homepage/selected_channel/selected_messages/selected_message_item_container.jsx -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/selected_message_list.jsx: -------------------------------------------------------------------------------- 1 | /* globals pusher */ 2 | import React from 'react'; 3 | import { withRouter } from 'react-router'; 4 | import SelectedMessageItem from './selected_message_item'; 5 | import ReactDOM from 'react-dom'; 6 | 7 | class SelectedMessageList extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | // scrollToBottom () { 14 | // 15 | // const node = ReactDOM.findDOMNode(selectedMessageList); 16 | // node.scrollIntoView({ behavior: "smooth" }); 17 | // } 18 | // 19 | // 20 | // componentDidUpdate() { 21 | // this.scrollToBottom(); 22 | // } 23 | 24 | 25 | componentDidMount() { 26 | let current_channel = this.props.channel; 27 | this.props.fetchSelectedMessages(current_channel); 28 | 29 | var pusher = new Pusher('7ed0f023347152a1d1c7', { 30 | cluster: 'us2', 31 | encrypted: true 32 | }); 33 | 34 | var channel = pusher.subscribe('channel_' + current_channel); 35 | channel.bind('message_published', (data) => { 36 | 37 | this.props.fetchSelectedMessages(current_channel); 38 | }); 39 | } 40 | 41 | componentWillReceiveProps(nextProps){ 42 | 43 | if (nextProps.messages.slice(-1).id !== this.props.messages.slice(-1).id || 44 | nextProps.channel !== this.props.channel ) { 45 | const channel = nextProps.channel; 46 | this.props.fetchSelectedMessages(channel); 47 | } 48 | 49 | } 50 | 51 | componentDidUpdate() { 52 | this.bottom.scrollIntoView(); 53 | 54 | } 55 | 56 | // if (nextProps.channel !== this.props.channel){ 57 | // const channel = this.props.channel; 58 | // this.props.fetchSelectedMessages(channel); 59 | // } 60 | 61 | render(){ 62 | const that = this; 63 | const allMessages = this.props.messages.map( (message, idx) => { 64 | if (message.channel_id === parseInt(that.props.channel)) { 65 | 66 | return ; 67 | } 68 | } 69 | ); 70 | 71 | return( 72 |
      73 | {allMessages} 74 |
      this.bottom = el}>
      75 |
    76 | ); 77 | } 78 | } 79 | 80 | export default withRouter(SelectedMessageList); 81 | -------------------------------------------------------------------------------- /frontend/components/homepage/selected_channel/selected_messages/selected_message_list_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import SelectedMessageList from './selected_message_list'; 3 | import { fetchSelectedMessages } from '../../../../actions/message_actions'; 4 | 5 | const mapStateToProps = (state, { channelId }) => { 6 | 7 | return ({ 8 | channel: channelId, 9 | messages: Object.keys(state.messages).map( 10 | (key) => { 11 | const message = state.messages[key]; 12 | const author = state.users[message.user_id]; 13 | const message_id = key; 14 | return Object.assign(message, {authorName: author.username, authorPic: author.avatar_url, message_id: key}); 15 | } 16 | ) 17 | }); 18 | }; 19 | 20 | 21 | const mapDispatchToProps = (dispatch) => { 22 | return({ 23 | fetchSelectedMessages: (channel) => {; return dispatch(fetchSelectedMessages(channel))} 24 | }); 25 | }; 26 | 27 | export default connect ( 28 | mapStateToProps, 29 | mapDispatchToProps 30 | )(SelectedMessageList); 31 | -------------------------------------------------------------------------------- /frontend/components/landing_page/landing_page.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import SessionForm from './session_form/session_form'; 4 | import NavBarContainer from './navbar/navbar_container'; 5 | 6 | class LandingPage extends React.Component { 7 | 8 | render (){ 9 | // debuggesr 10 | return( 11 |
    12 |
    13 | 14 |
    15 | 16 |
    17 |
    18 | 19 |
    20 | 21 |
    22 |
    23 |
    24 |
      25 |
    • 26 | This is a slack clone for presidents 27 |
    • 28 |
    • 29 | Where no work happens 30 |
    • 31 |
    • 32 | When you're in charge, life is good. 33 | The time for action can wait when you have all your presidential friends to hang out with. 34 |
    • 35 |
    36 |
    37 |
    38 |
    39 | ); 40 | } 41 | 42 | 43 | } 44 | 45 | export default LandingPage; 46 | -------------------------------------------------------------------------------- /frontend/components/landing_page/navbar/navbar.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import Modal from 'react-modal'; 4 | import SessionFormContainer from '../session_form/session_form_container'; 5 | import { Link, withRouter } from 'react-router-dom'; 6 | 7 | class Navbar extends React.Component { 8 | constructor(props){ 9 | super(props); 10 | 11 | this.state = { isOpen: false}; 12 | 13 | this.startDemo1 = this.startDemo1.bind(this); 14 | this.startDemo2 = this.startDemo2.bind(this); 15 | 16 | this.handleClick = this.handleClick.bind(this); 17 | this.openModal = this.openModal.bind(this); 18 | this.closeModal = this.closeModal.bind(this); 19 | this.handleModal = this.handleModal.bind(this); 20 | } 21 | 22 | closeModal(e) { 23 | if (this.state.isOpen){ 24 | $('.transform').toggleClass('transform-active'); 25 | } 26 | // this.props.clearErrors(); 27 | let that = this; 28 | setTimeout(function(){ 29 | that.setState({ isOpen: false }); 30 | }, 300); 31 | } 32 | 33 | 34 | openModal(){ 35 | this.setState({ isOpen: true }); 36 | } 37 | 38 | handleClick(type) { 39 | // 40 | return (e) => { 41 | if (type === "login"){ 42 | this.props.loginForm(); 43 | } else { 44 | this.props.signupForm(); 45 | } 46 | }; 47 | } 48 | 49 | startDemo1(){ 50 | const user = {user: {username: "WildernessRulez", password: "password123"}}; 51 | this.props.login(user).then(() => { this.props.history.push('/messages/19'); }); 52 | } 53 | startDemo2(){ 54 | const user = {user: {username: "RevolutionRinger", password: "password123"}}; 55 | this.props.login(user).then(() => { this.props.history.push('/messages/19'); }); 56 | } 57 | 58 | componentWillMount(){ 59 | Modal.setAppElement('body'); 60 | } 61 | 62 | handleModal(e){ 63 | // 64 | if (e.currentTarget.textContent === 'Login' ){ 65 | this.props.loginForm(); 66 | } else if (e.currentTarget.textContent === 'Sign Up'){ 67 | this.props.signupForm(); 68 | } 69 | this.openModal(); 70 | } 71 | 72 | render(){ 73 | 74 | const customStyles = { 75 | overlay : { 76 | position : 'fixed', 77 | top : 0, 78 | left : 0, 79 | right : 0, 80 | bottom : 0, 81 | backgroundColor : 'rgba(50, 50, 50, 0.50)' 82 | }, 83 | content : { 84 | top : '50%', 85 | left : '50%', 86 | right : 'auto', 87 | bottom : 'auto', 88 | marginRight : '-50%', 89 | transform : 'translate(-50%, -50%)' 90 | } 91 | }; 92 | let formType; 93 | if (this.props.modal === "login") { 94 | formType = "login"; 95 | } else { 96 | formType = "signup"; 97 | } 98 | return( 99 |
    100 | 105 | 106 | 107 | 110 | 111 | 116 | 117 | 118 | 121 | 122 | 125 | 126 | 129 |
    130 | ); 131 | } 132 | } 133 | 134 | 135 | 136 | export default withRouter(Navbar); 137 | 138 | 139 | //
    140 | //

    Slack image here

    141 | // 143 | // 145 | //
    146 | -------------------------------------------------------------------------------- /frontend/components/landing_page/navbar/navbar_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { signupForm, loginForm } from '../../../actions/modal_actions'; 3 | import { login } from '../../../actions/session_actions'; 4 | import Navbar from './navbar'; 5 | 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | modal: state.modal 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => ({ 14 | signupForm: () => dispatch(signupForm()), 15 | loginForm: () => dispatch(loginForm()), 16 | login: (user) => dispatch(login(user)) 17 | }); 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Navbar); 23 | -------------------------------------------------------------------------------- /frontend/components/landing_page/session_form/session_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | 4 | 5 | class SessionForm extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | username: 'username', 10 | password: 'password' 11 | }; 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | this.empty = this.empty.bind(this); 14 | } 15 | 16 | empty (type) { 17 | return (e) => { 18 | e.preventDefault(); 19 | if (e.currentTarget.value === "username" || 20 | e.currentTarget.value === "password"){ 21 | this.setState({[type]:''}); 22 | } 23 | }; 24 | } 25 | componentWillReceiveProps(nextProps) { 26 | if (nextProps.loggedIn) { 27 | this.props.history.push('/'); 28 | } 29 | } 30 | 31 | update(field) { 32 | return e => this.setState({ 33 | [field]: e.currentTarget.value 34 | }); 35 | } 36 | 37 | handleSubmit(e) { 38 | e.preventDefault(); 39 | const user = this.state; 40 | if (this.props.type === "login"){ 41 | this.props.login({user}).then(() => { this.props.history.push('/messages/18'); 42 | }); 43 | } else { 44 | this.props.signup({user}).then(() => { this.props.history.push('/messages/18'); 45 | }); 46 | } 47 | 48 | 49 | 50 | 51 | } 52 | 53 | // navLink() { 54 | // if (this.props.type === 'login') { 55 | // return sign up instead; 56 | // } else { 57 | // return log in instead; 58 | // } 59 | // } 60 | 61 | renderErrors() { 62 | // 63 | // return( 64 | //
      65 | // {this.props.errors.map((error, i) => ( 66 | //
    • 67 | // {error} 68 | //
    • 69 | // ))} 70 | //
    71 | // ); 72 | } 73 | 74 | render() { 75 | let message; 76 | if (this.props.type === "login") { 77 | message = "Login"; 78 | } else { 79 | message = "Sign up"; 80 | } 81 | // 82 | return ( 83 |
    84 |
    85 |
    86 |
    87 |
    88 | {message} to commence Slacking. 89 |
    90 |
    91 | {this.renderErrors()} 92 |
    93 | Enter your username and password. 94 |
    95 |
    96 |
    97 |
    98 | 104 |
    105 | 111 |
    112 | 113 |
    114 |
    115 |
    116 | ); 117 | } 118 | } 119 | 120 | export default withRouter(SessionForm); 121 | -------------------------------------------------------------------------------- /frontend/components/landing_page/session_form/session_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { login, logout, signup, receiveCurrentUser } from '../../../actions/session_actions'; 4 | import SessionForm from './session_form'; 5 | 6 | 7 | const mapStateToProps = (state) => { 8 | 9 | return { 10 | loggedIn: Boolean(state.session.currentUser), 11 | errors: state.session.errors, 12 | type: state.modal 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch, ownProps) => { 17 | return ({ 18 | login: (user) => dispatch(login(user)), 19 | signup: (user) => dispatch(signup(user)), 20 | receiveCurrentUser: (user) => dispatch(receiveCurrentUser(user)) 21 | }); 22 | }; 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(SessionForm); 28 | 29 | 30 | 31 | // const formType = ownProps.location.pathname.slice(1); 32 | // const processForm = (formType === 'login') ? login : signup; 33 | // return { 34 | // processForm: user => dispatch(processForm(user)), 35 | // formType 36 | // }; 37 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { HashRouter } from 'react-router-dom'; 4 | 5 | import App from './App'; 6 | 7 | const Root = ({ store }) => ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default Root; 16 | 17 | // import React from 'react'; 18 | // import { Provider } from 'react-redux'; 19 | // import { HashRouter } from 'react-router-dom'; 20 | // import App from './App'; 21 | // 22 | // const Root = ({ store }) => ( 23 | // 24 | // 25 | // 26 | // 27 | // 28 | // ); 29 | // 30 | // export default Root; 31 | -------------------------------------------------------------------------------- /frontend/entry.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | //Components 4 | import Root from './components/root'; 5 | import configureStore from './store/store'; 6 | 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | let store; 10 | if (window.currentUser) { 11 | const preloadedState = { session: { currentUser: window.currentUser } }; 12 | store = configureStore(preloadedState); 13 | delete window.currentUser; 14 | } else { 15 | store = configureStore(); 16 | } 17 | 18 | window.store = store; 19 | const root = document.getElementById('root'); 20 | ReactDOM.render(, root); 21 | }); 22 | -------------------------------------------------------------------------------- /frontend/reducers/channel_reducer.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import { RECEIVE_USER_CHANNELS, RECEIVE_USER_CHANNEL } from '../actions/channel_actions'; 3 | 4 | const default_state = {}; 5 | const ChannelReducer = (state = default_state, action) => { 6 | let newState; 7 | switch (action.type){ 8 | case RECEIVE_USER_CHANNELS: 9 | newState = merge({}, action.channels); 10 | return newState; 11 | case RECEIVE_USER_CHANNEL: 12 | newState = merge({}, state, {[action.channel.id]:action.channel}); 13 | return newState; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default ChannelReducer; 20 | -------------------------------------------------------------------------------- /frontend/reducers/current_message_reducer.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import { RECEIVE_CURRENT_MESSAGE } from '../actions/current_message_actions'; 3 | 4 | const default_state = {}; 5 | const CurrentMessageReducer = ( state = default_state, action) => { 6 | let newState; 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_MESSAGE: 9 | newState = merge({}, action.message); 10 | return newState; 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default CurrentMessageReducer; 17 | -------------------------------------------------------------------------------- /frontend/reducers/message_reducer.js: -------------------------------------------------------------------------------- 1 | import { merge } from 'lodash'; 2 | import { RECEIVE_SELECTED_MESSAGES, RECEIVE_SELECTED_MESSAGE } from '../actions/message_actions'; 3 | 4 | const default_state = {}; 5 | 6 | const MessageReducer = (state = default_state, action) => { 7 | let newState; 8 | switch (action.type){ 9 | case RECEIVE_SELECTED_MESSAGES: 10 | newState = merge({}, state, action.messages); 11 | return newState; 12 | case RECEIVE_SELECTED_MESSAGE: 13 | newState = merge({}, state, {[action.message.id]:action.message}); 14 | // return Object.assign({}, state, {[action.message.id]: action.message}); 15 | return newState; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default MessageReducer; 22 | -------------------------------------------------------------------------------- /frontend/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | 2 | import {LOGIN, SIGNUP} from '../actions/modal_actions'; 3 | import { merge } from "lodash"; 4 | 5 | const ModalReducer = (state = null, action) => { 6 | switch(action.type){ 7 | case LOGIN: 8 | return 'login'; 9 | case SIGNUP: 10 | return 'signup'; 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default ModalReducer; 17 | -------------------------------------------------------------------------------- /frontend/reducers/replies_reducer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/frontend/reducers/replies_reducer.js -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import SessionReducer from './session_reducer'; 4 | import ModalReducer from './modal_reducer'; 5 | import ChannelReducer from './channel_reducer'; 6 | import MessageReducer from './message_reducer'; 7 | import UserReducer from './user_reducer'; 8 | import CurrentMessageReducer from './current_message_reducer'; 9 | 10 | const RootReducer = combineReducers({ 11 | session: SessionReducer, 12 | modal: ModalReducer, 13 | channels: ChannelReducer, 14 | messages: MessageReducer, 15 | users: UserReducer, 16 | current_message: CurrentMessageReducer 17 | }); 18 | 19 | export default RootReducer; 20 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | // import merge from 'lodash/merge'; 2 | 3 | import { 4 | RECEIVE_CURRENT_USER, 5 | RECEIVE_SESSION_ERRORS 6 | } from '../actions/session_actions'; 7 | 8 | const nullUser = 9 | Object.freeze({ 10 | currentUser: null, 11 | errors: [] 12 | }); 13 | 14 | const SessionReducer = (state = nullUser, action) => { 15 | Object.freeze(state); 16 | switch(action.type) { 17 | case RECEIVE_CURRENT_USER: 18 | const currentUser = action.currentUser; 19 | return Object.assign({}, nullUser, { 20 | currentUser 21 | }); 22 | case RECEIVE_SESSION_ERRORS: 23 | const errors = action.errors; 24 | return Object.assign({}, nullUser, { 25 | errors 26 | }); 27 | default: 28 | return state; 29 | } 30 | }; 31 | 32 | export default SessionReducer; 33 | -------------------------------------------------------------------------------- /frontend/reducers/user_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ALL_USERS } from '../actions/user_actions'; 2 | import { merge } from "lodash"; 3 | 4 | const UserReducer = (state = null, action) => { 5 | let newState; 6 | switch(action.type){ 7 | case RECEIVE_ALL_USERS: 8 | newState = merge({}, state, action.users); 9 | return newState; 10 | default: 11 | return state; 12 | } 13 | }; 14 | 15 | export default UserReducer; 16 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import logger from 'redux-logger'; 4 | import RootReducer from '../reducers/root_reducer'; 5 | 6 | const configureStore = (preloadedState = {}) => ( 7 | createStore( 8 | RootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk, logger) 11 | ) 12 | ); 13 | 14 | export default configureStore; 15 | -------------------------------------------------------------------------------- /frontend/util/channel_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const fetchUserChannels = (user) => { 4 | return( 5 | $.ajax({ 6 | type: "GET", 7 | url: "api/channels", 8 | data: user 9 | }) 10 | ); 11 | }; 12 | 13 | 14 | 15 | 16 | 17 | export const fetchCurrentChannel = (channel_id) => { 18 | return( 19 | $.ajax({ 20 | type: "GET", 21 | url: `api/channels/${channel_id}`, 22 | data: {channel: channel_id} 23 | }) 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/util/membership_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export const createMembership = (membership) => { 5 | 6 | return ( 7 | $.ajax({ 8 | type: "POST", 9 | url: "api/channels", 10 | data: {channel: {name: membership.name, secret: membership.secret}, 11 | user_ids: membership.selected } 12 | }) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/util/message_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchSelectedMessages = (channel_id) => { 2 | return( 3 | $.ajax({ 4 | type: "GET", 5 | url: "api/messages", 6 | data: {channel: channel_id} 7 | }) 8 | ); 9 | }; 10 | 11 | export const fetchSelectedMessage = (message_id) => { 12 | return ( 13 | $.ajax({ 14 | type: "GET", 15 | url: "api/messages/${message_id}", 16 | data: {message: message_id} 17 | }) 18 | ) 19 | } 20 | 21 | 22 | export const createSelectedMessage = message => { 23 | return( 24 | $.ajax({ 25 | type:"POST", 26 | url:"api/messages", 27 | data: {message: message} 28 | }) 29 | ); 30 | }; 31 | 32 | export const createMessageReply = reply => { 33 | return( 34 | $.ajax({ 35 | type:"POST", 36 | url:"api/replies", 37 | data: {reply: reply} 38 | }) 39 | ) 40 | } 41 | 42 | export const updateSelectedMessage = message => { 43 | return( 44 | $.ajax({ 45 | type:"POST", 46 | url:"api/message/${message_id}", 47 | data: {message: message} 48 | }) 49 | ) 50 | } 51 | 52 | export const deleteSelectedMessage = message => { 53 | return ( 54 | $.ajax({ 55 | type:"DELETE", 56 | url:"api/messages", 57 | data: {message_id: message.message_id} 58 | }) 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /frontend/util/reply_api_util.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/frontend/util/reply_api_util.js -------------------------------------------------------------------------------- /frontend/util/route_util.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Redirect, withRouter } from 'react-router-dom'; 4 | 5 | const Auth = ({ component: Component, path, loggedIn }) => ( 6 | ( 7 | !loggedIn ? ( 8 | 9 | ) : ( 10 | 11 | ) 12 | )} /> 13 | ); 14 | 15 | const Protected = ({ component: Component, path, loggedIn }) => ( 16 | ( 17 | loggedIn ? ( 18 | 19 | ) : ( 20 | 21 | ) 22 | )} /> 23 | ); 24 | 25 | const mapStateToProps = state => ( 26 | {loggedIn: Boolean(state.session.currentUser)} 27 | ); 28 | 29 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth)); 30 | 31 | export const ProtectedRoute = withRouter(connect(mapStateToProps, null)(Protected)); 32 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const login = user => ( 4 | $.ajax({ 5 | method: 'POST', 6 | url: '/api/session', 7 | data: user 8 | }) 9 | ); 10 | 11 | export const signup = user => ( 12 | $.ajax({ 13 | method: 'POST', 14 | url: '/api/users', 15 | data: user 16 | }) 17 | ); 18 | 19 | export const logout = () => ( 20 | $.ajax({ 21 | method: 'DELETE', 22 | url: '/api/session' 23 | }) 24 | ); 25 | -------------------------------------------------------------------------------- /frontend/util/user_api_util.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const fetchAllUsers = () => { 4 | return( 5 | $.ajax({ 6 | type: "GET", 7 | url: "api/users" 8 | }) 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SlackClone", 3 | "version": "1.0.0", 4 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/mlmid1616/slacker.git" 13 | }, 14 | "scripts": { 15 | "postinstall": "webpack" 16 | }, 17 | "engines": { 18 | "node": "6.7.0", 19 | "npm": "3.10.7" 20 | }, 21 | "keywords": [], 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "babel-core": "^6.25.0", 26 | "babel-loader": "^7.1.0", 27 | "babel-preset-es2015": "^6.24.1", 28 | "babel-preset-react": "^6.24.1", 29 | "moment": "^2.18.1", 30 | "moment-timezone": "^0.5.13", 31 | "react": "^15.6.1", 32 | "react-dom": "^15.6.1", 33 | "react-modal": "^2.0.2", 34 | "react-redux": "^5.0.5", 35 | "react-router-dom": "^4.1.1", 36 | "react-timestamp": "^4.2.1", 37 | "redux": "^3.7.0", 38 | "redux-logger": "^3.0.6", 39 | "redux-thunk": "^2.2.0", 40 | "webpack": "^3.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/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/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/replies.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/test/models/.keep -------------------------------------------------------------------------------- /test/models/reply_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ReplyTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /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/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/tmp/.keep -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mlmid1616/slacker/dad94288aecc72f3ddbc853b30a67c84efdda9cc/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require("path"); 3 | 4 | module.exports = { 5 | context: __dirname, 6 | entry: "./frontend/entry.jsx", 7 | output: { 8 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 9 | filename: "bundle.js" 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: [/\.jsx?$/, /\.js?$/], 15 | exclude: /node_modules/, 16 | loader: 'babel-loader', 17 | query: { 18 | presets: ['es2015', 'react'] 19 | } 20 | } 21 | ] 22 | }, 23 | devtool: 'source-maps', 24 | resolve: { 25 | extensions: [".js", ".jsx", "*"] 26 | } 27 | }; 28 | --------------------------------------------------------------------------------