├── , ├── ] ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── home.png │ │ └── nyc.png │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ └── channels │ │ │ └── .keep │ └── stylesheets │ │ ├── application.css │ │ ├── discover.css │ │ ├── edit_profile.css │ │ ├── feed_page.css │ │ ├── homepage.css │ │ ├── modal.css │ │ ├── nav.css │ │ ├── notify_modal.css │ │ ├── photo_form.css │ │ ├── profile_page.scss │ │ ├── reset.css │ │ ├── session_form.css │ │ ├── show_image.css │ │ └── user_photo_form.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ ├── feeds_controller.rb │ │ ├── follows_controller.rb │ │ ├── notifications_controller.rb │ │ ├── pictures_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 │ ├── concerns │ │ └── .keep │ ├── follow.rb │ ├── like.rb │ ├── notification.rb │ ├── picture.rb │ └── user.rb └── views │ ├── api │ ├── feeds │ │ ├── fresh.json.jbuilder │ │ └── home.json.jbuilder │ ├── follows │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── notifications │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── pictures │ │ ├── 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 └── yarn ├── 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 │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20180605195708_create_users.rb │ ├── 20180605211823_change_users.rb │ ├── 20180607212713_create_pictures.rb │ ├── 20180610022153_create_follows.rb │ ├── 20180613202647_create_notifications.rb │ ├── 20180613203140_update_users.rb │ ├── 20180614170848_update_notification.rb │ ├── 20180614172931_update_users2.rb │ └── 20180729043814_create_likes.rb ├── schema.rb └── seeds.rb ├── frontend ├── actions │ ├── follow_actions.js │ ├── modal_actions.js │ ├── notification_actions.js │ ├── picture_actions.js │ └── session_actions.js ├── components │ ├── App.jsx │ ├── discover │ │ ├── discover_component.jsx │ │ ├── discover_container.js │ │ └── discover_feed_item.jsx │ ├── feed │ │ ├── feed.jsx │ │ ├── feed_container.js │ │ └── feed_list_item.jsx │ ├── greeting │ │ ├── greeting.jsx │ │ └── greeting_container.js │ ├── home.jsx │ ├── modal │ │ └── modal.jsx │ ├── nav │ │ ├── nav_bar.jsx │ │ └── nav_container.js │ ├── notification │ │ ├── notification_modal.jsx │ │ ├── notification_modal_container.js │ │ └── notify_modal_list_item.jsx │ ├── profile │ │ ├── edit_user_container.js │ │ ├── edit_user_form.jsx │ │ ├── photo_list_item.jsx │ │ ├── profile_container.js │ │ ├── profile_page.jsx │ │ ├── user_photo_container.js │ │ └── user_photo_form.jsx │ ├── root.jsx │ ├── session │ │ ├── login_form_container.js │ │ ├── session_form.jsx │ │ └── signup_form_container.js │ ├── show │ │ ├── edit_picture_container.js │ │ ├── show_picture.jsx │ │ └── show_picture_container.js │ └── upload │ │ ├── picture_form.jsx │ │ └── upload_picture_container.js ├── index.jsx ├── reducers │ ├── entities_reducer.js │ ├── errors_reducer.js │ ├── follows_reducer.js │ ├── modal_reducer.js │ ├── notifications_reducer.js │ ├── pictures_reducer.js │ ├── root_reducer.js │ ├── session_errors_reducer.js │ ├── session_reducer.js │ ├── ui_reducer.js │ └── users_reducer.js ├── store │ └── store.js └── util │ ├── feed_util.js │ ├── follow_util.js │ ├── notification_util.js │ ├── picture_api_util.js │ ├── route_util.jsx │ ├── selectors.js │ └── session_api_util.js ├── lib ├── assets │ └── .keep └── tasks │ └── .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 ├── application_system_test_case.rb ├── controllers │ └── .keep ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── follows.yml │ ├── notifications.yml │ ├── pictures.yml │ └── users.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── follow_test.rb │ ├── notification_test.rb │ ├── picture_test.rb │ └── user_test.rb ├── system │ └── .keep └── test_helper.rb ├── vendor └── .keep └── webpack.config.js /,: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/, -------------------------------------------------------------------------------- /]: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | /node_modules 17 | /yarn-error.log 18 | 19 | .byebug_history 20 | 21 | node_modules/ 22 | bundle.js 23 | bundle.js.map 24 | .byebug_history 25 | .DS_Store 26 | npm-debug.log 27 | 28 | # Ignore application configuration 29 | /config/application.yml 30 | -------------------------------------------------------------------------------- /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 'faker', :git => 'https://github.com/stympy/faker.git', :branch => 'master' 9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 10 | gem 'rails', '~> 5.1.6' 11 | # Use postgresql as the database for Active Record 12 | gem 'pg', '>= 0.18', '< 2.0' 13 | # Use Puma as the app server 14 | gem 'puma', '~> 3.7' 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 | # See https://github.com/rails/execjs#readme for more supported runtimes 20 | # gem 'therubyracer', platforms: :ruby 21 | gem 'figaro' 22 | # Use CoffeeScript for .coffee assets and views 23 | gem 'coffee-rails', '~> 4.2' 24 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 25 | gem 'jbuilder', '~> 2.5' 26 | # Use Redis adapter to run Action Cable in production 27 | # gem 'redis', '~> 4.0' 28 | # Use ActiveModel has_secure_password 29 | # gem 'bcrypt', '~> 3.1.7' 30 | gem 'dotenv-rails', groups: [:development, :test] 31 | # Use Capistrano for deployment 32 | # gem 'capistrano-rails', group: :development 33 | gem 'annotate' 34 | gem 'bcrypt' 35 | gem 'jquery-rails' 36 | group :development, :test do 37 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 38 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 39 | # Adds support for Capybara system testing and selenium driver 40 | gem 'capybara', '~> 2.13' 41 | gem 'selenium-webdriver' 42 | end 43 | 44 | 45 | group :development do 46 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 47 | gem 'web-console', '>= 3.3.0' 48 | gem 'listen', '>= 3.0.5', '< 3.2' 49 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 50 | gem 'spring' 51 | gem 'spring-watcher-listen', '~> 2.0.0' 52 | gem 'better_errors' 53 | gem 'binding_of_caller' 54 | gem 'pry-rails' 55 | 56 | end 57 | 58 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 59 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 60 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/stympy/faker.git 3 | revision: a7c03e67c18356b32a487795332c01c3d8d9add7 4 | branch: master 5 | specs: 6 | faker (1.8.7) 7 | i18n (>= 0.7) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actioncable (5.1.6) 13 | actionpack (= 5.1.6) 14 | nio4r (~> 2.0) 15 | websocket-driver (~> 0.6.1) 16 | actionmailer (5.1.6) 17 | actionpack (= 5.1.6) 18 | actionview (= 5.1.6) 19 | activejob (= 5.1.6) 20 | mail (~> 2.5, >= 2.5.4) 21 | rails-dom-testing (~> 2.0) 22 | actionpack (5.1.6) 23 | actionview (= 5.1.6) 24 | activesupport (= 5.1.6) 25 | rack (~> 2.0) 26 | rack-test (>= 0.6.3) 27 | rails-dom-testing (~> 2.0) 28 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 29 | actionview (5.1.6) 30 | activesupport (= 5.1.6) 31 | builder (~> 3.1) 32 | erubi (~> 1.4) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 35 | activejob (5.1.6) 36 | activesupport (= 5.1.6) 37 | globalid (>= 0.3.6) 38 | activemodel (5.1.6) 39 | activesupport (= 5.1.6) 40 | activerecord (5.1.6) 41 | activemodel (= 5.1.6) 42 | activesupport (= 5.1.6) 43 | arel (~> 8.0) 44 | activesupport (5.1.6) 45 | concurrent-ruby (~> 1.0, >= 1.0.2) 46 | i18n (>= 0.7, < 2) 47 | minitest (~> 5.1) 48 | tzinfo (~> 1.1) 49 | addressable (2.5.2) 50 | public_suffix (>= 2.0.2, < 4.0) 51 | annotate (2.7.4) 52 | activerecord (>= 3.2, < 6.0) 53 | rake (>= 10.4, < 13.0) 54 | arel (8.0.0) 55 | bcrypt (3.1.12) 56 | better_errors (2.4.0) 57 | coderay (>= 1.0.0) 58 | erubi (>= 1.0.0) 59 | rack (>= 0.9.0) 60 | bindex (0.5.0) 61 | binding_of_caller (0.8.0) 62 | debug_inspector (>= 0.0.1) 63 | builder (3.2.3) 64 | byebug (10.0.2) 65 | capybara (2.18.0) 66 | addressable 67 | mini_mime (>= 0.1.3) 68 | nokogiri (>= 1.3.3) 69 | rack (>= 1.0.0) 70 | rack-test (>= 0.5.4) 71 | xpath (>= 2.0, < 4.0) 72 | childprocess (0.9.0) 73 | ffi (~> 1.0, >= 1.0.11) 74 | coderay (1.1.2) 75 | coffee-rails (4.2.2) 76 | coffee-script (>= 2.2.0) 77 | railties (>= 4.0.0) 78 | coffee-script (2.4.1) 79 | coffee-script-source 80 | execjs 81 | coffee-script-source (1.12.2) 82 | concurrent-ruby (1.0.5) 83 | crass (1.0.4) 84 | debug_inspector (0.0.3) 85 | dotenv (2.4.0) 86 | dotenv-rails (2.4.0) 87 | dotenv (= 2.4.0) 88 | railties (>= 3.2, < 6.0) 89 | erubi (1.7.1) 90 | execjs (2.7.0) 91 | ffi (1.9.23) 92 | figaro (1.1.1) 93 | thor (~> 0.14) 94 | globalid (0.4.1) 95 | activesupport (>= 4.2.0) 96 | i18n (1.0.1) 97 | concurrent-ruby (~> 1.0) 98 | jbuilder (2.7.0) 99 | activesupport (>= 4.2.0) 100 | multi_json (>= 1.2) 101 | jquery-rails (4.3.3) 102 | rails-dom-testing (>= 1, < 3) 103 | railties (>= 4.2.0) 104 | thor (>= 0.14, < 2.0) 105 | listen (3.1.5) 106 | rb-fsevent (~> 0.9, >= 0.9.4) 107 | rb-inotify (~> 0.9, >= 0.9.7) 108 | ruby_dep (~> 1.2) 109 | loofah (2.2.2) 110 | crass (~> 1.0.2) 111 | nokogiri (>= 1.5.9) 112 | mail (2.7.0) 113 | mini_mime (>= 0.1.1) 114 | method_source (0.9.0) 115 | mini_mime (1.0.0) 116 | mini_portile2 (2.3.0) 117 | minitest (5.11.3) 118 | multi_json (1.13.1) 119 | nio4r (2.3.1) 120 | nokogiri (1.8.2) 121 | mini_portile2 (~> 2.3.0) 122 | pg (1.0.0) 123 | pry (0.11.3) 124 | coderay (~> 1.1.0) 125 | method_source (~> 0.9.0) 126 | pry-rails (0.3.6) 127 | pry (>= 0.10.4) 128 | public_suffix (3.0.2) 129 | puma (3.11.4) 130 | rack (2.0.5) 131 | rack-test (1.0.0) 132 | rack (>= 1.0, < 3) 133 | rails (5.1.6) 134 | actioncable (= 5.1.6) 135 | actionmailer (= 5.1.6) 136 | actionpack (= 5.1.6) 137 | actionview (= 5.1.6) 138 | activejob (= 5.1.6) 139 | activemodel (= 5.1.6) 140 | activerecord (= 5.1.6) 141 | activesupport (= 5.1.6) 142 | bundler (>= 1.3.0) 143 | railties (= 5.1.6) 144 | sprockets-rails (>= 2.0.0) 145 | rails-dom-testing (2.0.3) 146 | activesupport (>= 4.2.0) 147 | nokogiri (>= 1.6) 148 | rails-html-sanitizer (1.0.4) 149 | loofah (~> 2.2, >= 2.2.2) 150 | railties (5.1.6) 151 | actionpack (= 5.1.6) 152 | activesupport (= 5.1.6) 153 | method_source 154 | rake (>= 0.8.7) 155 | thor (>= 0.18.1, < 2.0) 156 | rake (12.3.1) 157 | rb-fsevent (0.10.3) 158 | rb-inotify (0.9.10) 159 | ffi (>= 0.5.0, < 2) 160 | ruby_dep (1.5.0) 161 | rubyzip (1.2.1) 162 | sass (3.5.6) 163 | sass-listen (~> 4.0.0) 164 | sass-listen (4.0.0) 165 | rb-fsevent (~> 0.9, >= 0.9.4) 166 | rb-inotify (~> 0.9, >= 0.9.7) 167 | sass-rails (5.0.7) 168 | railties (>= 4.0.0, < 6) 169 | sass (~> 3.1) 170 | sprockets (>= 2.8, < 4.0) 171 | sprockets-rails (>= 2.0, < 4.0) 172 | tilt (>= 1.1, < 3) 173 | selenium-webdriver (3.12.0) 174 | childprocess (~> 0.5) 175 | rubyzip (~> 1.2) 176 | spring (2.0.2) 177 | activesupport (>= 4.2) 178 | spring-watcher-listen (2.0.1) 179 | listen (>= 2.7, < 4.0) 180 | spring (>= 1.2, < 3.0) 181 | sprockets (3.7.1) 182 | concurrent-ruby (~> 1.0) 183 | rack (> 1, < 3) 184 | sprockets-rails (3.2.1) 185 | actionpack (>= 4.0) 186 | activesupport (>= 4.0) 187 | sprockets (>= 3.0.0) 188 | thor (0.20.0) 189 | thread_safe (0.3.6) 190 | tilt (2.0.8) 191 | tzinfo (1.2.5) 192 | thread_safe (~> 0.1) 193 | uglifier (4.1.10) 194 | execjs (>= 0.3.0, < 3) 195 | web-console (3.6.2) 196 | actionview (>= 5.0) 197 | activemodel (>= 5.0) 198 | bindex (>= 0.4.0) 199 | railties (>= 5.0) 200 | websocket-driver (0.6.5) 201 | websocket-extensions (>= 0.1.0) 202 | websocket-extensions (0.1.3) 203 | xpath (3.1.0) 204 | nokogiri (~> 1.8) 205 | 206 | PLATFORMS 207 | ruby 208 | 209 | DEPENDENCIES 210 | annotate 211 | bcrypt 212 | better_errors 213 | binding_of_caller 214 | byebug 215 | capybara (~> 2.13) 216 | coffee-rails (~> 4.2) 217 | dotenv-rails 218 | faker! 219 | figaro 220 | jbuilder (~> 2.5) 221 | jquery-rails 222 | listen (>= 3.0.5, < 3.2) 223 | pg (>= 0.18, < 2.0) 224 | pry-rails 225 | puma (~> 3.7) 226 | rails (~> 5.1.6) 227 | sass-rails (~> 5.0) 228 | selenium-webdriver 229 | spring 230 | spring-watcher-listen (~> 2.0.0) 231 | tzinfo-data 232 | uglifier (>= 1.3.0) 233 | web-console (>= 3.3.0) 234 | 235 | BUNDLED WITH 236 | 1.16.1 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This site is a clone of 500px.com. While logged in, and one can view other people's photos on their feed, follow other users, and upload pictures of their own. Ruby on Rails was used for the back end, and React was used for the front end. 4 | 5 | [360px](https://threesixtypixels.herokuapp.com/#/) 6 | 7 | # Technologies 8 | 9 | * Ruby on Rails with postgresql 10 | * React-Redux 11 | * Cloudinary 12 | * AJAX 13 | * Vanilla DOM 14 | 15 | # Key Features 16 | 17 | ### Home and Discover Feeds 18 | 19 | ![Text](https://s15.postimg.cc/3yhcnad3f/Screen_Shot_2018-07-06_at_11.47.53_AM.png) 20 | 21 | * At first, when tackling how to fetch the correct pictures for both feeds, I was not sure how to differenctiate between the two. Up until that point, I was only taught about the standard routes. After researching this topic, I came across custom routes , which proved to be a viable solution. My routes for my two feeds are as follows: 22 | 23 | ```ruby 24 | get '/feed', to: 'feeds#home', controller: 'feeds' 25 | get '/fresh', to: 'feeds#fresh', controller: 'feeds' 26 | ``` 27 | * In the feeds controller, I used a predefined User method on the current user in order to get the two feeds accordingly. Right now, my app fetches pictures based on who the user is following, as well as whatever pictures are most recent. My logic also adds pictures from users the current user isn't following, so their feed won't be empty upon signing up for an account: 28 | 29 | ```ruby 30 | def get_home_feed 31 | followings = self.followings 32 | pictures = [] 33 | followings.each do |follow| 34 | user = User.find(follow.leader_id) 35 | pictures += user.pictures 36 | end 37 | if pictures.length < 30 38 | other_pictures = Picture.all.sort {|a,b| b.created_at <=> a.created_at} 39 | count = 0 40 | other_pictures.each do |picture| 41 | unless count == 3 || picture.uploader_id === self.id || pictures.include?(picture) 42 | pictures.push(picture) 43 | count += 1 44 | end 45 | 46 | end 47 | end 48 | pictures.sort {|a,b| b.created_at <=> a.created_at}.take(30) 49 | end 50 | ``` 51 | On the front end, html elements are rendered based on if the current user is following the user who posted the picture: 52 | 53 | ![](https://res.cloudinary.com/dbm56y2y/image/upload/v1529098063/suggested.png) 54 | 55 | ### Followings 56 | 57 | ![Follow demo](https://s15.postimg.cc/fc3vydl0b/ezgif.com-video-to-gif.gif) 58 | 59 | I really enjoyed implementing this in my website. It was fun planning out and and going through the steps these step. While the backend for implementing follows was faily simple, more logic was required in the front end. 60 | 61 | * One of the key mechanices for rendering the follow button correctly is knowing whether the current user is following the user whose profile they are on. The follow for both users is retreived in the following selector: 62 | 63 | ```javascript 64 | export const findFollow = (follows, currentUserId, profileUserId) => { 65 | const followArr = Object.values(follows); 66 | for (var i = 0; i < followArr.length; i++) { 67 | const follow = followArr[i]; 68 | if (follow.follower_id === currentUserId && follow.leader_id === profileUserId) { 69 | return follow; 70 | } 71 | } 72 | return null; 73 | }; 74 | ``` 75 | 76 | * In order for the follow button is stay updated, I determined that I would have to dispatch the proper action based on whether the current user is following the user who the button corrensponds to: 77 | 78 | ```javascript 79 | updateFollow(){ 80 | const {deleteFollow, createFollow, follow, user, currentUser, createNotification} = this.props; 81 | if (follow){ 82 | deleteFollow(follow.id); 83 | } else { 84 | createFollow({leader_id: user.id, follower_id: currentUser.id}); 85 | createNotification({initiator_id: currentUser.id, user_id: user.id}); 86 | } 87 | } 88 | ``` 89 | 90 | A notification is also created so the new followee will receive a notification letting them know that they received a new follower. 91 | 92 | # Things to Note 93 | 94 | * Logging in through demo login provides the user with a new notification so they can see the feature without having to make multiple accounts. 95 | 96 | In the near future, I plan on coming back to this project and adding new features. These include: 97 | 98 | * Taggings for pictures 99 | * A search feature for users and tags 100 | * Likes and comments 101 | * A notification page 102 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/app/assets/images/home.png -------------------------------------------------------------------------------- /app/assets/images/nyc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/app/assets/images/nyc.png -------------------------------------------------------------------------------- /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, or any plugin's 5 | // 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 rails-ujs 16 | //= require_tree . 17 | -------------------------------------------------------------------------------- /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/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/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, or any plugin's 6 | * 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/discover.css: -------------------------------------------------------------------------------- 1 | 2 | .h1 { 3 | top: 100px; 4 | position: absolute; 5 | } 6 | 7 | .discover-holder { 8 | background-color: #f0f0f5; 9 | width: 100vw; 10 | height: 100%; 11 | } 12 | 13 | .discover-top-div { 14 | position: absolute; 15 | top: 54px; 16 | width: 100vw; 17 | height: 125px; 18 | background-color: white; 19 | border-bottom: 1px lightgrey solid; 20 | padding-left: 60px; 21 | } 22 | 23 | .discover-top-div h1 { 24 | font-family: sans-serif; 25 | margin-bottom: 10px; 26 | color: #2D2D2D; 27 | font-size: 36px; 28 | } 29 | 30 | .discover-top-div p { 31 | margin-top: 0px; 32 | font-family: sans-serif; 33 | color: #5B5B5B; 34 | font-size: 18px; 35 | } 36 | 37 | .discover-picture-list { 38 | margin: auto; 39 | padding: 0; 40 | } 41 | 42 | .discover-picture-list li{ 43 | list-style: none; 44 | margin: 0px .60vw .0px 0px; 45 | border-bottom: solid #f0f0f5 .60vw; 46 | position: relative; 47 | height: 22.3vw; 48 | } 49 | 50 | .discover-list-holder { 51 | margin: 0 auto; 52 | width: 86vw; 53 | display: flex; 54 | flex-wrap: wrap; 55 | align-items: center; 56 | margin-top: 180px; 57 | border-top: #f0f0f5 solid 20px 58 | } 59 | 60 | .discover-photo { 61 | /* height: 300px; 62 | width: 400px; */ 63 | width: 27.4vw; 64 | height: 22.3vw; 65 | /* transition-property: filter; 66 | transition-duration: .5s; */ 67 | } 68 | 69 | .discover-photo:hover { 70 | /* filter: drop-shadow(8px 8px 8px black); */ 71 | cursor: pointer; 72 | /* transition-property: filter; 73 | transition-duration: .5s; */ 74 | } 75 | 76 | /* .user-info { 77 | width: 27.4vw; 78 | height: 22.3vw; 79 | background-color: red; 80 | z-index: 50; 81 | position: absolute; 82 | top: 0%; 83 | left: 0%; 84 | } */ 85 | 86 | .user-info { 87 | z-index: 1; 88 | width: 27.4vw; 89 | height: 70px; 90 | /* background-color:grey; */ 91 | position: relative; 92 | top: -75px; 93 | opacity: 0; 94 | background: linear-gradient(rgba(0,0,0,0),black); 95 | transition: .5s ease 96 | } 97 | 98 | 99 | 100 | .discover-username { 101 | font-family: Rajdhani; 102 | color: white; 103 | position: relative; 104 | bottom: 23px; 105 | margin-left: 10px; 106 | } 107 | 108 | .discover-profile-icon { 109 | border-radius: 50%; 110 | height: 50px; 111 | width: 50px; 112 | position: relative; 113 | margin-top: 10px; 114 | margin-bottom: 7px; 115 | margin-left: 15px; 116 | box-shadow: 0px 2px 4px rgba(0,0,0,0.2); 117 | 118 | } 119 | 120 | .discover-picture-list li:hover .user-info { 121 | display: block; 122 | opacity: 1; 123 | } 124 | -------------------------------------------------------------------------------- /app/assets/stylesheets/edit_profile.css: -------------------------------------------------------------------------------- 1 | .upload-cover-button { 2 | width: 300px; 3 | background-color: #00aaff; 4 | color: white; 5 | font-size: 16px; 6 | border-width: 0px; 7 | height: 40px; 8 | margin-left: 50px; 9 | border-radius: 3px; 10 | margin-top: 40px 11 | } 12 | 13 | .upload-cover-button:hover{ 14 | background-color: #6FB7FE; 15 | cursor: pointer; 16 | } 17 | 18 | .upload-profile-button { 19 | width: 300px; 20 | background-color: #ce54ff; 21 | color: white; 22 | font-size: 16px; 23 | border-width: 0px; 24 | height: 40px; 25 | margin-left: 50px; 26 | border-radius: 3px; 27 | margin-top: 40px; 28 | } 29 | 30 | .upload-profile-button:hover{ 31 | background-color: #d670ff; 32 | cursor: pointer; 33 | } 34 | 35 | .description-submit { 36 | width: 300px; 37 | background-color: #34bf49; 38 | color: white; 39 | font-size: 16px; 40 | border-width: 0px; 41 | height: 40px; 42 | margin-left: 50px; 43 | border-radius: 3px; 44 | } 45 | 46 | .description-submit:hover { 47 | background-color: #41cb56; 48 | cursor: pointer; 49 | } 50 | 51 | .description-input { 52 | height: 35px; 53 | width: 300px; 54 | margin-left: 50px; 55 | margin-top: 0px; 56 | margin-bottom: 35px; 57 | 58 | } 59 | 60 | .description { 61 | color: grey; 62 | font-family: sans-serif; 63 | font-size: 14px; 64 | text-align: center; 65 | margin-top: 40px 66 | } 67 | 68 | .edit-close { 69 | position: absolute 70 | } 71 | 72 | .edit-close:hover { 73 | cursor: pointer; 74 | color: red; 75 | } 76 | -------------------------------------------------------------------------------- /app/assets/stylesheets/feed_page.css: -------------------------------------------------------------------------------- 1 | .feed-container { 2 | background-color: #f0f0f5; 3 | width: 100vw; 4 | height: 100%; 5 | } 6 | 7 | .feed-photo { 8 | /* height: 600px; 9 | width: 800px; */ 10 | height: 32.6vw; 11 | width: 48.5vw; 12 | } 13 | 14 | .feed-photo:hover { 15 | cursor: zoom-in; 16 | } 17 | 18 | 19 | 20 | 21 | 22 | 23 | .photo-holder li { 24 | list-style-type: none; 25 | /* top: 70px; */ 26 | position: relative; 27 | } 28 | 29 | 30 | .feed-list { 31 | padding: 0; 32 | margin-top: 50px; 33 | border-top: #f0f0f5 solid 20px 34 | } 35 | 36 | .feed-profile-icon { 37 | border-radius: 50%; 38 | height: 50px; 39 | width: 50px; 40 | position: relative; 41 | margin-top: 10px; 42 | margin-bottom: 7px; 43 | margin-left: 15px; 44 | } 45 | 46 | .feed-username { 47 | font-weight: 400; 48 | font-size: 17px; 49 | font-weight: bolder; 50 | color: #5f5754; 51 | position: relative; 52 | bottom: 23px; 53 | margin-left: 10px; 54 | } 55 | 56 | .photo-holder { 57 | /* height: 740px; 58 | width: 800px; */ 59 | /* height: 39.8vw; */ 60 | /* height: 42.8vw; */ 61 | width: 48.5vw; 62 | background-color: white; 63 | margin: 0 auto; 64 | margin-bottom: 20px; 65 | border: 1px solid lightgrey; 66 | 67 | } 68 | 69 | .feed-title { 70 | font-size: 22px; 71 | font-family: Rajdhani; 72 | color: #5f5754; 73 | margin-top: 15px; 74 | margin-left: 15px; 75 | margin-right: 15px; 76 | width: 47vw; 77 | overflow: wrap break-word; 78 | padding-bottom: 15px; 79 | } 80 | .feed-title2 { 81 | font-size: 22px; 82 | font-family: Rajdhani; 83 | color: white; 84 | margin-top: 15px; 85 | margin-left: 15px; 86 | margin-right: 15px; 87 | width: 48.5vw; 88 | overflow: wrap break-word; 89 | padding-bottom: 15px; 90 | } 91 | -------------------------------------------------------------------------------- /app/assets/stylesheets/homepage.css: -------------------------------------------------------------------------------- 1 | .homeBackground{ 2 | background: url(https://res.cloudinary.com/dbm56y2y/image/upload/v1528870698/benjamin-patin-419152-unsplash.jpg); 3 | width: 100vw; 4 | height: 100vh; 5 | background-repeat: no-repeat; 6 | background-size:cover 7 | } 8 | 9 | .home_content{ 10 | top: 400px; 11 | align-content: center; 12 | position: absolute; 13 | top:40%; 14 | left: 0; 15 | right: 0; 16 | margin: auto; 17 | } 18 | /* got help from https://stackoverflow.com/questions/2570972/css-font-border from Pablo Bianchi for this text shadow */ 19 | .home_header { 20 | color: white; 21 | text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; 22 | text-align: center; 23 | font-family: Rajdhani; 24 | font-size: 44px; 25 | font-weight: 900; 26 | } 27 | 28 | .home_p { 29 | color: white; 30 | text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; 31 | text-align: center; 32 | margin: 40px 0px; 33 | font-family: Rajdhani; 34 | font-size: 38px; 35 | } 36 | 37 | .home_link { 38 | text-align: center; 39 | 40 | } 41 | 42 | .home_button { 43 | background-color: #00aaff; 44 | border-radius: 30px; 45 | color: white; 46 | height: 55px; 47 | width: 160px; 48 | font-size: 18px; 49 | border-width: 0px; 50 | 51 | } 52 | 53 | .home_button:hover { 54 | /* background-color: #3BB0FE; */ 55 | background-color: #6FB7FE; 56 | cursor: pointer; 57 | 58 | } 59 | 60 | 61 | .home_link { 62 | align-content: center; 63 | 64 | } 65 | -------------------------------------------------------------------------------- /app/assets/stylesheets/modal.css: -------------------------------------------------------------------------------- 1 | .modal-background { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | left: 0; 7 | background: rgba(0, 0, 0, 0.7); 8 | z-index: 30; 9 | } 10 | 11 | .modal-background2 { 12 | position: fixed; 13 | top: 0; 14 | bottom: 0; 15 | right: 0; 16 | left: 0; 17 | z-index: 200; 18 | } 19 | 20 | .modal-child { 21 | position: absolute; 22 | background-color: white; 23 | top: 50%; 24 | left: 50%; 25 | transform: translate(-50%, -50%); 26 | width: 400px; 27 | height: 550px; 28 | z-index: 50; 29 | } 30 | 31 | .modal-child-edit { 32 | position: absolute; 33 | background-color: white; 34 | top: 50%; 35 | left: 50%; 36 | transform: translate(-50%, -50%); 37 | width: 400px; 38 | height: 387px; 39 | z-index: 50; 40 | } 41 | 42 | .modal-child-show { 43 | position: absolute; 44 | background-color: white; 45 | top: 50%; 46 | left: 50%; 47 | transform: translate(-50%, -50%); 48 | /* width: 800px; 49 | height: 600px; */ 50 | width: 72.9vw; 51 | height: 45.1vw; 52 | } 53 | 54 | .modal-child-user { 55 | position: absolute; 56 | background-color: white; 57 | top: 50%; 58 | left: 50%; 59 | transform: translate(-50%, -50%); 60 | width: 400px; 61 | height: 470px; 62 | z-index: 50; 63 | } 64 | 65 | .modal-child-notify { 66 | z-index: 50; 67 | position: fixed; 68 | background-color: white; 69 | right: 0%; 70 | top: 0%; 71 | margin-top: 57px; 72 | margin-right: 10px; 73 | border: 1px solid rgba(34,34,34,0.1); 74 | /* transform: translate(-50%, -50%); */ 75 | width: 356px; 76 | } 77 | -------------------------------------------------------------------------------- /app/assets/stylesheets/nav.css: -------------------------------------------------------------------------------- 1 | .nav_bar { 2 | width: 100%; 3 | justify-content: space-between; 4 | display: flex; 5 | } 6 | 7 | .nav_bar li { 8 | list-style: none; 9 | } 10 | 11 | .left_nav { 12 | padding: 0px; 13 | margin: 15px 0px 0px 50px; 14 | } 15 | 16 | 17 | .right_nav { 18 | width: 160px; 19 | display: flex; 20 | position: relative; 21 | justify-content: space-between; 22 | margin: 15px 50px 0px 0px; 23 | } 24 | 25 | .right_nav li{ 26 | margin: auto 0px 27 | } 28 | 29 | .logo{ 30 | color: white; 31 | text-decoration: none; 32 | font-size: 30px; 33 | font-family: Rajdhani; 34 | text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; 35 | } 36 | 37 | .logo:hover { 38 | cursor: default; 39 | } 40 | 41 | .nav_button{ 42 | background-color: #00aaff; 43 | border-radius: 30px; 44 | color: white; 45 | height: 30px; 46 | width: 90px; 47 | font-size: 14px; 48 | border-width: 0px 49 | } 50 | 51 | .nav_login { 52 | color: white; 53 | text-decoration: none; 54 | vertical-align: center; 55 | font-size: 16px; 56 | text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black; 57 | } 58 | 59 | .nav_login:hover { 60 | color: #00aaff; 61 | } 62 | 63 | .nav_button:hover { 64 | background-color: #6FB7FE; 65 | cursor: pointer; 66 | } 67 | 68 | /* this is for the login nav bar */ 69 | 70 | .session_bar { 71 | width: 100%; 72 | justify-content: space-between; 73 | display: flex; 74 | background-color: white; 75 | border-bottom: 1px lightgrey solid; 76 | height: 53px; 77 | position: fixed; 78 | top: 0px; 79 | z-index: 99; 80 | /* overflow: hidden; */ 81 | } 82 | 83 | 84 | 85 | .session_bar ul{ 86 | margin-top: 0px; 87 | 88 | } 89 | 90 | .session_bar li { 91 | list-style: none 92 | } 93 | 94 | .session_container { 95 | background-color: #f0f0f5; 96 | width: 100vw; 97 | height: 100vh; 98 | } 99 | 100 | .session_logo { 101 | color: black; 102 | text-decoration: none; 103 | font-size: 30px; 104 | font-family: Rajdhani; 105 | z-index: 20; 106 | } 107 | 108 | .session_logo:hover { 109 | cursor: pointer; 110 | } 111 | 112 | .login_signup { 113 | background-color: #34bf49; 114 | color: white; 115 | height: 53px; 116 | width: 90px; 117 | font-size: 14px; 118 | border-width: 0px; 119 | z-index: 20; 120 | } 121 | 122 | .login_signup:hover { 123 | background-color: #41cb56; 124 | cursor: pointer; 125 | } 126 | 127 | .login_left { 128 | padding: 10px; 129 | margin: 0px 0px 0px 45px; 130 | display: flex; 131 | position: relative; 132 | justify-content: space-between; 133 | width: 200px; 134 | } 135 | 136 | .login_left li { 137 | position: absolute; 138 | } 139 | 140 | 141 | /* nav bar css for feed page */ 142 | 143 | .feed_logo { 144 | color: black; 145 | text-decoration: none; 146 | font-size: 30px; 147 | font-family: Rajdhani; 148 | z-index: 20; 149 | } 150 | 151 | .feed_logo:hover { 152 | cursor: default; 153 | } 154 | 155 | .upload { 156 | background-color: #34bf49; 157 | color: white; 158 | height: 53px; 159 | width: 110px; 160 | /* font-size: 14px; */ 161 | border-width: 0px; 162 | z-index: 99; 163 | justify-content: space-between; 164 | padding: 0; 165 | font-size: 20px; 166 | } 167 | 168 | .icon_upload { 169 | margin-right: 7px; 170 | } 171 | 172 | .font_overide { 173 | font-size: 14px; 174 | } 175 | 176 | .upload:hover { 177 | background-color: #41cb56; 178 | cursor: pointer; 179 | } 180 | 181 | .profile-icon { 182 | border-radius: 50%; 183 | /* background-color: grey; */ 184 | height: 25px; 185 | width: 25px; 186 | position: relative; 187 | } 188 | /* had to user css friends phase2 for help with the hamburger menu */ 189 | .side-holder:hover .profile-list { 190 | display: block 191 | } 192 | 193 | .profile-list { 194 | display: none; 195 | position: absolute; 196 | left: 10px; 197 | font-size: 14px; 198 | white-space: nowrap; 199 | background: #fff; 200 | overflow: hidden; 201 | padding: 0px; 202 | top: 40px; 203 | } 204 | 205 | .profile-list > li { 206 | width: 80px; 207 | text-align: center; 208 | padding-bottom: 10px; 209 | 210 | } 211 | 212 | .first-item-list { 213 | padding-top: 10px; 214 | } 215 | .profile-link { 216 | text-decoration: none; 217 | color: black; 218 | } 219 | 220 | .profile-link:hover { 221 | color: blue; 222 | cursor: pointer; 223 | } 224 | 225 | .profile-list > li:hover { 226 | color: blue; 227 | cursor: pointer; 228 | } 229 | 230 | /* nav bar styling for profile page */ 231 | .profile_logo { 232 | color: black; 233 | text-decoration: none; 234 | font-size: 30px; 235 | font-family: Rajdhani; 236 | z-index: 20; 237 | } 238 | 239 | .profile_logo:hover { 240 | cursor: pointer; 241 | } 242 | 243 | /* nav bar for discover page */ 244 | 245 | .discover { 246 | left: 105px; 247 | top: 21px; 248 | } 249 | 250 | .discover-text{ 251 | text-decoration: none; 252 | color: black; 253 | } 254 | 255 | .discover-no-link{ 256 | left: 105px; 257 | top: 21px; 258 | } 259 | .discover-text:hover { 260 | cursor: pointer; 261 | color: blue; 262 | } 263 | 264 | .discover-hidden { 265 | display: none; 266 | } 267 | 268 | .login-right-nav { 269 | width: 190px; 270 | display: flex; 271 | position: relative; 272 | justify-content: space-between; 273 | margin: 15px 50px 0px 0px; 274 | } 275 | 276 | .login-right-nav li{ 277 | margin: auto 0px 278 | } 279 | 280 | .bell { 281 | font-size: 19px; 282 | position: relative; 283 | top: -1px; 284 | } 285 | 286 | .bell:hover { 287 | cursor: pointer; 288 | color: #68a2ff; 289 | } 290 | 291 | .notify { 292 | background-color: red; 293 | height: 25px; 294 | width: 25px; 295 | border-radius: 50%; 296 | text-align: center; 297 | color: white; 298 | top: -1px; 299 | position: relative; 300 | } 301 | 302 | .notiNumber { 303 | top: 4px; 304 | position: relative; 305 | font-weight: bold; 306 | } 307 | 308 | .notify:hover { 309 | cursor: pointer; 310 | opacity: .8; 311 | } 312 | -------------------------------------------------------------------------------- /app/assets/stylesheets/notify_modal.css: -------------------------------------------------------------------------------- 1 | .notify-header { 2 | height: 45px; 3 | width: 100%; 4 | background-color: #f7f8fa; 5 | border-bottom: 1px solid rgba(34,34,34,0.1); 6 | } 7 | 8 | .notify-title { 9 | color: #71767a; 10 | font-size: 20px; 11 | font-weight: bold; 12 | font-family: Rajdhani; 13 | font-weight: bolder; 14 | position: absolute; 15 | top: 9px; 16 | left: 10px; 17 | } 18 | 19 | .no-notify { 20 | background-color: white; 21 | height: 55px; 22 | } 23 | 24 | .no-notify span { 25 | color: #71767a; 26 | font-family: sans-serif; 27 | font-size: 14px; 28 | top: 62.5px; 29 | left: 100.8px; 30 | position: absolute; 31 | } 32 | 33 | 34 | .notify-link { 35 | text-decoration: none; 36 | } 37 | 38 | .notify-list { 39 | margin-top: 0px; 40 | padding-left: 0px; 41 | overflow: scroll; 42 | margin-bottom: 0px; 43 | max-height: 496px; 44 | } 45 | 46 | .notify-list li { 47 | height: 70px; 48 | width: 356px; 49 | list-style: none; 50 | border-bottom: 1px solid rgba(34,34,34,0.1); 51 | } 52 | 53 | .notify-text { 54 | position: relative; 55 | bottom: 31px; 56 | margin-left: 10px; 57 | overflow: wrap break-word; 58 | color: #71767a; 59 | } 60 | 61 | .notify-link-text { 62 | text-decoration: none; 63 | font-weight: bold; 64 | color: #71767a; 65 | } 66 | 67 | .notify-link-text:hover { 68 | color: #4eaae8 69 | } 70 | 71 | 72 | .notify-list li img { 73 | height: 50px; 74 | width: 50px; 75 | margin: 10px 0px 10px 10px; 76 | } 77 | 78 | .notify-list li img:hover { 79 | opacity: .8; 80 | } 81 | 82 | .notify-close { 83 | height: 30px; 84 | } 85 | 86 | .notify-close:hover { 87 | cursor: pointer 88 | } 89 | 90 | .notify-close span { 91 | color: #71767a; 92 | font-size: 14px; 93 | position: relative; 94 | top: -9px; 95 | left: 160px; 96 | } 97 | -------------------------------------------------------------------------------- /app/assets/stylesheets/photo_form.css: -------------------------------------------------------------------------------- 1 | .form-image { 2 | width:300px; 3 | height: 300px; 4 | margin: 17px 50px 19px 50px 5 | } 6 | 7 | .modal-upload-button { 8 | width: 300px; 9 | background-color: #34bf49; 10 | color: white; 11 | font-size: 16px; 12 | border-width: 0px; 13 | height: 40px; 14 | margin-left: 50px; 15 | border-radius: 3px; 16 | 17 | } 18 | 19 | .modal-upload-button:hover { 20 | background-color: #41cb56; 21 | cursor: pointer; 22 | } 23 | 24 | .upload-input { 25 | height: 35px; 26 | width: 300px; 27 | margin-left: 50px; 28 | margin-top: 0px; 29 | margin-bottom: 35px; 30 | 31 | } 32 | 33 | .upload-label { 34 | color: grey; 35 | font-family: sans-serif; 36 | font-size: 14px; 37 | margin-left: 182px; 38 | } 39 | 40 | .upload-label2 { 41 | color: grey; 42 | font-family: sans-serif; 43 | font-size: 14px; 44 | margin-left: 187px; 45 | } 46 | 47 | .close-font-upload { 48 | font-size: 24px 49 | } 50 | .upload-close:hover { 51 | cursor: pointer; 52 | color: red; 53 | } 54 | -------------------------------------------------------------------------------- /app/assets/stylesheets/profile_page.scss: -------------------------------------------------------------------------------- 1 | .profile-holder { 2 | background-color: #f0f0f5; 3 | width: 100vw; 4 | height: 100%; 5 | } 6 | 7 | .profile-holder2 { 8 | background-color: #f0f0f5; 9 | width: 100vw; 10 | height: 100vh; 11 | } 12 | 13 | 14 | 15 | .profile-photo { 16 | /* height: 300px; 17 | width: 400px; */ 18 | width: 20.8vw; 19 | height: 16.9vw; 20 | transition-property: filter; 21 | transition-duration: .5s; 22 | /* transition: .35s ease; */ 23 | } 24 | 25 | .profile-photo:hover { 26 | filter: drop-shadow(4px 4px 4px black); 27 | cursor: pointer; 28 | transition-property: filter; 29 | transition-duration: .5s; 30 | } 31 | 32 | .picture-list { 33 | 34 | margin: auto; 35 | padding: 0; 36 | } 37 | 38 | .picture-list li { 39 | list-style: none; 40 | margin: 0px .60vw .35vw 0px; 41 | position: relative; 42 | } 43 | 44 | .list-holder { 45 | margin: 0 auto; 46 | width: 86vw; 47 | display: flex; 48 | flex-wrap: wrap; 49 | align-items: center; 50 | } 51 | 52 | 53 | 54 | 55 | 56 | .following { 57 | background-color: #34bf49; 58 | color: white; 59 | border-radius: 10%; 60 | font-size: 14px; 61 | width: 78px; 62 | height: 28px; 63 | /* left: 100%; 64 | top: 54%; */ 65 | top: 81vh; 66 | right: 25px; 67 | position: absolute; 68 | 69 | } 70 | 71 | .following2 { 72 | background-color: #34bf49; 73 | color: white; 74 | border-radius: 10%; 75 | font-size: 14px; 76 | width: 78px; 77 | height: 28px; 78 | /* left: 100%; 79 | top: 54%; */ 80 | left: 93%; 81 | top: 170px; 82 | position: relative; 83 | } 84 | 85 | .following:hover span { 86 | display: none; 87 | } 88 | 89 | .following2:hover span { 90 | display: none; 91 | } 92 | 93 | .following:hover:before { 94 | content: "Unfollow" 95 | } 96 | 97 | .following2:hover:before { 98 | content: "Unfollow" 99 | } 100 | 101 | 102 | 103 | .following:hover { 104 | background-color: #ff3333; 105 | cursor: pointer; 106 | } 107 | 108 | .following2:hover { 109 | background-color: #ff3333; 110 | cursor: pointer; 111 | } 112 | 113 | .follow { 114 | background-color: #00aaff; 115 | content: "Follow"; 116 | color: white; 117 | font-size: 14px; 118 | width: 78px; 119 | height: 28px; 120 | top: 81vh; 121 | right: 25px; 122 | position: absolute; 123 | 124 | } 125 | .follow2 { 126 | background-color: #00aaff; 127 | content: "Follow"; 128 | color: white; 129 | font-size: 14px; 130 | width: 78px; 131 | height: 28px; 132 | left: 93%; 133 | top: 170px; 134 | position: relative; 135 | } 136 | 137 | .follow:hover { 138 | background-color: #6FB7FE; 139 | content: "Follow"; 140 | cursor: pointer; 141 | } 142 | 143 | .follow2:hover { 144 | background-color: #6FB7FE; 145 | content: "Follow"; 146 | cursor: pointer; 147 | } 148 | 149 | .cover-photo { 150 | 151 | /*background-size: cover; 152 | background-repeat: no-repeat; 153 | background-position: 50% 50%; */ 154 | overflow: hidden; 155 | justify-content: center; 156 | align-items: center; 157 | width: 100vw; 158 | height: 80vh; 159 | display: flex; 160 | .inside-cover { 161 | /* flex-shrink: 0; */ 162 | max-width: 100%; 163 | } 164 | } 165 | 166 | .cover-div { 167 | width: 100vw; 168 | height: 130px; 169 | background-color: #f0f0f5; 170 | border: 1px solid white; 171 | } 172 | 173 | .profile-pic { 174 | border-radius: 50%; 175 | height: 100px; 176 | width: 7%; 177 | border: 2px solid white; 178 | position: absolute; 179 | left: 46.2%; 180 | } 181 | 182 | .profile-image-holder { 183 | position: relative; 184 | width: 90vw; 185 | height: 100px; 186 | top: -50px; 187 | margin: auto; 188 | } 189 | /* .button-holder { 190 | width: 100vw; 191 | height: 185px; 192 | position: relative; 193 | } */ 194 | 195 | .profile-title { 196 | text-align: center; 197 | font-size: 30px; 198 | color: #5f5754; 199 | font-family: Rajdhani; 200 | margin: 0px 0px 15px 0px; 201 | } 202 | .profile-description { 203 | text-align: center; 204 | font-size: 14px; 205 | color: #5f5754; 206 | margin: 0px 0px 15px 0px; 207 | 208 | } 209 | .profile-follow { 210 | text-align: center; 211 | font-size: 14px; 212 | color: #b9c1c7; 213 | margin: 20px auto; 214 | } 215 | 216 | .number { 217 | font-weight: bolder; 218 | margin-left: 10px; 219 | } 220 | 221 | .profile-footer { 222 | height: 54px; 223 | width: 100vw; 224 | bottom: 0; 225 | left: 0; 226 | position: relative; 227 | background-color: #f0f0f5; 228 | } 229 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/session_form.css: -------------------------------------------------------------------------------- 1 | .session_form { 2 | background-color: white; 3 | width: 400px; 4 | height: 500px; 5 | border: 1px lightgrey solid; 6 | position: absolute; 7 | margin: auto; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | } 13 | 14 | .internal-session-form > label { 15 | color: grey; 16 | font-family: sans-serif; 17 | font-size: 14px; 18 | margin-left: 42px; 19 | } 20 | 21 | .form-input { 22 | height: 35px; 23 | width: 316px; 24 | margin-left: 42px; 25 | margin-top: 5px; 26 | margin-bottom: 35px; 27 | 28 | } 29 | 30 | .session_header { 31 | color: grey; 32 | font-family: Rajdhani; 33 | text-align: center; 34 | } 35 | 36 | .login_login { 37 | width: 316px; 38 | background-color: #34bf49; 39 | color: white; 40 | font-size: 16px; 41 | border-width: 0px; 42 | height: 40px; 43 | margin-bottom: 20px; 44 | margin-left: 42px; 45 | border-radius: 3px; 46 | 47 | } 48 | 49 | .login_login:hover { 50 | background-color: #41cb56; 51 | cursor: pointer; 52 | } 53 | 54 | .demo_login { 55 | width: 316px; 56 | background-color: #00aaff; 57 | color: white; 58 | font-size: 16px; 59 | border-width: 0px; 60 | height: 40px; 61 | margin-bottom: 20px; 62 | margin-left: 42px; 63 | border-radius: 3px; 64 | 65 | } 66 | 67 | .demo_login:hover{ 68 | background-color:#6FB7FE; 69 | cursor: pointer; 70 | } 71 | 72 | .signup_link { 73 | color: grey; 74 | font-family: sans-serif; 75 | font-size: 12px; 76 | text-align: center; 77 | } 78 | 79 | /* .session_errors { 80 | background-color: #f34f50; 81 | z-index: 10; 82 | position: absolute; 83 | margin: 0 auto; 84 | padding-left: 0px; 85 | margin: auto 86 | } */ 87 | 88 | .session_errors li { 89 | color: white; 90 | list-style: none; 91 | text-align: center; 92 | z-index: 999; 93 | } 94 | 95 | .session_errors:nth-child(odd) { 96 | padding: 5px 5px 5px 5px 97 | } 98 | 99 | .session_errors:nth-child(even) { 100 | padding: 0px 0px 5px 0px 101 | } 102 | 103 | 104 | .error_holder ul { 105 | background-color: #f34f50; 106 | z-index: 999; 107 | position: relative; 108 | padding-left: 0px; 109 | margin-top: 0px; 110 | width: 310px; 111 | border-radius: 3px; 112 | margin: 0 auto 113 | } 114 | 115 | .error_holder { 116 | width: 100vw; 117 | position: absolute; 118 | z-index:1; 119 | top: 50px; 120 | z-index: 500; 121 | } 122 | 123 | .error_holder_transition { 124 | 125 | 126 | } 127 | -------------------------------------------------------------------------------- /app/assets/stylesheets/show_image.css: -------------------------------------------------------------------------------- 1 | .show-image{ 2 | /* width: 700px; 3 | height: 600px; */ 4 | width: 59vw; 5 | height: 45.1vw; 6 | } 7 | 8 | .default-cursor:hover { 9 | cursor: default; 10 | } 11 | 12 | .show-title { 13 | font-family: Rajdhani; 14 | text-align: center; 15 | top: 10px; 16 | position: relative; 17 | } 18 | 19 | .show-link { 20 | text-decoration: none; 21 | color: black; 22 | } 23 | 24 | .show-username { 25 | margin-left: 5px; 26 | } 27 | .show-div { 28 | /* width: 900px; */ 29 | width: 72.9vw; 30 | /* height: 600px; */ 31 | height: 45.1vw; 32 | display: flex; 33 | flex-wrap: wrap; 34 | background-color: white; 35 | } 36 | 37 | .show-div div { 38 | 39 | } 40 | 41 | .image-placeholder{ 42 | width: 35px; 43 | height: 35px; 44 | border-radius: 50%; 45 | background-color: grey; 46 | float: left; 47 | margin-left: 10px 48 | 49 | } 50 | 51 | .show-child-1 { 52 | 53 | } 54 | 55 | .show-child-2 { 56 | position: relative; 57 | /* width: 200px; */ 58 | width: 13.9vw; 59 | /* height: 600px; */ 60 | height: 45.1vw; 61 | /* text-align: center; */ 62 | } 63 | .button-holder{ 64 | align-items: center; 65 | position: absolute; 66 | bottom: 0px; 67 | left: 0px; 68 | } 69 | .button1 { 70 | background-color: #1a1aff; 71 | color: white; 72 | height: 40px; 73 | /* width: 100px; */ 74 | width: 6.95vw; 75 | font-size: 16px; 76 | border: none; 77 | } 78 | 79 | .button1:hover { 80 | /* background-color:#4d4dff; */ 81 | background-color: white; 82 | color: #1a1aff; 83 | cursor: pointer; 84 | border: 2px solid #1a1aff; 85 | } 86 | .button2{ 87 | background-color: #e60000; 88 | color: white; 89 | height: 40px; 90 | /* width: 100px; */ 91 | width: 6.95vw; 92 | font-size: 16px; 93 | border: none; 94 | } 95 | 96 | .button2:hover { 97 | /* background-color: #ff4d4d; */ 98 | background-color: white; 99 | color: #e60000; 100 | cursor: pointer; 101 | border: 2px solid #e60000; 102 | } 103 | 104 | .show-following { 105 | margin: 5px 0px 0px 50px; 106 | position: absolute; 107 | background-color: #34bf49; 108 | color: white; 109 | border-radius: 10%; 110 | font-size: 11px; 111 | width: 69px; 112 | height: 22px; 113 | display: block; 114 | } 115 | 116 | .show-following:hover span { 117 | display: none; 118 | } 119 | 120 | .show-following:hover:before { 121 | content: "Unfollow" 122 | } 123 | 124 | 125 | 126 | .show-following:hover { 127 | background-color: #ff3333; 128 | cursor: pointer; 129 | } 130 | 131 | .show-follow { 132 | margin: 5px 0px 0px 50px; 133 | position: absolute; 134 | background-color: #00aaff; 135 | content: "Follow"; 136 | color: white; 137 | font-size: 11px; 138 | width: 69px; 139 | height: 22px; 140 | display: block; 141 | 142 | } 143 | 144 | .show-follow:hover { 145 | background-color: #6FB7FE; 146 | content: "Follow"; 147 | cursor: pointer; 148 | } 149 | -------------------------------------------------------------------------------- /app/assets/stylesheets/user_photo_form.css: -------------------------------------------------------------------------------- 1 | .profile-title { 2 | text-align: center; 3 | color: grey; 4 | font-family: sans-serif; 5 | font-size: 14px; 6 | margin-bottom: 20px; 7 | } 8 | 9 | 10 | .user-image { 11 | width:300px; 12 | height: 300px; 13 | margin: 0px 50px 30px 50px 14 | } 15 | -------------------------------------------------------------------------------- /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/feeds_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FeedsController < ApplicationController 2 | 3 | def home 4 | @pictures = current_user.get_home_feed 5 | render :home 6 | end 7 | 8 | def fresh 9 | @pictures = current_user.get_fresh_feed 10 | render :fresh 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/api/follows_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::FollowsController < ApplicationController 2 | def create 3 | @follow = Follow.new(follow_params) 4 | if @follow.save 5 | render :show 6 | else 7 | render json: @follow.errors.full_messages, status: 422 8 | end 9 | end 10 | 11 | def index 12 | @follows = Follow.all 13 | render :index 14 | end 15 | 16 | def destroy 17 | @follow = Follow.find(params[:id]) 18 | @follow.destroy 19 | render :show 20 | end 21 | 22 | private 23 | 24 | def follow_params 25 | params.require(:follow).permit(:leader_id,:follower_id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/api/notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::NotificationsController < ApplicationController 2 | def create 3 | @notification = Notification.new(notification_params) 4 | @notification.viewed = false 5 | if @notification.save! 6 | render :show 7 | else 8 | render json: @notification.errors.full_messages, status: 422 9 | end 10 | end 11 | 12 | def update 13 | @notification = Notification.find_by(id: params[:id].to_i) 14 | if @notification.update(notification_params) 15 | render :show 16 | else 17 | render json: @notification.errors.full_messages, status: 422 18 | end 19 | end 20 | 21 | def index 22 | @notifications = current_user.get_notifications 23 | render :index 24 | end 25 | 26 | def destroy 27 | @notification = Notification.find(params[:id]) 28 | @notification.destroy 29 | render :show 30 | end 31 | 32 | private 33 | 34 | def notification_params 35 | params.require(:notification).permit(:initiator_id,:user_id, :viewed) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/controllers/api/pictures_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::PicturesController < ApplicationController 2 | 3 | def create 4 | @picture = Picture.new(picture_params) 5 | @picture.uploader_id = params[:user_id] 6 | if @picture.save 7 | render :show 8 | else 9 | render json: @picture.errors.full_messages, status: 422 10 | end 11 | end 12 | 13 | def update 14 | @picture = Picture.find_by(id: params[:id].to_i) 15 | if @picture.update(picture_params) 16 | render :show 17 | else 18 | render json: @picture.errors.full_messages, status: 422 19 | end 20 | end 21 | 22 | def destroy 23 | @picture = Picture.find_by(id: params[:id]) 24 | @picture.destroy 25 | render :show 26 | end 27 | 28 | def index 29 | # the params thing may be wrong. going to have to check this out when I test 30 | puts params 31 | user = User.find_by(id: params[:user_id]) 32 | @pictures = user.pictures 33 | end 34 | 35 | def show 36 | @picture = Picture.find_by(id: params[:id]) 37 | render :show 38 | end 39 | 40 | private 41 | 42 | def picture_params 43 | params.require(:picture).permit(:title, :image_url) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials(params[:user][:username], params[:user][:password]) 5 | if @user 6 | login(@user) 7 | if @user.username == 'Felix' 8 | Notification.create(user_id: @user.id, initiator_id: User.all[3].id, viewed: false) 9 | Follow.create(leader_id: @user.id,follower_id: User.all[3].id) 10 | end 11 | render 'api/users/show' 12 | else 13 | render json: ["Must have valid username and password"], status: 401 14 | end 15 | end 16 | 17 | def destroy 18 | @user = current_user 19 | if @user 20 | if @user.username == 'Felix' 21 | notify = Notification.find_by(initiator_id: User.all[3].id) 22 | notify.destroy 23 | follow = Follow.find_by(follower_id: User.all[3].id) 24 | follow.destroy 25 | end 26 | logout 27 | render 'api/users/show' 28 | else 29 | render json: ["Must be logged in to log out"], status: 404 30 | end 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | 5 | @user = User.new(user_params) 6 | if @user.save 7 | login(@user) 8 | render :show 9 | else 10 | render json: @user.errors.full_messages, status: 422 11 | end 12 | end 13 | 14 | def index 15 | @users = User.all 16 | render :index 17 | end 18 | 19 | def show 20 | @user = User.find_by(id: params['id']) 21 | render :show 22 | end 23 | 24 | 25 | def update 26 | @user = User.find(params['id']) 27 | if @user.update(user_params) 28 | login(@user) 29 | render :show 30 | else 31 | render json: @user.errors.full_messages, status: 422 32 | end 33 | end 34 | 35 | private 36 | 37 | def user_params 38 | params.require(:user).permit(:username,:password,:description,:cover_url,:profile_url,:new_notification) 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | 4 | helper_method :logged_in?, :current_user 5 | 6 | def current_user 7 | @current_user ||= User.find_by(session_token: session[:session_token]) 8 | end 9 | 10 | def login(user) 11 | @current_user = user 12 | session[:session_token] = user.reset_session_token! 13 | end 14 | 15 | def logout 16 | current_user.reset_session_token! 17 | session[:session_token] = nil 18 | end 19 | 20 | def logged_in? 21 | !!current_user 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | 3 | def root 4 | render :root 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /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/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/follow.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :bigint(8) not null, primary key 6 | # leader_id :integer not null 7 | # follower_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Follow < ApplicationRecord 13 | 14 | validates :leader_id, :follower_id, presence: true 15 | validates_uniqueness_of :leader_id, :scope => [:follower_id] 16 | validates_uniqueness_of :follower_id, :scope => [:leader_id] 17 | 18 | belongs_to :leader, 19 | primary_key: :id, 20 | foreign_key: :leader_id, 21 | class_name: 'User' 22 | 23 | belongs_to :follower, 24 | primary_key: :id, 25 | foreign_key: :follower_id, 26 | class_name: 'User' 27 | 28 | end 29 | -------------------------------------------------------------------------------- /app/models/like.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :bigint(8) not null, primary key 6 | # liker_id :integer not null 7 | # picture_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Like < ApplicationRecord 13 | 14 | validates :liker_id, :picture_id, presence: true 15 | validates_uniqueness_of :liker_id, :scope => [:picture_id] 16 | validates_uniqueness_of :picture, :scope => [:liker_id] 17 | 18 | 19 | 20 | # belongs_to :picture, 21 | # primary_key: :id, 22 | # foreign_key: :picture_id, 23 | # class_name: 'Picture' 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/models/notification.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: notifications 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # initiator_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # viewed :boolean 11 | # 12 | 13 | class Notification < ApplicationRecord 14 | 15 | validates :initiator_id, :user_id, presence: true 16 | 17 | belongs_to :user, 18 | primary_key: :id, 19 | foreign_key: :user_id, 20 | class_name: 'User' 21 | 22 | belongs_to :initiator, 23 | primary_key: :id, 24 | foreign_key: :initiator_id, 25 | class_name: 'User' 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/models/picture.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: pictures 4 | # 5 | # id :bigint(8) not null, primary key 6 | # title :string 7 | # image_url :string not null 8 | # uploader_id :integer not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Picture < ApplicationRecord 14 | 15 | validates :image_url, :uploader_id, presence: true 16 | 17 | belongs_to :user, 18 | primary_key: :id, 19 | foreign_key: :uploader_id, 20 | class_name: 'User' 21 | end 22 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint(8) not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # description :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # profile_url :string 13 | # cover_url :string 14 | # 15 | 16 | class User < ApplicationRecord 17 | attr_reader :password 18 | validates :username, :password_digest, :session_token, presence: true 19 | validates :username, uniqueness: true 20 | validates :password, length: {minimum: 6}, allow_nil: true 21 | before_validation :ensure_session_token 22 | 23 | has_many :pictures, 24 | primary_key: :id, 25 | foreign_key: :uploader_id, 26 | class_name: 'Picture' 27 | 28 | has_many :followers, 29 | primary_key: :id, 30 | foreign_key: :leader_id, 31 | class_name: 'Follow' 32 | 33 | has_many :followings, 34 | primary_key: :id, 35 | foreign_key: :follower_id, 36 | class_name: 'Follow' 37 | 38 | def get_notifications 39 | Notification.all.select{|notification| self.id == notification.user_id} 40 | end 41 | 42 | def get_fresh_feed 43 | followings = self.followings 44 | leader_ids = [] 45 | followings.each do |follow| 46 | leader_ids.push(follow.leader_id) 47 | end 48 | pictures = Picture.all.sort{|a,b| b.created_at <=> a.created_at} 49 | pictures.reject! {|picture| leader_ids.include?(picture.uploader_id) || picture.uploader_id == self.id} 50 | pictures.take(45) 51 | end 52 | 53 | def get_home_feed 54 | followings = self.followings 55 | pictures = [] 56 | followings.each do |follow| 57 | user = User.find(follow.leader_id) 58 | pictures += user.pictures 59 | end 60 | if pictures.length < 30 61 | other_pictures = Picture.all.sort {|a,b| b.created_at <=> a.created_at} 62 | count = 0 63 | other_pictures.each do |picture| 64 | unless count == 10 || picture.uploader_id === self.id || pictures.include?(picture) 65 | pictures.push(picture) 66 | count += 1 67 | end 68 | 69 | end 70 | end 71 | pictures.sort {|a,b| b.created_at <=> a.created_at}.take(30) 72 | end 73 | 74 | def self.find_by_credentials(username,password) 75 | user = User.find_by(username: username) 76 | return nil unless user 77 | user.is_password?(password) ? user : nil 78 | end 79 | 80 | def password=(password) 81 | @password = password 82 | self.password_digest = BCrypt::Password.create(password) 83 | end 84 | 85 | def is_password?(password) 86 | BCrypt::Password.new(self.password_digest).is_password?(password) 87 | end 88 | 89 | def ensure_session_token 90 | self.session_token ||= SecureRandom.urlsafe_base64 91 | end 92 | 93 | def reset_session_token! 94 | self.session_token = SecureRandom.urlsafe_base64 95 | self.save! 96 | self.session_token 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /app/views/api/feeds/fresh.json.jbuilder: -------------------------------------------------------------------------------- 1 | @pictures.each do |picture| 2 | json.set! picture.id do 3 | json.extract! picture, :id, :title, :image_url, :uploader_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/feeds/home.json.jbuilder: -------------------------------------------------------------------------------- 1 | @pictures.each do |picture| 2 | json.set! picture.id do 3 | json.extract! picture, :id, :title, :image_url, :uploader_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/follows/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @follows.each do |follow| 2 | json.set! follow.id do 3 | json.extract! follow, :id, :leader_id, :follower_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/follows/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @follow, :id, :leader_id, :follower_id 2 | -------------------------------------------------------------------------------- /app/views/api/notifications/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @notifications.each do |notification| 2 | json.set! notification.id do 3 | json.extract! notification, :id, :user_id, :initiator_id, :viewed 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/notifications/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @notification, :id, :user_id, :initiator_id, :viewed 2 | -------------------------------------------------------------------------------- /app/views/api/pictures/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @pictures.each do |picture| 2 | json.set! picture.id do 3 | json.extract! picture, :id, :title, :image_url, :uploader_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/pictures/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @picture, :id, :title, :image_url, :uploader_id 2 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :username, :description, :cover_url, :profile_url 2 | -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @users.each do |user| 2 | json.set! user.id do 3 | json.extract! user,:id, :username, :cover_url, :profile_url, :description 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.partial! "api/users/user", user: @user 3 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ThreeSixtyPx 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | <%= javascript_include_tag 'application' %> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | <%= yield %> 28 | 29 | 30 | -------------------------------------------------------------------------------- /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 | <% if logged_in? %> 2 | 8 | <% end %> 9 | 10 |
11 | -------------------------------------------------------------------------------- /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 | # Install JavaScript dependencies if using Yarn 22 | # system('bin/yarn') 23 | 24 | 25 | # puts "\n== Copying sample files ==" 26 | # unless File.exist?('config/database.yml') 27 | # cp 'config/database.yml.sample', 'config/database.yml' 28 | # end 29 | 30 | puts "\n== Preparing database ==" 31 | system! 'bin/rails db:setup' 32 | 33 | puts "\n== Removing old logs and tempfiles ==" 34 | system! 'bin/rails log:clear tmp:clear' 35 | 36 | puts "\n== Restarting application server ==" 37 | system! 'bin/rails restart' 38 | end 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | VENDOR_PATH = File.expand_path('..', __dir__) 3 | Dir.chdir(VENDOR_PATH) do 4 | begin 5 | exec "yarnpkg #{ARGV.join(" ")}" 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /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 | 10 | module ThreeSixtyPx 11 | class Application < Rails::Application 12 | # Initialize configuration defaults for originally generated Rails version. 13 | config.load_defaults 5.1 14 | 15 | # Settings in config/environments/* take precedence over those specified here. 16 | # Application configuration should go into files in config/initializers 17 | # -- all .rb files in that directory are automatically loaded. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /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 | channel_prefix: ThreeSixtyPx_production 11 | -------------------------------------------------------------------------------- /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: ThreeSixtyPx_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: ThreeSixtyPx 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: ThreeSixtyPx_test 61 | 62 | 63 | # As with config/secrets.yml, you never want to store sensitive information, 64 | # like your database password, in your source code. If your source code is 65 | # ever seen by anyone, they now have access to your database. 66 | # 67 | # Instead, provide the password as a unix environment variable when you boot 68 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 69 | # for a full rundown on how to provide these environment variables in a 70 | # production deployment. 71 | # 72 | # On Heroku and other platform providers, you may have a full connection URL 73 | # available as an environment variable. For example: 74 | # 75 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 76 | # 77 | # You can use this database configuration with: 78 | # 79 | # production: 80 | # url: <%= ENV['DATABASE_URL'] %> 81 | # 82 | production: 83 | <<: *default 84 | database: ThreeSixtyPx_production 85 | username: ThreeSixtyPx 86 | password: <%= ENV['THREESIXTYPX_DATABASE_PASSWORD'] %> 87 | -------------------------------------------------------------------------------- /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=#{2.days.seconds.to_i}" 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 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`. 18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or 19 | # `config/secrets.yml.key`. 20 | config.read_encrypted_secrets = true 21 | 22 | # Disable serving static files from the `/public` folder by default since 23 | # Apache or NGINX already handles this. 24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 25 | 26 | # Compress JavaScripts and CSS. 27 | config.assets.js_compressor = :uglifier 28 | # config.assets.css_compressor = :sass 29 | 30 | # Do not fallback to assets pipeline if a precompiled asset is missed. 31 | config.assets.compile = false 32 | 33 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 34 | 35 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 36 | # config.action_controller.asset_host = 'http://assets.example.com' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 41 | 42 | # Mount Action Cable outside main process or domain 43 | # config.action_cable.mount_path = nil 44 | # config.action_cable.url = 'wss://example.com/cable' 45 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 46 | 47 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 48 | # config.force_ssl = true 49 | 50 | # Use the lowest log level to ensure availability of diagnostic information 51 | # when problems arise. 52 | config.log_level = :debug 53 | 54 | # Prepend all log lines with the following tags. 55 | config.log_tags = [ :request_id ] 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Use a real queuing backend for Active Job (and separate queues per environment) 61 | # config.active_job.queue_adapter = :resque 62 | # config.active_job.queue_name_prefix = "ThreeSixtyPx_#{Rails.env}" 63 | config.action_mailer.perform_caching = false 64 | 65 | # Ignore bad email addresses and do not raise email delivery errors. 66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 67 | # config.action_mailer.raise_delivery_errors = false 68 | 69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 70 | # the I18n.default_locale when a translation cannot be found). 71 | config.i18n.fallbacks = true 72 | 73 | # Send deprecation notices to registered listeners. 74 | config.active_support.deprecation = :notify 75 | 76 | # Use default logging formatter so that PID and timestamp are not suppressed. 77 | config.log_formatter = ::Logger::Formatter.new 78 | 79 | # Use a different logger for distributed setups. 80 | # require 'syslog/logger' 81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 82 | 83 | if ENV["RAILS_LOG_TO_STDOUT"].present? 84 | logger = ActiveSupport::Logger.new(STDOUT) 85 | logger.formatter = config.log_formatter 86 | config.logger = ActiveSupport::TaggedLogging.new(logger) 87 | end 88 | 89 | # Do not dump schema after migrations. 90 | config.active_record.dump_schema_after_migration = false 91 | end 92 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 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 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /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 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /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/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 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. 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 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html 3 | 4 | namespace :api, defaults: {format: :json} do 5 | get '/feed', to: 'feeds#home', controller: 'feeds' 6 | get '/fresh', to: 'feeds#fresh', controller: 'feeds' 7 | resources :follows, only: [:index, :create, :destroy] 8 | resources :notifications, only: [:index, :create, :destroy, :update] 9 | resources :users, only: [:create,:update, :show, :index] do 10 | resources :pictures, only: [ :index, :create] 11 | end 12 | resources :pictures, only: [:show, :update, :destroy] 13 | resource :session, only: [:create, :destroy] 14 | end 15 | 16 | root 'static_pages#root' 17 | end 18 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: 8ed3602384faacfdf1eb9efaf65643899d9e2d26eda005d4d2b928a0e90d00c4afc8dc0ff2cf61b1b88c7a34a644e30cb0f9650e1e32999f0c271ec52eec5fe0 22 | 23 | test: 24 | secret_key_base: da72116f264d0c04b790341b7d17e8c721b85375dd09c75edb443596e8cda1459d2fdbb0cad7f7e3dfd8dc17e3d0620505c8c0a9a54e5f416c7d6ca85d3bd055 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /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/20180605195708_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null:false 5 | t.string :password_digest, null:false 6 | t.string :session_token, null:false 7 | t.string :profile_url, null:false 8 | t.string :cover_url, null:false 9 | t.string :description 10 | 11 | t.timestamps 12 | end 13 | add_index :users, :username, unique: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20180605211823_change_users.rb: -------------------------------------------------------------------------------- 1 | class ChangeUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :users, :profile_url 4 | remove_column :users, :cover_url 5 | add_column :users, :profile_url, :string 6 | add_column :users, :cover_url, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180607212713_create_pictures.rb: -------------------------------------------------------------------------------- 1 | class CreatePictures < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :pictures do |t| 4 | t.string :title 5 | t.string :image_url, null: false 6 | t.integer :uploader_id, null: false 7 | t.timestamps 8 | end 9 | add_index :pictures, :uploader_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180610022153_create_follows.rb: -------------------------------------------------------------------------------- 1 | class CreateFollows < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :follows do |t| 4 | t.integer :leader_id, null: false 5 | t.integer :follower_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :follows, :leader_id 10 | add_index :follows, :follower_id 11 | add_index :follows, [:leader_id, :follower_id], :unique => true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180613202647_create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateNotifications < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :notifications do |t| 4 | t.integer :user_id, null: false 5 | t.integer :initiator_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :notifications, :user_id 10 | add_index :notifications, :initiator_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180613203140_update_users.rb: -------------------------------------------------------------------------------- 1 | class UpdateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :users, :new_notification, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180614170848_update_notification.rb: -------------------------------------------------------------------------------- 1 | class UpdateNotification < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :notifications, :viewed, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180614172931_update_users2.rb: -------------------------------------------------------------------------------- 1 | class UpdateUsers2 < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :users, :new_notification 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180729043814_create_likes.rb: -------------------------------------------------------------------------------- 1 | class CreateLikes < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :likes do |t| 4 | t.integer :liker_id, null: false 5 | t.integer :picture_id, null: false 6 | 7 | t.timestamps 8 | end 9 | add_index :likes, :liker_id 10 | add_index :likes, :picture_id 11 | add_index :likes, [:liker_id, :picture_id], :unique => true 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /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: 20180729043814) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "follows", force: :cascade do |t| 19 | t.integer "leader_id", null: false 20 | t.integer "follower_id", null: false 21 | t.datetime "created_at", null: false 22 | t.datetime "updated_at", null: false 23 | t.index ["follower_id"], name: "index_follows_on_follower_id" 24 | t.index ["leader_id", "follower_id"], name: "index_follows_on_leader_id_and_follower_id", unique: true 25 | t.index ["leader_id"], name: "index_follows_on_leader_id" 26 | end 27 | 28 | create_table "likes", force: :cascade do |t| 29 | t.integer "liker_id", null: false 30 | t.integer "picture_id", null: false 31 | t.datetime "created_at", null: false 32 | t.datetime "updated_at", null: false 33 | t.index ["liker_id", "picture_id"], name: "index_likes_on_liker_id_and_picture_id", unique: true 34 | t.index ["liker_id"], name: "index_likes_on_liker_id" 35 | t.index ["picture_id"], name: "index_likes_on_picture_id" 36 | end 37 | 38 | create_table "notifications", force: :cascade do |t| 39 | t.integer "user_id", null: false 40 | t.integer "initiator_id", null: false 41 | t.datetime "created_at", null: false 42 | t.datetime "updated_at", null: false 43 | t.boolean "viewed" 44 | t.index ["initiator_id"], name: "index_notifications_on_initiator_id" 45 | t.index ["user_id"], name: "index_notifications_on_user_id" 46 | end 47 | 48 | create_table "pictures", force: :cascade do |t| 49 | t.string "title" 50 | t.string "image_url", null: false 51 | t.integer "uploader_id", null: false 52 | t.datetime "created_at", null: false 53 | t.datetime "updated_at", null: false 54 | t.index ["uploader_id"], name: "index_pictures_on_uploader_id" 55 | end 56 | 57 | create_table "users", force: :cascade do |t| 58 | t.string "username", null: false 59 | t.string "password_digest", null: false 60 | t.string "session_token", null: false 61 | t.string "description" 62 | t.datetime "created_at", null: false 63 | t.datetime "updated_at", null: false 64 | t.string "profile_url" 65 | t.string "cover_url" 66 | t.index ["username"], name: "index_users_on_username", unique: true 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | # 9 | 10 | User.destroy_all 11 | user1 = User.create({username: "Felix", password: "password", cover_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528916078/dabtje61xxqksoljk470.jpg", profile_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528916116/pxdpqlokeeyhonybowbv.jpg", description: Faker::Movie.quote }) 12 | user2 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", description: Faker::Movie.quote}) 13 | user3 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", description: Faker::Movie.quote}) 14 | user4 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", description: Faker::Movie.quote}) 15 | user5 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528776849/coex5jgohodc0mqgty5r.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528776350/boy4qqret5rpranjk18j.jpg", description: Faker::Movie.quote}) 16 | user6 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528776541/nren1oul9wgoqmsyeiux.jpg", description: Faker::Movie.quote}) 17 | user7 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528776272/ygty6yjd99z9n32bi0kn.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528776894/aljdnek7yzyjcaamtyje.jpg", description: Faker::Movie.quote}) 18 | user8 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528776588/bemxylyfht8kh8xhi7lh.jpg", description: Faker::Movie.quote}) 19 | user9 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528822826/freestocks-org-570360-unsplash.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528777304/dw2qed71upmhlolo769c.jpg", description: Faker::Movie.quote}) 20 | user10 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528494930/kulrpduwnhic8fbh9prf.png", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528490121/azwif34ctvpdbsbwtshp.jpg", description: Faker::Movie.quote}) 21 | user11 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528867401/asaf-r-467443-unsplash.jpg", cover_url:"", description: Faker::Movie.quote}) 22 | user12 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528867416/touzeey-302645-unsplash.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867310/valentino-funghi-275990-unsplash.jpg", description: Faker::Movie.quote}) 23 | user13 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528914135/jason-leung-667985-unsplash.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867310/valentino-funghi-275990-unsplash.jpg", description: Faker::Movie.quote}) 24 | user14 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1528914169/jordan-andrews-396838-unsplash.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528914153/jon-tyson-520952-unsplash.jpg", description: Faker::Movie.quote}) 25 | user15 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1529038851/ezra-comeau-jeffrey-661583-unsplash.jpg", cover_url:"", description: Faker::Movie.quote}) 26 | user16 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1529038852/brad-halcrow-590097-unsplash.jpg", cover_url:"", description: Faker::Movie.quote}) 27 | user17 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1529038882/demetrius-washington-670055-unsplash.jpg", cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038861/murray-campbell-29990-unsplash.jpg", description: Faker::Movie.quote}) 28 | user18 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1529079265/kyle-loftus-592128-unsplash.jpg", cover_url:"", description: Faker::Movie.quote}) 29 | user19 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1529079285/roberto-nickson-g-366181-unsplash.jpg", description: Faker::Movie.quote}) 30 | 31 | user20 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1530853920/blake-cheek-725235-unsplash.jpg",cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853927/bence-boros-725201-unsplash.jpg", description: Faker::Movie.quote}) 32 | user21 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1530853927/afa-ah-loo-724939-unsplash.jpg", description: Faker::Movie.quote}) 33 | user22 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1530853941/roberto-delgado-webb-724962-unsplash.jpg",cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853929/qi-bin-725231-unsplash.jpg", description: Faker::Movie.quote}) 34 | user23 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1530853946/stephany-lorena-726155-unsplash.jpg", description: Faker::Movie.quote}) 35 | user24 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1530853952/yiran-ding-726220-unsplash.jpg", description: Faker::Movie.quote}) 36 | user25 = User.create({username: Faker::Twitter.unique.screen_name, password: "password", profile_url: "https://res.cloudinary.com/dbm56y2y/image/upload/v1530853943/liliia-beda-724286-unsplash.jpg",cover_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853932/j-basiga-724297-unsplash.jpg", description: Faker::Movie.quote}) 37 | 38 | 39 | Picture.destroy_all 40 | Picture.create({title: "Made it to sf",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528915569/james-donovan-150590-unsplash.jpg", uploader_id: user1.id}) 41 | Picture.create({title: "Outdoor adventures",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528914897/luo-lei-625155-unsplash_2.jpg", uploader_id: user2.id }) 42 | Picture.create({title: "Ready for new years",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528914995/chuttersnap-336571-unsplash.jpg", uploader_id: user3.id }) 43 | Picture.create({title: "Finally Here!",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528915078/thomas-ribaud-665075-unsplash.jpg", uploader_id: user4.id }) 44 | Picture.create({title: "Love this city",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528915184/sasha-stories-267948-unsplash.jpg", uploader_id: user5.id }) 45 | Picture.create({title: "Pure relaxation",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528915244/seth-doyle-133176-unsplash.jpg", uploader_id: user6.id }) 46 | 47 | 48 | Picture.create({title: "Morning Essentials",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528776389/zjc06v3xthn94njm8vnl.jpg", uploader_id: user7.id }) 49 | Picture.create({title: "Beauty in the night",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528777129/s9imhavzxntyo3y7gmcc.jpg", uploader_id: user8.id }) 50 | Picture.create({title: "never look back",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528776631/qfeakv2w9rdfbhkvbpff.jpg", uploader_id: user9.id }) 51 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528915367/wenni-zhou-463395-unsplash.jpg", uploader_id: user10.id }) 52 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528777262/qevtrfepysdcivhcn41b.jpg", uploader_id: user11.id }) 53 | Picture.create({title: "beauty is art",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528822802/scott-webb-271478-unsplash.jpg", uploader_id: user12.id }) 54 | Picture.create({title: "The joys of brunch",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528776915/zu1koeinx1axioxafjih.jpg", uploader_id: user13.id }) 55 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528825624/ya74xwyn1gxspcpdrhqw.jpg", uploader_id: user14.id }) 56 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528827269/erik-witsoe-366860-unsplash.jpg", uploader_id: user15.id }) 57 | Picture.create({title: "last night was awesome",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528825718/nouv1lfig99eth0iatrl.jpg", uploader_id: user16.id }) 58 | Picture.create({title: "nature speaks",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867316/alex-koch-513845-unsplash.jpg", uploader_id: user17.id }) 59 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867285/dulcey-lima-456862-unsplash.jpg", uploader_id: user18.id }) 60 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867266/braden-jarvis-625463-unsplash.jpg", uploader_id: user19.id }) 61 | Picture.create({title: "Wedding Bells",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867264/callie-morgan-285271-unsplash.jpg", uploader_id: user20.id }) 62 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528867278/eric-ward-304333-unsplash.jpg", uploader_id: user21.id }) 63 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528914160/florin-kozma-678292-unsplash.jpg", uploader_id: user22.id }) 64 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528914176/joshua-fuller-204247-unsplash.jpg", uploader_id: user23.id }) 65 | Picture.create({title: "No adventure is too far off",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1528914153/jon-tyson-520952-unsplash.jpg", uploader_id: user24.id }) 66 | Picture.create({title: "My little happy place",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038894/allef-vinicius-253985-unsplash.jpg", uploader_id: user25.id }) 67 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038873/vladimir-kudinov-61639-unsplash.jpg", uploader_id: user1.id }) 68 | Picture.create({title: "Pure Solice",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038913/jorge-flores-223750-unsplash.jpg", uploader_id: user2.id }) 69 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038926/fancycrave-225507-unsplash.jpg", uploader_id: user3.id }) 70 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038906/pietro-de-grandi-329892-unsplash.jpg", uploader_id: user4.id }) 71 | Picture.create({title: "This trip was well worth it",image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529038920/liam-pozz-247954-unsplash.jpg", uploader_id: user5.id }) 72 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529079276/annie-spratt-460959-unsplash.jpg", uploader_id: user6.id }) 73 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529079292/amber-teasley-369443-unsplash.jpg", uploader_id: user7.id }) 74 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853931/ricardo-l-tamayo-725857-unsplash.jpg", uploader_id: user8.id }) 75 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853933/jakub-dziubak-724845-unsplash.jpg", uploader_id: user9.id }) 76 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853933/iswanto-arif-726203-unsplash.jpg", uploader_id: user10.id }) 77 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853938/karl-fredrickson-724710-unsplash.jpg", uploader_id: user11.id }) 78 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853938/derek-story-724325-unsplash.jpg", uploader_id: user12.id }) 79 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853938/ben-konfrst-724459-unsplash.jpg", uploader_id: user13.id }) 80 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853941/julie-johnson-723618-unsplash.jpg", uploader_id: user14.id }) 81 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853942/rosie-kerr-726224-unsplash.jpg", uploader_id: user15.id }) 82 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853942/karl-fredrickson-724719-unsplash.jpg", uploader_id: user16.id }) 83 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853944/mads-schmidt-723416-unsplash.jpg", uploader_id: user17.id }) 84 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853947/marko-blazevic-725854-unsplash.jpg", uploader_id: user18.id }) 85 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853946/shridhar-gupta-724669-unsplash.jpg", uploader_id: user19.id }) 86 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853949/victor-rodriguez-726159-unsplash.jpg", uploader_id: user20.id }) 87 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853952/mesut-kaya-724174-unsplash.jpg", uploader_id: user21.id }) 88 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853956/qi-bin-725216-unsplash.jpg", uploader_id: user22.id }) 89 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530853965/wes-hicks-725738-unsplash.jpg", uploader_id: user23.id }) 90 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530901252/da-kraplak-727110-unsplash.jpg", uploader_id: user24.id }) 91 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530901254/grillot-edouard-726756-unsplash.jpg", uploader_id: user25.id }) 92 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530901254/usukhbayar-gankhuyag-726907-unsplash.jpg", uploader_id: user1.id }) 93 | Picture.create({title: Faker::Movie.quote,image_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1530901255/peter-oslanec-726455-unsplash.jpg", uploader_id: user2.id }) 94 | 95 | Follow.create({leader_id:user3.id, follower_id:user1.id}) 96 | Follow.create({leader_id:user22.id, follower_id:user1.id}) -------------------------------------------------------------------------------- /frontend/actions/follow_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIFollows from '../util/follow_util'; 2 | 3 | 4 | export const RECEIVE_FOLLOW = 'RECEIVE_FOLLOW'; 5 | export const REMOVE_FOLLOW = 'REMOVE_FOLLOW'; 6 | export const RECEIVE_FOLLOWS = 'RECEIVE_FOLLOWS'; 7 | 8 | export const receiveFollow = (follow) => ({ 9 | type: RECEIVE_FOLLOW, 10 | follow 11 | }); 12 | 13 | export const receiveFollows = (follows) => ({ 14 | type: RECEIVE_FOLLOWS, 15 | follows 16 | }); 17 | 18 | export const removeFollow = (follow) => ({ 19 | type: REMOVE_FOLLOW, 20 | follow 21 | }); 22 | 23 | export const fetchFollows = () => dispatch => ( 24 | APIFollows.fetchFollows().then(follows => dispatch(receiveFollows(follows))) 25 | ); 26 | 27 | export const createFollow = (follow) => dispatch => ( 28 | APIFollows.createFollow(follow).then(follow => { 29 | return dispatch(receiveFollow(follow)); 30 | }) 31 | ); 32 | 33 | export const deleteFollow = (id) => dispatch => ( 34 | APIFollows.deleteFollow(id).then(follow => dispatch(removeFollow(follow))) 35 | ); 36 | -------------------------------------------------------------------------------- /frontend/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | export const OPEN_MODAL = 'OPEN_MODAL'; 2 | export const CLOSE_MODAL = 'CLOSE_MODAL'; 3 | 4 | export const openModal = modal => { 5 | return { 6 | type: OPEN_MODAL, 7 | modal 8 | }; 9 | }; 10 | 11 | export const closeModal = () => { 12 | return { 13 | type: CLOSE_MODAL 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/actions/notification_actions.js: -------------------------------------------------------------------------------- 1 | import * as APINotifications from '../util/notification_util'; 2 | 3 | 4 | export const RECEIVE_NOTIFICATION = 'RECEIVE_NOTIFICATION'; 5 | export const RECEIVE_NOTIFICATIONS = 'RECEIVE_NOTIFICATIONS'; 6 | 7 | export const receiveNotification = (notification) => ( 8 | { 9 | type: RECEIVE_NOTIFICATION, 10 | notification 11 | } 12 | ); 13 | // { 14 | // return { 15 | // type: RECEIVE_NOTIFICATION, 16 | // notification 17 | // }; 18 | // }; 19 | 20 | export const receiveNotifications = (notifications) => ({ 21 | type: RECEIVE_NOTIFICATIONS, 22 | notifications 23 | }); 24 | 25 | export const fetchNotifications = () => dispatch => ( 26 | APINotifications.fetchNotifications().then(notifications => dispatch(receiveNotifications(notifications))) 27 | ); 28 | 29 | export const createNotification = (notification) => dispatch => { 30 | return APINotifications.createNotification(notification).then(notification => { 31 | return dispatch(receiveNotification(notification)); 32 | }); 33 | }; 34 | 35 | export const updateNotification = (notification) => dispatch => { 36 | return APINotifications.updateNotification(notification).then(notification => { 37 | return dispatch(receiveNotification(notification)); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/actions/picture_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIPictures from '../util/picture_api_util'; 2 | import * as APIFeed from '../util/feed_util'; 3 | import RECEIVE_USERS from './session_actions'; 4 | export const RECEIVE_PICTURE = 'RECEIVE_PICTURE'; 5 | export const RECEIVE_PICTURES = 'RECEIVE_PICTURES'; 6 | export const CLEAR_PICTURES = 'CLEAR_PICTURES'; 7 | export const REMOVE_PICTURE = 'REMOVE_PICTURE'; 8 | 9 | 10 | export const receivePictures = (pictures) => ({ 11 | type: RECEIVE_PICTURES, 12 | pictures 13 | }); 14 | 15 | export const receivePicture = (picture) => { 16 | 17 | return {type: RECEIVE_PICTURE, 18 | picture}; 19 | }; 20 | 21 | export const removePicture = (picture) => { 22 | 23 | return {type: REMOVE_PICTURE, 24 | picture}; 25 | }; 26 | 27 | export const clearPictures = () => { 28 | return {type: CLEAR_PICTURES}; 29 | }; 30 | 31 | export const getUserPictures = (userId) => dispatch => { 32 | return APIPictures.fetchUserPictures(userId).then(pictures => dispatch(receivePictures(pictures))); 33 | }; 34 | 35 | export const getUserPicture = (pictureId) => dispatch => { 36 | return APIPictures.fetchUserPicture(pictureId).then(picture => dispatch(receivePicture(picture))); 37 | }; 38 | 39 | export const uploadPicture = (picture, userId) => dispatch => { 40 | return APIPictures.uploadPicture(picture, userId).then(picture => { 41 | return dispatch(receivePicture(picture)); 42 | }); 43 | }; 44 | 45 | export const updatePicture = (picture) => dispatch => { 46 | return APIPictures.updatePicture(picture).then(picture => { 47 | return dispatch(receivePicture(picture)); 48 | }); 49 | }; 50 | 51 | export const deletePicture = (pictureId) => dispatch => { 52 | return APIPictures.deletePicture(pictureId).then(picture => dispatch(removePicture(picture))); 53 | }; 54 | 55 | export const homeFeed = () => dispatch => { 56 | return APIFeed.homeFeed().then(pictures => { 57 | dispatch(receivePictures(pictures)); 58 | } 59 | ); 60 | }; 61 | 62 | export const discoverFeed = () => dispatch => { 63 | return APIFeed.discoverFeed().then(pictures => { 64 | dispatch(receivePictures(pictures)); 65 | } 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 2 | export const LOGOUT_CURRENT_USER = 'LOGOUT_CURRENT_USER'; 3 | export const RECEIVE_ERRORS = 'RECEIVE_ERRORS'; 4 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 5 | export const RECEIVE_USER = 'RECEIVE_USER'; 6 | export const REMOVE_USER = 'CLEAR_USER'; 7 | export const RECEIVE_USERS = 'RECEIVE_USERS'; 8 | import * as APIUtil from '../util/session_api_util'; 9 | import {fetchFollows} from './follow_actions'; 10 | import {fetchNotifications} from './notification_actions'; 11 | 12 | export const receiveCurrentUser = (currentUser) => { 13 | return { 14 | type: RECEIVE_CURRENT_USER, 15 | currentUser 16 | }; 17 | }; 18 | 19 | export const logoutCurrentUser = () => { 20 | return { 21 | type: LOGOUT_CURRENT_USER, 22 | }; 23 | }; 24 | 25 | export const receiveErrors = (errors) => { 26 | return { 27 | type: RECEIVE_ERRORS, 28 | errors 29 | }; 30 | }; 31 | 32 | export const clearErrors = () => { 33 | return { 34 | type: CLEAR_ERRORS 35 | 36 | }; 37 | }; 38 | 39 | export const receiveUser = (user) => { 40 | return { 41 | type: RECEIVE_USER, 42 | user 43 | }; 44 | }; 45 | 46 | export const removeUser = (user) => { 47 | return {type: REMOVE_USER, user}; 48 | }; 49 | 50 | export const receiveUsers = (users) => { 51 | return {type: RECEIVE_USERS, users}; 52 | }; 53 | 54 | export const getUser = (user) => dispatch => { 55 | return APIUtil.fetchUser(user) 56 | .then((user) => 57 | dispatch(receiveUser(user))); 58 | }; 59 | 60 | 61 | export const login = (user) => dispatch => { 62 | return APIUtil.login(user) 63 | .then((currentUser) => { 64 | dispatch(fetchUsers()); 65 | dispatch(fetchNotifications()); 66 | return dispatch(receiveCurrentUser(currentUser));}, 67 | (errors) => dispatch(receiveErrors(errors.responseJSON)) 68 | ); 69 | }; 70 | 71 | export const logout = () => dispatch => { 72 | return APIUtil.logout() 73 | .then(() => dispatch(logoutCurrentUser())); 74 | }; 75 | 76 | export const signup = (user) => dispatch => ( 77 | APIUtil.signup(user).then((currentUser) => { 78 | dispatch(fetchUsers()); 79 | return dispatch(receiveCurrentUser(currentUser));},(errors) => dispatch(receiveErrors(errors.responseJSON)))); 80 | 81 | export const fetchUsers = () => dispatch => { 82 | return APIUtil.fetchUsers().then((users) => dispatch(receiveUsers(users))); 83 | }; 84 | 85 | export const updateUser = (user) => dispatch => ( 86 | APIUtil.updateUser(user).then((currentUser) => dispatch(receiveCurrentUser(currentUser)),(errors) => dispatch(receiveErrors(errors.responseJSON)))); 87 | 88 | export const updateUserNotification = (user) => dispatch => ( 89 | APIUtil.updateUser(user).then((user) => dispatch(receiveUser(user)),(errors) => dispatch(receiveErrors(errors.responseJSON)))); 90 | 91 | // APIUtil.signup(user) 92 | // .then((currentUser) => 93 | // dispatch(receiveCurrentUser(currentUser)), 94 | // (errors) => dispatch(receiveErrors(errors.responseJSON))) 95 | -------------------------------------------------------------------------------- /frontend/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GreetingContainer from './greeting/greeting_container'; 3 | import LoginFormContainer from './session/login_form_container'; 4 | import SignupFormContainer from './session/signup_form_container'; 5 | import Home from './home'; 6 | import {Route, Switch} from 'react-router'; 7 | import {AuthRoute, LogRoute} from '../util/route_util'; 8 | import FeedContainer from './feed/feed_container'; 9 | import ProfileContainer from './profile/profile_container'; 10 | import Modal from './modal/modal'; 11 | import DiscoverContainer from './discover/discover_container'; 12 | 13 | 14 | const App = () => ( 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ); 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /frontend/components/discover/discover_component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavContainer from '../nav/nav_container'; 3 | import DiscoverFeedItem from './discover_feed_item'; 4 | 5 | class DiscoverComponent extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount () { 11 | const {fetchFollows, discoverFeed, fetchUsers} = this.props; 12 | 13 | fetchFollows(); 14 | discoverFeed(); 15 | fetchUsers(); 16 | } 17 | 18 | render() { 19 | const {pictures, follows, currentUser, users, clearPictures, openModal, parseUrl} = this.props; 20 | const pictureList = pictures.map((picture =>{ 21 | return( ); 22 | })); 23 | return ( 24 |
25 | 26 |
27 |

The Newest Pictures

28 |

Discover the the freshest photos just added to 360px

29 |
30 |
    31 |
    32 | {pictureList} 33 |
    34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default DiscoverComponent; 43 | -------------------------------------------------------------------------------- /frontend/components/discover/discover_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import DiscoverComponent from './discover_component'; 4 | import {getUserPictures, discoverFeed} from '../../actions/picture_actions'; 5 | import {fetchUsers} from '../../actions/session_actions'; 6 | import {fetchFollows} from '../../actions/follow_actions'; 7 | import {pictureList, parseUrl} from '../../util/selectors'; 8 | import {clearPictures} from '../../actions/picture_actions'; 9 | import { openModal} from '../../actions/modal_actions'; 10 | 11 | 12 | const mapStateToProps = ({follows, entities, session}) => ( 13 | { 14 | currentUser: entities.users[session.id], 15 | pictures: pictureList(entities.pictures), 16 | users: entities.users, 17 | follows: follows, 18 | parseUrl: parseUrl 19 | }); 20 | 21 | const mapDispatchToProps = () => dispatch => ({ 22 | getUserPictures: (userId) => dispatch(getUserPictures(userId)), 23 | fetchFollows: (follows) => dispatch(fetchFollows(follows)), 24 | discoverFeed: () => dispatch(discoverFeed()), 25 | fetchUsers: () => dispatch(fetchUsers()), 26 | clearPictures: () => dispatch(clearPictures()), 27 | openModal: (modal) => dispatch(openModal(modal)) 28 | }); 29 | 30 | export default connect(mapStateToProps, mapDispatchToProps)(DiscoverComponent); 31 | -------------------------------------------------------------------------------- /frontend/components/discover/discover_feed_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | 4 | class DiscoverFeedItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | profileDirect(e) { 10 | e.stopPropagation(); 11 | } 12 | 13 | render() { 14 | const {picture, openModal, user, clearPictures, parseUrl} = this.props; 15 | const defaultUser = user || {id:"", username:"", profile_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"}; 16 | const profile = defaultUser.profile_url || "https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"; 17 | return ( 18 |
19 |
  • openModal({string:'show',picture: picture, user: user})}> 20 |
    21 | clearPictures()} to={`/profile/${picture.uploader_id}`}> 22 | this.profileDirect(e)} className="discover-profile-icon" src={profile}/> 23 | 24 | this.profileDirect(e)} className={`show-link`} to={`/profile/${picture.uploader_id}`}> 25 | {defaultUser.username} 26 | 27 |
    28 |
  • 29 | 30 |
    31 | ); 32 | } 33 | } 34 | 35 | export default DiscoverFeedItem; 36 | -------------------------------------------------------------------------------- /frontend/components/feed/feed.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavContainer from '../nav/nav_container'; 3 | import {Link} from 'react-router-dom'; 4 | import FeedListItem from './feed_list_item'; 5 | 6 | class FeedComponent extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount () { 12 | const {fetchFollows, homeFeed, fetchUsers} = this.props; 13 | fetchFollows(); 14 | homeFeed(); 15 | fetchUsers(); 16 | } 17 | 18 | render(){ 19 | const {users,clearPictures, openModal, currentUser, follows, parseUrlBig} = this.props; 20 | const pictures = this.props.pictures.map((picture) => { 21 | if (currentUser.id !== picture.uploader_id){ 22 | return ; 23 | } 24 | }); 25 | return( 26 |
    27 | 28 |
      29 | {pictures} 30 |
    31 |
    32 |
    33 |
    34 | ); 35 | } 36 | } 37 | 38 | export default FeedComponent; 39 | -------------------------------------------------------------------------------- /frontend/components/feed/feed_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import Feed from './feed'; 4 | import {getUserPictures, homeFeed} from '../../actions/picture_actions'; 5 | import {fetchUsers} from '../../actions/session_actions'; 6 | import {fetchFollows} from '../../actions/follow_actions'; 7 | import {pictureList, parseUrlBig} from '../../util/selectors'; 8 | import {clearPictures} from '../../actions/picture_actions'; 9 | import { openModal} from '../../actions/modal_actions'; 10 | 11 | 12 | const mapStateToProps = ({follows, entities, session}) => ({ 13 | currentUser: entities.users[session.id], 14 | pictures: pictureList(entities.pictures), 15 | users: entities.users, 16 | follows: follows, 17 | parseUrlBig: parseUrlBig 18 | }); 19 | 20 | const mapDispatchToProps = () => dispatch => ({ 21 | getUserPictures: (userId) => dispatch(getUserPictures(userId)), 22 | fetchFollows: (follows) => dispatch(fetchFollows(follows)), 23 | homeFeed: () => dispatch(homeFeed()), 24 | fetchUsers: () => dispatch(fetchUsers()), 25 | clearPictures: () => dispatch(clearPictures()), 26 | openModal: (modal) => dispatch(openModal(modal)) 27 | }); 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(Feed); 30 | -------------------------------------------------------------------------------- /frontend/components/feed/feed_list_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import {findFollow} from '../../util/selectors'; 4 | 5 | class FeedListItem extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | suggested() { 11 | const {follows, currentUser, user} = this.props; 12 | if (user){ 13 | if (findFollow(follows, currentUser.id, user.id)){ 14 | return; 15 | } else { 16 | return ( " (suggested)" ); 17 | } 18 | } 19 | } 20 | feedTitle() { 21 | const {picture} = this.props; 22 | if (picture.title === "") { 23 | return(
    title
    ); 24 | }else { 25 | return (
    {picture.title}
    ); 26 | } 27 | } 28 | 29 | render() { 30 | const {picture, user, clearPictures, openModal, currentUser, follows, parseUrlBig} = this.props; 31 | const defaultUser = user || {id:"", username: "", profile_url:"https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"}; 32 | const profile = defaultUser.profile_url || "https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"; 33 | return ( 34 |
    35 | clearPictures()} to={`/profile/${picture.uploader_id}`}> 36 | {defaultUser.username}{this.suggested()} 37 |
  • openModal({string:'show',picture: picture, user: user})}> 38 | 39 |
  • 40 | {this.feedTitle()} 41 | 42 |
    43 | ); 44 | } 45 | } 46 | 47 | export default FeedListItem; 48 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const greeting = (props) => { 5 | if (props.currentUser){ 6 | return ( 7 |
    8 |

    Welcome {props.currentUser.username}!!!

    9 | 10 |
    11 | ); 12 | } else { 13 | return ( 14 |
    15 |

    Welcome!! Your can log in or sign up here :

    16 | Signup 17 | Login 18 |
    19 | ); 20 | } 21 | }; 22 | 23 | export default greeting; 24 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import Greeting from './greeting'; 4 | 5 | const mapStateToProps = ({entities, session}) => ({ 6 | currentUser: entities.users[session.id] 7 | }); 8 | 9 | const mapDispatchToProps = () => dispatch => ({ 10 | logout: () => dispatch(logout()) 11 | }); 12 | 13 | export default connect(mapStateToProps, mapDispatchToProps)(Greeting); 14 | -------------------------------------------------------------------------------- /frontend/components/home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavContainer from './nav/nav_container'; 3 | import {Link} from 'react-router-dom'; 4 | import {Redirect} from 'react-router'; 5 | 6 | const Home = () => { 7 | 8 | return ( 9 | 10 |
    11 | 12 | 13 |
    14 |

    Find inspiration and discover the world through gorgeous photos

    15 |

    Share your vision with the world

    16 |
    17 | 18 |
    19 |
    20 |
    21 | ); 22 | }; 23 | 24 | export default Home; 25 | -------------------------------------------------------------------------------- /frontend/components/modal/modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { closeModal } from '../../actions/modal_actions'; 3 | import { connect } from 'react-redux'; 4 | import UploadPictureContainer from '../upload/upload_picture_container'; 5 | import EditPictureContainer from '../show/edit_picture_container'; 6 | import ShowPictureContainer from '../show/show_picture_container'; 7 | import {Animated} from "react-animated-css"; 8 | import EditUserContainer from '../profile/edit_user_container'; 9 | import UserPhotoContainer from '../profile/user_photo_container'; 10 | import NotificationModalContainer from '../notification/notification_modal_container'; 11 | 12 | 13 | const Modal = ({modal, closeModal}) => { 14 | if (!modal) { 15 | return null; 16 | } 17 | let component; 18 | switch (modal.string) { 19 | case 'upload': 20 | component = ; 21 | break; 22 | case 'edit': 23 | component = ; 24 | break; 25 | case 'show': 26 | component = ; 27 | break; 28 | case 'editUser': 29 | component = ; 30 | break; 31 | case 'uploadProfile': 32 | component = ; 33 | break; 34 | case 'uploadCover': 35 | component = ; 36 | break; 37 | case 'notify': 38 | component = ; 39 | break; 40 | default: 41 | return null; 42 | } 43 | if (modal.string === 'upload' || modal.string === 'edit' ){ 44 | return ( 45 |
    46 |
    e.stopPropagation()}> 47 | { component } 48 |
    49 |
    50 | ); 51 | } else if (modal.string === 'show') { 52 | return ( 53 |
    54 |
    e.stopPropagation()}> 55 | { component } 56 |
    57 |
    58 | ); 59 | } else if ( modal.string === 'editUser') { 60 | return ( 61 |
    62 |
    e.stopPropagation()}> 63 | { component } 64 |
    65 |
    66 | ); 67 | } else if ( modal.string ==='uploadProfile' || modal.string === 'uploadCover' ) { 68 | return ( 69 |
    70 |
    e.stopPropagation()}> 71 | { component } 72 |
    73 |
    74 | ); 75 | } else if ( modal.string ==='notify' ) { 76 | return ( 77 | //
    78 |
    e.stopPropagation()}> 79 | { component } 80 |
    81 | //
    82 | ); 83 | } 84 | }; 85 | 86 | const mapStateToProps = state => { 87 | return { 88 | modal: state.ui.modal 89 | }; 90 | }; 91 | 92 | const mapDispatchToProps = dispatch => { 93 | return { 94 | closeModal: () => dispatch(closeModal()) 95 | }; 96 | }; 97 | 98 | export default connect(mapStateToProps, mapDispatchToProps)(Modal); 99 | -------------------------------------------------------------------------------- /frontend/components/nav/nav_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | 4 | class NavBar extends React.Component { 5 | 6 | componentDidMount() { 7 | if(this.props.currentUser){ 8 | this.props.fetchNotifications(); 9 | this.props.fetchUsers(); 10 | } 11 | } 12 | 13 | 14 | nav(){ 15 | const path = this.props.match.path; 16 | if (path === "/"){ 17 | return "nav_bar"; 18 | }else { 19 | return "session_bar"; 20 | } 21 | } 22 | logo(){ 23 | 24 | const path = this.props.match.path; 25 | if (path === "/"){ 26 | return ( 27 | 360px 28 | ); 29 | }else if (path === "/login" || path === "/signup") { 30 | return ( 31 | 360px 32 | ); 33 | } 34 | else if (path === "/feed") { 35 | return ( 36 | 360px 37 | ); 38 | } 39 | else if (path.includes("profile") || path === "/discover" || path === "/notifications") { 40 | return ( 41 | this.props.clearPictures()} className="profile_logo">360px 42 | ); 43 | } 44 | } 45 | 46 | prepareFeed() { 47 | // const {clearPictures, homePage} = this.props; 48 | // clearPictures(); 49 | // homePage(); 50 | } 51 | 52 | // updateState(num){ 53 | // this.props.clearPictures(); 54 | // this.props.getUserPictures(num); 55 | // this.props.getUser(num); 56 | // } 57 | 58 | 59 | sideLink(){ 60 | const path = this.props.match.path; 61 | const{getUserPictures, currentUser, clearPictures} = this.props; 62 | if (path === "/"){ 63 | return ( 64 | Log in 65 | ); 66 | } else if (path === "/feed" || path.includes("profile") || path === "/discover" || path === "/notifications") { 67 | const defaultProfile = this.props.currentUser.profile_url || "https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"; 68 | return ( 69 |
    70 | clearPictures()} to={`/profile/${this.props.currentUser.id}`} className = "profile-link"> 71 |
      72 |
    • clearPictures()} to={`/profile/${currentUser.id}`} className = "profile-link">Profile
    • 73 |
    • this.handleLogout()}>Logout
    • 74 |
    75 |
    76 | ); 77 | } 78 | } 79 | 80 | handleLogout() { 81 | this.props.closeModal(); 82 | this.props.logout(); 83 | 84 | } 85 | 86 | sideButton() { 87 | const path = this.props.match.path; 88 | if (path === "/"){ 89 | return ( 90 | 91 | ); 92 | }else if (path === "/login") { 93 | return ( 94 | 95 | ); 96 | } else if (path === "/signup") { 97 | return ( 98 | 99 | ); 100 | }else if (path === "/feed" || path.includes("profile") || path === "/discover" || path === "/notifications") { 101 | return ( 102 | 103 | ); 104 | } 105 | } 106 | 107 | leftNav(){ 108 | const path = this.props.match.path; 109 | if (path === "/"){ 110 | return "left_nav"; 111 | }else { 112 | return "login_left"; 113 | } 114 | } 115 | 116 | uploadPicture(e) { 117 | e.preventDefault(); 118 | const currentUserId = this.props.currentUser.id; 119 | cloudinary.openUploadWidget(window.cloudinary_options, (error, picture) => { 120 | if (error === null){ 121 | this.props.openModal({string:'upload', picture: picture[0]}); 122 | } 123 | }); 124 | } 125 | discover() { 126 | const path = this.props.match.path; 127 | if (path === "/" || path === "/login" || path === "/signup") { 128 | return (
  • Discover
  • ); 129 | } else if (path === "/feed" || path.includes("profile") || path === "/notifications"){ 130 | return (
  • this.props.clearPictures()} className="discover-text" to="/discover">Discover
  • ); 131 | } else if (path === "/discover") { 132 | return (
  • Discover
  • ); 133 | } 134 | } 135 | 136 | bell() { 137 | const {openModal, newNotifications} = this.props; 138 | const path = this.props.match.path; 139 | const length = this.props.newNotifications.length; 140 | if (length === 0 && (path === "/feed" || path.includes("profile") || path === "/discover" || path === "/notifications") ) { 141 | return ( openModal({string: 'notify', newNotifications: newNotifications})} className="bell">); 142 | } else if (length > 0 && (path === "/feed" || path.includes("profile") || path === "/discover" || path === "/notifications") ) { 143 | return (
    this.notifyClick()} className="notify">{length}
    ); 144 | 145 | } 146 | } 147 | 148 | notifyClick() { 149 | const {newNotifications,updateNotification, openModal, notifications} = this.props; 150 | newNotifications.forEach((n) => { 151 | notifications.forEach((n2) => { 152 | if (n.user_id === n2.user_id && n2.initiator_id === n.initiator_id && n.id !== n2.id){ 153 | updateNotification({id:n2.id, viewed: true}); 154 | } 155 | }); 156 | updateNotification({id:n.id, viewed: true}); 157 | }); 158 | openModal({string: 'notify', newNotifications: newNotifications}); 159 | } 160 | 161 | rightNav() { 162 | const path = this.props.match.path; 163 | if (path === "/" || path === "/login" || path === "/signup"){ 164 | return "right_nav"; 165 | } else { 166 | return "login-right-nav"; 167 | } 168 | } 169 | 170 | 171 | render() { 172 | return ( 173 | 184 | ); 185 | } 186 | } 187 | 188 | export default withRouter(NavBar); 189 | -------------------------------------------------------------------------------- /frontend/components/nav/nav_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { signup, login, logout } from '../../actions/session_actions'; 3 | import { Link } from 'react-router-dom'; 4 | import React from 'react'; 5 | import NavBar from './nav_bar'; 6 | import {uploadPicture} from '../../actions/picture_actions'; 7 | import { openModal, closeModal} from '../../actions/modal_actions'; 8 | import {getUserPictures, clearPictures, homePage} from '../../actions/picture_actions'; 9 | import {getUser,fetchUsers} from '../../actions/session_actions'; 10 | import {fetchNotifications, updateNotification} from '../../actions/notification_actions'; 11 | import {newNotifications, notifyArray} from '../../util/selectors'; 12 | 13 | const mapStateToProps = ({entities, session, notifications}, ownProps) => { 14 | return {currentUser: entities.users[session.id], 15 | profileUser: ownProps.user, 16 | newNotifications: newNotifications(notifications), 17 | notifications: notifyArray(notifications) 18 | }; 19 | }; 20 | const mapDispatchToProps = () => dispatch => ({ 21 | logout: () => dispatch(logout()), 22 | uploadPicture: (picture, userId) => dispatch(uploadPicture(picture, userId)), 23 | openModal: (modal) => dispatch(openModal(modal)), 24 | getUserPictures: (userId) => dispatch(getUserPictures(userId)), 25 | getUser: (userId) => dispatch(getUser(userId)), 26 | clearPictures: () => dispatch(clearPictures()), 27 | homePage: () => dispatch(homePage()), 28 | fetchNotifications: () => dispatch(fetchNotifications()), 29 | closeModal: () => dispatch(closeModal()), 30 | updateNotification: (notification) => dispatch(updateNotification(notification)), 31 | fetchUsers: () => dispatch(fetchUsers()) 32 | }); 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(NavBar); 35 | -------------------------------------------------------------------------------- /frontend/components/notification/notification_modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import NotifyModalListItem from './notify_modal_list_item'; 4 | 5 | class NotifyModal extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | componentDidMount(){ 11 | 12 | document.addEventListener('keydown', (e) => { 13 | if (e.keyCode === 27) this.props.closeModal(); 14 | }); 15 | 16 | let notifyModal = document.getElementsByClassName('modal-child-notify'); 17 | 18 | document.addEventListener('click', (e) => { 19 | if (notifyModal.length > 0){ 20 | var clickOnModal = notifyModal[0].contains(e.target); 21 | 22 | if (!clickOnModal) { 23 | this.props.closeModal(); 24 | notifyModal = []; 25 | } 26 | } 27 | }); 28 | 29 | 30 | 31 | } 32 | 33 | notifyBody(notificationList) { 34 | if (this.props.notifications.length === 0){ 35 | return(
    You have no notifications
    ); 36 | }else { 37 | return ( 38 |
      39 | {notificationList} 40 |
    41 | ); 42 | } 43 | } 44 | render() { 45 | const {users, closeModal} = this.props; 46 | let ids = []; 47 | const notificationList = this.props.notifications.map((n) => { 48 | if (!ids.includes(n.initiator_id)){ 49 | ids.push(n.initiator_id); 50 | return ; 51 | } 52 | }); 53 | return ( 54 |
    55 |
    56 | Notifications 57 |
    58 | {this.notifyBody(notificationList)} 59 |
    60 | 61 | ); 62 | } 63 | } 64 | 65 | export default NotifyModal; 66 | -------------------------------------------------------------------------------- /frontend/components/notification/notification_modal_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import {findFollow, notifyArray} from '../../util/selectors'; 4 | import NotifyModal from './notification_modal'; 5 | import { closeModal, openModal } from '../../actions/modal_actions'; 6 | import {deletePicture, updatePicture} from '../../actions/picture_actions'; 7 | import {fetchFollows, createFollow, deleteFollow} from '../../actions/follow_actions'; 8 | import {createNotification} from '../../util/notification_util'; 9 | 10 | 11 | const mapStateToProps = ({follows,notifications, session, entities:{users},ui:{modal}}, ownProps) => { 12 | return { 13 | currentUser: users[session.id], 14 | users: users, 15 | newNotifications: modal.newNotifications, 16 | notifications: notifyArray(notifications) 17 | }; 18 | }; 19 | const mapDispatchToProps = () => dispatch => ({ 20 | closeModal: () => dispatch(closeModal()) 21 | 22 | }); 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(NotifyModal); 25 | -------------------------------------------------------------------------------- /frontend/components/notification/notify_modal_list_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | 4 | class NotifyModalListItem extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | render() { 10 | const {initiator, closeModal} = this.props; 11 | let url = initiator.profile_url || "https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"; 12 | if (url === ""){ url = "https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg";} 13 | return ( 14 |
  • 15 | closeModal()} to={`/profile/${initiator.id}`}> closeModal()} className="notify-link-text" to={`/profile/${initiator.id}`}>{initiator.username} started following you 16 |
  • 17 | ); 18 | } 19 | } 20 | 21 | export default NotifyModalListItem; 22 | -------------------------------------------------------------------------------- /frontend/components/profile/edit_user_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import EditUserForm from './edit_user_form'; 4 | import { closeModal, openModal } from '../../actions/modal_actions'; 5 | import { updateUser } from '../../actions/session_actions'; 6 | import {fetchFollows, createFollow, deleteFollow} from '../../actions/follow_actions'; 7 | 8 | const mapStateToProps = ({follows, session, entities:{users},ui:{modal}}, ownProps) => { 9 | return { 10 | user: modal.user 11 | }; 12 | }; 13 | const mapDispatchToProps = () => dispatch => ({ 14 | closeModal: () => dispatch(closeModal()), 15 | openModal: (object) => dispatch(openModal(object)), 16 | updateUser: (user) => dispatch(updateUser(user)) 17 | }); 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(EditUserForm); 20 | -------------------------------------------------------------------------------- /frontend/components/profile/edit_user_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class EditUserForm extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {description: this.props.user.description}; 7 | this.handleSubmit = this.handleSubmit.bind(this); 8 | this.handleChange = this.handleChange.bind(this); 9 | } 10 | 11 | handleChange() { 12 | return (e) => { 13 | return this.setState({description: e.target.value}); 14 | }; 15 | } 16 | 17 | handleSubmit() { 18 | const {updateUser, user,closeModal} = this.props; 19 | updateUser({id: user.id, username: user.username, description: this.state.description, cover_url: user.cover_url, profile_url: user.profile_url}); 20 | closeModal(); 21 | } 22 | 23 | handleUpload(e, string) { 24 | this.props.closeModal(); 25 | e.preventDefault(); 26 | cloudinary.openUploadWidget(window.cloudinary_options, (error, picture) => { 27 | if (error === null){ 28 | this.props.openModal({string:string, picture: picture[0]}); 29 | 30 | } 31 | }); 32 | } 33 | 34 | 35 | 36 | render() { 37 | return( 38 |
    39 | this.props.closeModal()} className="fa fa-close edit-close"> 40 | 41 | 42 |

    Add/Change Description

    43 |
    this.handleSubmit()}> 44 | 45 | 46 |
    47 |
    48 | ); 49 | } 50 | } 51 | 52 | export default EditUserForm; 53 | -------------------------------------------------------------------------------- /frontend/components/profile/photo_list_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const PhotoListItem = ({picture, openModal, user, parseUrl}) => { 4 | return ( 5 |
  • openModal({string:'show',picture: picture, user: user})}>
  • 6 | ); 7 | }; 8 | 9 | export default PhotoListItem; 10 | -------------------------------------------------------------------------------- /frontend/components/profile/profile_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ProfilePage from './profile_page'; 3 | import {pictureList, findFollow, getFollowers, getFollowings, parseUrl} from '../../util/selectors'; 4 | import {getUserPictures} from '../../actions/picture_actions'; 5 | import {getUser} from '../../actions/session_actions'; 6 | import { openModal} from '../../actions/modal_actions'; 7 | import {fetchFollows, createFollow, deleteFollow} from '../../actions/follow_actions'; 8 | import {createNotification} from '../../util/notification_util'; 9 | 10 | const mapStateToProps = ({entities, session, follows}, ownProps) =>{ 11 | const defaultUser = {id: "",username:""}; 12 | return { 13 | currentUser: entities.users[session.id], 14 | userId: parseInt(ownProps.match.params.id), 15 | user: entities.users[parseInt(ownProps.match.params.id)] || defaultUser, 16 | pictures: pictureList(entities.pictures), 17 | follow: findFollow(follows,session.id, parseInt(ownProps.match.params.id)), 18 | followers: getFollowers(follows,parseInt(ownProps.match.params.id)), 19 | following: getFollowings(follows,parseInt(ownProps.match.params.id)), 20 | parseUrl: parseUrl, 21 | createNotification: createNotification 22 | }; 23 | }; 24 | 25 | const mapDispatchToProps = () => dispatch => ({ 26 | getUserPictures: (userId) => dispatch(getUserPictures(userId)), 27 | getUser: (userId) => dispatch(getUser(userId)), 28 | openModal: (modal) => dispatch(openModal(modal)), 29 | fetchFollows: (follows) => dispatch(fetchFollows(follows)), 30 | createFollow: (follow) => dispatch(createFollow(follow)), 31 | deleteFollow: (id) => dispatch(deleteFollow(id)) 32 | }); 33 | 34 | export default connect(mapStateToProps, mapDispatchToProps)(ProfilePage); 35 | -------------------------------------------------------------------------------- /frontend/components/profile/profile_page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavContainer from '../nav/nav_container'; 3 | import PhotoListItem from './photo_list_item'; 4 | class ProfilePage extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | this.state = {user: this.props.user}; 8 | this.updateFollow = this.updateFollow.bind(this); 9 | } 10 | 11 | componentDidMount () { 12 | this.props.getUserPictures(this.props.userId); 13 | this.props.getUser(this.props.userId); 14 | this.props.fetchFollows(); 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (this.props.userId !== nextProps.userId){ 19 | this.props.getUserPictures(nextProps.userId); 20 | } 21 | } 22 | 23 | holderClass() { 24 | if (this.props.pictures.length < 5 && !this.props.user.cover_url) { 25 | return "profile-holder2"; 26 | } else { 27 | return "profile-holder"; 28 | } 29 | 30 | } 31 | 32 | updateFollow(){ 33 | const {deleteFollow, createFollow, follow, user, currentUser, createNotification} = this.props; 34 | if (follow){ 35 | deleteFollow(follow.id); 36 | } else { 37 | createFollow({leader_id: user.id, follower_id: currentUser.id}); 38 | createNotification({initiator_id: currentUser.id, user_id: user.id}); 39 | } 40 | } 41 | 42 | profileButton() { 43 | const {follow,currentUser,user, openModal} = this.props; 44 | const followButtonClass = this.determineClass(); 45 | const followingButtonClass = this.determineFollowingClass(); 46 | if (follow && currentUser.id !== user.id){ 47 | return ( 48 | 49 | ); 50 | } else if (currentUser.id !== user.id) { 51 | return ( 52 | 53 | ); 54 | } else if (currentUser.id === user.id) { 55 | return ( 56 | 57 | ); 58 | } 59 | } 60 | 61 | determineFollowingClass() { 62 | const {user} = this.props; 63 | if (user.cover_url) { 64 | return "following"; 65 | } else { 66 | return "following2"; 67 | } 68 | } 69 | 70 | determineClass() { 71 | const {user} = this.props; 72 | if (user.cover_url) { 73 | return "follow"; 74 | } else { 75 | return "follow2"; 76 | } 77 | } 78 | 79 | userDescription() { 80 | const {user} = this.props; 81 | if (user.description){ 82 | return ( 83 |

    {user.description}

    84 | ); 85 | } 86 | } 87 | 88 | cover() { 89 | const {user} = this.props; 90 | if (user.cover_url){ 91 | return( 92 |
    93 | 94 |
    95 | ); 96 | } else { 97 | return ( 98 |
    99 | ); 100 | } 101 | } 102 | 103 | render() { 104 | const {openModal, user, followers, following, parseUrl} = this.props; 105 | const profileUrl = user.profile_url || "https://s33.postimg.cc/nk3sgiaa7/default_profile.jpg"; 106 | const pictures = this.props.pictures.map((picture) => { 107 | if (user.id === picture.uploader_id){ 108 | return ; 109 | } 110 | }); 111 | return ( 112 |
    113 | {this.profileButton()} 114 | 115 | {this.cover()} 116 |
    117 | 118 |
    119 |

    {user.username}

    120 |

    {user.description}

    121 |

    {followers} Followers {following} Following

    122 |
      123 |
      124 | {pictures} 125 |
      126 |
    127 |
    128 |
    129 |
    130 | ); 131 | 132 | 133 | } 134 | } 135 | 136 | export default ProfilePage; 137 | -------------------------------------------------------------------------------- /frontend/components/profile/user_photo_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import UserPhotoForm from './user_photo_form'; 4 | import { closeModal, openModal } from '../../actions/modal_actions'; 5 | import { updateUser } from '../../actions/session_actions'; 6 | import {fetchFollows, createFollow, deleteFollow} from '../../actions/follow_actions'; 7 | 8 | const mapStateToProps = ({follows, session, entities:{users},ui:{modal}}, ownProps) => { 9 | return { 10 | string: modal.string, 11 | url: modal.picture.secure_url, 12 | currentUser: users[session.id] 13 | }; 14 | }; 15 | const mapDispatchToProps = () => dispatch => ({ 16 | closeModal: () => dispatch(closeModal()), 17 | openModal: (object) => dispatch(openModal(object)), 18 | updateUser: (user) => dispatch(updateUser(user)) 19 | }); 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(UserPhotoForm); 22 | -------------------------------------------------------------------------------- /frontend/components/profile/user_photo_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class UserPhotoForm extends React.Component { 4 | 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | title() { 10 | const {string} = this.props; 11 | if (string === 'uploadProfile'){ 12 | return "Profile Picture"; 13 | } else if (string === 'uploadCover'){ 14 | return "Cover Photo"; 15 | } 16 | } 17 | 18 | updateUser() { 19 | const {currentUser, string,url, updateUser, closeModal} = this.props; 20 | if (string === 'uploadCover') { 21 | updateUser({id: currentUser.id, username: currentUser.username, cover_url: url, profile_url: currentUser.profile_url}); 22 | } else if (string === 'uploadProfile') { 23 | updateUser({id: currentUser.id, username: currentUser.username, cover_url: currentUser.cover_url, profile_url: url}); 24 | } 25 | closeModal(); 26 | } 27 | 28 | render() { 29 | const {url} = this.props; 30 | return ( 31 |
    32 | this.props.closeModal()} className="fa fa-close upload-close"> 33 |

    {this.title()}

    34 | 35 |
    36 | 37 |
    38 | ); 39 | } 40 | 41 | } 42 | 43 | export default UserPhotoForm; 44 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | const Root = ({ store }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default Root; 15 | -------------------------------------------------------------------------------- /frontend/components/session/login_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { login, clearErrors } from '../../actions/session_actions'; 3 | import SessionForm from './session_form'; 4 | import { Link } from 'react-router-dom'; 5 | import React from 'react'; 6 | 7 | 8 | const mapStateToProps = (state,ownProps) => ({ 9 | errors: state.errors.session, 10 | formType: 'Log In', 11 | link: Sign up , 12 | user: state.entities.users[state.session.id] 13 | }); 14 | const mapDispatchToProps = () => dispatch => ({ 15 | processForm: (user) => dispatch(login(user)), 16 | clear: () => dispatch(clearErrors()) 17 | }); 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(SessionForm); 20 | -------------------------------------------------------------------------------- /frontend/components/session/session_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withRouter, Link } from 'react-router-dom'; 3 | import NavContainer from '../nav/nav_container'; 4 | import {Redirect} from 'react-router'; 5 | import {Animated} from "react-animated-css"; 6 | 7 | class SessionForm extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | username: "", 12 | password: "" 13 | }; 14 | this.handleSubmit = this.handleSubmit.bind(this); 15 | this.handleChange = this.handleChange.bind(this); 16 | } 17 | 18 | componentWillMount () { 19 | this.props.clear(); 20 | } 21 | 22 | 23 | 24 | handleSubmit(e) { 25 | e.preventDefault(); 26 | const username = this.state.username; 27 | const password = this.state.password; 28 | this.props.processForm(this.state); 29 | return ( 30 | 31 | ); 32 | } 33 | 34 | handleChange(field) { 35 | return (e) => { 36 | this.setState({[field]: e.target.value}); 37 | }; 38 | } 39 | 40 | listErrors() { 41 | if (this.props.errors.length > 0){ 42 | return( 43 | 44 |
      45 | {this.props.errors.map((error,i) => { 46 | return(
    • {error}
    • ); 47 | } 48 | )} 49 |
    50 |
    51 | ); 52 | } 53 | } 54 | 55 | demo(){ 56 | const {formType} = this.props; 57 | if (formType === 'Log In'){ 58 | return ( 59 | 60 | ); 61 | } else { 62 | return ( 63 | 64 | ) 65 | } 66 | } 67 | 68 | bottomLink() { 69 | const {formType} = this.props; 70 | if (formType === 'Log In'){ 71 | return ( 72 |

    Don't have an account? {this.props.link}

    73 | ); 74 | } else { 75 | return ( 76 |

    Already have an account? {this.props.link}

    77 | ); 78 | } 79 | } 80 | 81 | render(){ 82 | 83 | const {formType} = this.props; 84 | return ( 85 |
    86 | 87 |
    88 | {this.listErrors()} 89 |
    90 |
    91 |

    {formType}

    92 |
    93 |
    this.handleSubmit(e)}> 94 | 95 |
    96 | 97 |
    98 | 99 | 100 |
    101 | 102 |
    103 |
    104 | {this.demo()} 105 | 106 | {this.bottomLink()} 107 |
    108 |
    109 | ); 110 | } 111 | } 112 | 113 | export default withRouter(SessionForm); 114 | -------------------------------------------------------------------------------- /frontend/components/session/signup_form_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { signup, clearErrors, login } from '../../actions/session_actions'; 3 | import SessionForm from './session_form'; 4 | import { Link } from 'react-router-dom'; 5 | import React from 'react'; 6 | 7 | const mapStateToProps = (state,ownProps) => ( 8 | { 9 | errors: state.errors.session, 10 | formType: 'Sign Up', 11 | link: Log In , 12 | user: state.entities.users[state.session.id] 13 | } 14 | ); 15 | const mapDispatchToProps = () => dispatch => ({ 16 | processForm: (user) => dispatch(signup(user)), 17 | clear: () => dispatch(clearErrors()), 18 | login: (user) => dispatch(login(user)) 19 | }); 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(SessionForm); 22 | -------------------------------------------------------------------------------- /frontend/components/show/edit_picture_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import PictureForm from '../upload/picture_form'; 4 | import { closeModal } from '../../actions/modal_actions'; 5 | import {updatePicture} from '../../actions/picture_actions'; 6 | 7 | const mapStateToProps = ({session, entities:{users},ui:{modal}}) => ( 8 | { 9 | currentUser: users[session.id], 10 | picture: modal.picture 11 | } 12 | ); 13 | const mapDispatchToProps = () => dispatch => ({ 14 | action: (picture) => dispatch(updatePicture(picture)), 15 | closeModal: () => dispatch(closeModal()) 16 | }); 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(PictureForm); 19 | -------------------------------------------------------------------------------- /frontend/components/show/show_picture.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | class ShowPictureComponent extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.handleDelete.bind(this); 7 | } 8 | 9 | componentDidMount(){ 10 | document.addEventListener('keydown', (e) => { 11 | if (e.keyCode === 27) this.props.closeModal(); 12 | }); 13 | } 14 | 15 | nameLink() { 16 | if (this.props.picture.uploader_id === this.props.currentUser.id) { 17 | return "default-cursor"; 18 | } 19 | } 20 | 21 | buttonDisplay() { 22 | if (this.props.currentUser.id === this.props.picture.uploader_id){ 23 | return ( 24 |
    25 | 26 | 27 |
    28 | ); 29 | } 30 | } 31 | 32 | handleDelete() { 33 | this.props.deletePicture(this.props.picture.id); 34 | this.props.closeModal(); 35 | } 36 | 37 | handleEdit() { 38 | this.props.closeModal(); 39 | this.props.openModal({string:'edit', picture:this.props.picture}); 40 | } 41 | 42 | updateFollow(){ 43 | const {deleteFollow, createFollow, follow, user, currentUser, createNotification} = this.props; 44 | if (follow){ 45 | deleteFollow(follow.id); 46 | } else { 47 | createFollow({leader_id: user.id, follower_id: currentUser.id}); 48 | createNotification({initiator_id: currentUser.id, user_id: user.id}); 49 | } 50 | } 51 | 52 | followButton() { 53 | const {follow,currentUser,user} = this.props; 54 | if (follow && currentUser.id !== user.id){ 55 | return ( 56 | 57 | ); 58 | } else if (currentUser.id !== user.id) { 59 | return ( 60 | 61 | ); 62 | } 63 | } 64 | 65 | handleModalClose () { 66 | 67 | } 68 | 69 | render() { 70 | const {closeModal, picture, user} = this.props; 71 | const defaultProfile = this.props.user.profile_url || "https://res.cloudinary.com/dbm56y2y/image/upload/v1529040240/default_profile.jpg"; 72 | return ( 73 |
    74 | 75 |
    76 |
    77 |

    closeModal()} className={`show-link ${this.nameLink()}`} to={`/profile/${user.id}`} > 78 | closeModal()} className={`show-link ${this.nameLink()}`} to={`/profile/${user.id}`} >{user.username}{this.followButton()}

    79 | 80 |

    {picture.title}

    81 |
    82 | {this.buttonDisplay()} 83 |
    84 | 85 |
    86 |
    87 | ); 88 | } 89 | } 90 | 91 | export default ShowPictureComponent; 92 | -------------------------------------------------------------------------------- /frontend/components/show/show_picture_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import {findFollow} from '../../util/selectors'; 4 | import ShowPictureComponent from './show_picture'; 5 | import { closeModal, openModal } from '../../actions/modal_actions'; 6 | import {deletePicture, updatePicture} from '../../actions/picture_actions'; 7 | import {fetchFollows, createFollow, deleteFollow} from '../../actions/follow_actions'; 8 | import {createNotification} from '../../util/notification_util'; 9 | 10 | const mapStateToProps = ({follows, session, entities:{users},ui:{modal}}, ownProps) => { 11 | return { 12 | currentUser: users[session.id], 13 | picture: modal.picture, 14 | user: modal.user, 15 | follow: findFollow(follows,session.id, parseInt(modal.picture.uploader_id)), 16 | createNotification: createNotification 17 | }; 18 | }; 19 | const mapDispatchToProps = () => dispatch => ({ 20 | closeModal: () => dispatch(closeModal()), 21 | deletePicture: (photoId) => dispatch(deletePicture(photoId)), 22 | updatePicture: (photo) => dispatch(updatePicture(photo)), 23 | openModal: (object) => dispatch(openModal(object)), 24 | fetchFollows: (follows) => dispatch(fetchFollows(follows)), 25 | createFollow: (follow) => dispatch(createFollow(follow)), 26 | deleteFollow: (id) => dispatch(deleteFollow(id)) 27 | 28 | }); 29 | 30 | export default connect(mapStateToProps, mapDispatchToProps)(ShowPictureComponent); 31 | -------------------------------------------------------------------------------- /frontend/components/upload/picture_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class PictureForm extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | const title = this.props.picture.title || ""; 7 | this.state = {title:title}; 8 | this.handleChange = this.handleChange.bind(this); 9 | this.handleSubmit = this.handleSubmit.bind(this); 10 | } 11 | 12 | handleChange() { 13 | return (e) => { 14 | this.setState({title:e.target.value}); 15 | }; 16 | } 17 | 18 | handleSubmit() { 19 | 20 | const {picture, currentUser, closeModal, action} = this.props; 21 | if (picture.url){ 22 | action({title: this.state.title, image_url:picture.url}, currentUser.id); 23 | } else if (picture.image_url) { 24 | action({id: picture.id, title: this.state.title, image_url:picture.secure_url, uploader_id: picture.uploader_id}); 25 | } 26 | closeModal(); 27 | } 28 | 29 | render() { 30 | const url = this.props.picture.image_url || this.props.picture.url; 31 | return ( 32 |
    33 | this.props.closeModal()} className="fa fa-close upload-close"> 34 | 35 |
    this.handleSubmit(e)}> 36 | 37 |
    38 | 39 |
    40 | 41 |
    42 |
    43 | ); 44 | } 45 | } 46 | export default PictureForm; 47 | -------------------------------------------------------------------------------- /frontend/components/upload/upload_picture_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import PictureForm from './picture_form'; 4 | import { closeModal } from '../../actions/modal_actions'; 5 | import {uploadPicture} from '../../actions/picture_actions'; 6 | 7 | const mapStateToProps = ({session, entities:{users},ui:{modal}}) => ( 8 | { 9 | currentUser: users[session.id], 10 | picture: modal.picture 11 | } 12 | ); 13 | const mapDispatchToProps = () => dispatch => ({ 14 | action: (picture, userId) => dispatch(uploadPicture(picture, userId)), 15 | closeModal: () => dispatch(closeModal()) 16 | }); 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(PictureForm); 19 | -------------------------------------------------------------------------------- /frontend/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import configureStore from './store/store'; 4 | import Root from './components/root'; 5 | import * as APIUtil from './util/session_api_util'; 6 | import * as thunkAction from './actions/session_actions'; 7 | import * as APIPicture from './util/picture_api_util'; 8 | import * as pictureAction from './actions/picture_actions'; 9 | import * as followAction from './actions/follow_actions'; 10 | import * as followUtil from './util/follow_util'; 11 | import * as feedUtil from './util/feed_util'; 12 | import * as NotificationUtil from './util/notification_util'; 13 | import * as NotificationAction from './actions/notification_actions'; 14 | document.addEventListener('DOMContentLoaded', () => { 15 | window.deleteFollow = followUtil.deleteFollow; 16 | window.makeFollow = followAction.createFollow; 17 | window.getFollows = followAction.fetchFollows; 18 | window.removeFollow = followAction.deleteFollow; 19 | window.homeFeed = feedUtil.homeFeed; 20 | window.home = pictureAction.homeFeed; 21 | window.fetchUsers = APIUtil.fetchUsers; 22 | window.receiveUsers = thunkAction.fetchUsers; 23 | window.updateUser = APIUtil.updateUser; 24 | window.updateUser = APIUtil.updateUser; 25 | window.updateCurrentUser = thunkAction.updateUser; 26 | window.fetchNotifications = NotificationUtil.fetchNotifications; 27 | window.createNotification = NotificationUtil.createNotification; 28 | window.deleteNotification = NotificationUtil.deleteNotification; 29 | window.getNotifications = NotificationAction.fetchNotifications; 30 | window.makeNotification = NotificationAction.createNotification; 31 | window.updateNotification = NotificationAction.updateNotification; 32 | let store; 33 | if (window.currentUser) { 34 | const preloadedState = { 35 | entities: { 36 | users: { [window.currentUser.id]: window.currentUser } 37 | }, 38 | session: { id: window.currentUser.id } 39 | }; 40 | store = configureStore(preloadedState); 41 | delete window.currentUser; 42 | } else { 43 | store = configureStore(); 44 | } 45 | window.getState = store.getState; 46 | window.dispatch = store.dispatch; 47 | 48 | const root = document.getElementById('root'); 49 | ReactDOM.render(, root); 50 | }); 51 | -------------------------------------------------------------------------------- /frontend/reducers/entities_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import usersReducer from './users_reducer'; 3 | import picturesReducer from './pictures_reducer'; 4 | 5 | const entitiesReducer = combineReducers({ 6 | users: usersReducer, 7 | pictures: picturesReducer, 8 | 9 | }); 10 | 11 | export default entitiesReducer; 12 | -------------------------------------------------------------------------------- /frontend/reducers/errors_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import sessionErrorsReducer from './session_errors_reducer'; 4 | 5 | const errorsReducer = combineReducers({ 6 | session: sessionErrorsReducer 7 | }); 8 | 9 | export default errorsReducer; 10 | -------------------------------------------------------------------------------- /frontend/reducers/follows_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_FOLLOW, RECEIVE_FOLLOWS, REMOVE_FOLLOW} from '../actions/follow_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const followsReducer = (initialState = {}, action) => { 5 | Object.freeze(initialState); 6 | switch (action.type){ 7 | case RECEIVE_FOLLOW: 8 | return merge({}, initialState, {[action.follow.id]:action.follow}); 9 | case RECEIVE_FOLLOWS: 10 | return merge({}, action.follows); 11 | case REMOVE_FOLLOW: 12 | const newState = merge({}, initialState); 13 | delete newState[action.follow.id]; 14 | return newState; 15 | default: 16 | return initialState; 17 | } 18 | }; 19 | 20 | export default followsReducer; 21 | -------------------------------------------------------------------------------- /frontend/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | import { OPEN_MODAL, CLOSE_MODAL } from '../actions/modal_actions'; 2 | 3 | export default function modalReducer(initialState = null, action) { 4 | Object.freeze(initialState); 5 | switch (action.type) { 6 | case OPEN_MODAL: 7 | return action.modal; 8 | case CLOSE_MODAL: 9 | return null; 10 | default: 11 | return initialState; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/reducers/notifications_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_NOTIFICATION, RECEIVE_NOTIFICATIONS} from '../actions/notification_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const notificationsReducer = (initialState = {}, action) => { 5 | Object.freeze(initialState); 6 | switch (action.type){ 7 | case RECEIVE_NOTIFICATION: 8 | return merge({}, initialState, {[action.notification.id]:action.notification}); 9 | case RECEIVE_NOTIFICATIONS: 10 | return merge({}, action.notifications); 11 | default: 12 | return initialState; 13 | } 14 | }; 15 | 16 | export default notificationsReducer; 17 | -------------------------------------------------------------------------------- /frontend/reducers/pictures_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_PICTURE, RECEIVE_PICTURES, CLEAR_PICTURES, REMOVE_PICTURE} from '../actions/picture_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const picturesReducer = (initialState = {}, action) => { 5 | Object.freeze(initialState); 6 | switch (action.type){ 7 | case RECEIVE_PICTURE: 8 | return merge({}, initialState, {[action.picture.id]:action.picture}); 9 | case RECEIVE_PICTURES: 10 | return merge({}, action.pictures); 11 | case CLEAR_PICTURES: 12 | return {}; 13 | case REMOVE_PICTURE: 14 | const newState = merge({}, initialState); 15 | delete newState[action.picture.id]; 16 | return newState; 17 | default: 18 | return initialState; 19 | } 20 | }; 21 | 22 | export default picturesReducer; 23 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import errorsReducer from './errors_reducer'; 3 | import sessionReducer from './session_reducer'; 4 | import entitiesReducer from './entities_reducer'; 5 | import followsReducer from './follows_reducer'; 6 | import ui from './ui_reducer'; 7 | import notificationsReducer from './notifications_reducer'; 8 | 9 | const rootReducer = combineReducers({ 10 | entities: entitiesReducer, 11 | follows: followsReducer, 12 | session: sessionReducer, 13 | ui: ui, 14 | errors: errorsReducer, 15 | notifications: notificationsReducer 16 | }); 17 | 18 | export default rootReducer; 19 | -------------------------------------------------------------------------------- /frontend/reducers/session_errors_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_CURRENT_USER, 2 | RECEIVE_ERRORS, CLEAR_ERRORS} from '../actions/session_actions.js'; 3 | 4 | const sessionErrorsReducer = (initialState = [], action) => { 5 | Object.freeze(initialState); 6 | switch(action.type){ 7 | case RECEIVE_CURRENT_USER: 8 | return []; 9 | case CLEAR_ERRORS: 10 | return []; 11 | case RECEIVE_ERRORS: 12 | return action.errors; 13 | default: 14 | return initialState; 15 | } 16 | }; 17 | 18 | export default sessionErrorsReducer; 19 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_CURRENT_USER, 2 | LOGOUT_CURRENT_USER, REMOVE_USER} from '../actions/session_actions.js'; 3 | import merge from 'lodash/merge'; 4 | 5 | const defaultUser = Object.freeze({ 6 | id: null 7 | }); 8 | 9 | const sessionReducer = (state = defaultUser, action) => { 10 | Object.freeze(state); 11 | switch(action.type) { 12 | case RECEIVE_CURRENT_USER: 13 | return { id: action.currentUser.id }; 14 | case LOGOUT_CURRENT_USER: 15 | return defaultUser; 16 | case REMOVE_USER: 17 | const newState = merge({}, state); 18 | delete newState[action.user.id]; 19 | return newState; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | export default sessionReducer; 26 | -------------------------------------------------------------------------------- /frontend/reducers/ui_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import modal from './modal_reducer'; 3 | 4 | export default combineReducers({ 5 | modal 6 | }); 7 | -------------------------------------------------------------------------------- /frontend/reducers/users_reducer.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_CURRENT_USER, RECEIVE_USER, RECEIVE_USERS} from '../actions/session_actions.js'; 2 | import merge from 'lodash/merge'; 3 | 4 | const usersReducer = (initialState = {}, action) => { 5 | Object.freeze(initialState); 6 | switch(action.type){ 7 | case RECEIVE_CURRENT_USER: 8 | return merge({}, initialState, 9 | {[action.currentUser.id]: action.currentUser}); 10 | case RECEIVE_USER: 11 | return merge({}, initialState, {[action.user.id]:action.user}); 12 | case RECEIVE_USERS: 13 | return merge({}, action.users); 14 | default: 15 | return initialState; 16 | } 17 | }; 18 | 19 | export default usersReducer; 20 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers/root_reducer'; 4 | // import logger from 'redux-logger' 5 | 6 | const configureStore = (preloadedState = {}) => ( 7 | createStore( 8 | rootReducer, 9 | preloadedState, 10 | applyMiddleware(thunk) 11 | ) 12 | ); 13 | 14 | export default configureStore; 15 | -------------------------------------------------------------------------------- /frontend/util/feed_util.js: -------------------------------------------------------------------------------- 1 | export const homeFeed = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/feed' 5 | }); 6 | }; 7 | 8 | export const discoverFeed = () => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: '/api/fresh' 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/util/follow_util.js: -------------------------------------------------------------------------------- 1 | export const fetchFollows = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/follows' 5 | }); 6 | }; 7 | 8 | export const createFollow = (follow) => { 9 | return $.ajax({ 10 | method: 'POST', 11 | url: '/api/follows', 12 | data: {follow} 13 | }); 14 | }; 15 | 16 | export const deleteFollow = (id) => { 17 | return $.ajax({ 18 | method: 'DELETE', 19 | url: `/api/follows/${id}` 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/util/notification_util.js: -------------------------------------------------------------------------------- 1 | export const fetchNotifications = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/notifications' 5 | }); 6 | }; 7 | 8 | export const createNotification = (notification) => { 9 | return $.ajax({ 10 | method: 'POST', 11 | url: '/api/notifications', 12 | data: {notification} 13 | }); 14 | }; 15 | 16 | export const deleteNotification = (id) => { 17 | return $.ajax({ 18 | method: 'DELETE', 19 | url: `/api/notifications/${id}` 20 | }); 21 | }; 22 | 23 | export const updateNotification = (notification) => { 24 | return $.ajax({ 25 | method: 'PATCH', 26 | url: `/api/notifications/${notification.id}`, 27 | data: {notification} 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/util/picture_api_util.js: -------------------------------------------------------------------------------- 1 | export const uploadPicture = (picture, userId) => { 2 | return $.ajax({ 3 | method: 'POST', 4 | url: `/api/users/${userId}/pictures`, 5 | data: {picture} 6 | }); 7 | }; 8 | 9 | export const fetchUserPictures = (userId) => { 10 | return $.ajax({ 11 | method: 'GET', 12 | url: `/api/users/${userId}/pictures` 13 | }); 14 | }; 15 | 16 | export const fetchPicture = (photoId) => { 17 | return $.ajax({ 18 | method: 'GET', 19 | url: `/api/pictures/${photoId}` 20 | }); 21 | }; 22 | 23 | export const updatePicture = (picture) => { 24 | return $.ajax({ 25 | method: 'PATCH', 26 | url: `/api/pictures/${picture.id}`, 27 | data: {picture} 28 | }); 29 | }; 30 | 31 | export const deletePicture = (photoId) => { 32 | return $.ajax({ 33 | method: 'DELETE', 34 | url: `/api/pictures/${photoId}` 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter, Route, Redirect } from 'react-router-dom'; 3 | import React from 'react'; 4 | 5 | const Auth = ({component: Component, path, loggedIn, exact}) => ( 6 | ( 7 | !loggedIn ? ( 8 | 9 | ) : ( 10 | 11 | ) 12 | )}/> 13 | ); 14 | 15 | const mapStateToProps = state => { 16 | return {loggedIn: Boolean(state.session.id)}; 17 | }; 18 | 19 | const LogAuth = ({component: Component, path, loggedIn, exact}) => ( 20 | ( 21 | loggedIn ? ( 22 | 23 | ) : ( 24 | 25 | ) 26 | )}/> 27 | ); 28 | 29 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth)); 30 | export const LogRoute = withRouter(connect(mapStateToProps, null)(LogAuth)); 31 | -------------------------------------------------------------------------------- /frontend/util/selectors.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const pictureList = (pictures) => { 4 | return Object.values(pictures).reverse(); 5 | }; 6 | 7 | export const pictureListFeed = (pictures) => { 8 | return Object.values(pictures).reverse(); 9 | }; 10 | 11 | export const findFollow = (follows, currentUserId, profileUserId) => { 12 | const followArr = Object.values(follows); 13 | for (var i = 0; i < followArr.length; i++) { 14 | const follow = followArr[i]; 15 | if (follow.follower_id === currentUserId && follow.leader_id === profileUserId) { 16 | return follow; 17 | } 18 | } 19 | return null; 20 | }; 21 | 22 | export const getFollowers = (follows, id) => { 23 | const followArr = Object.values(follows); 24 | let followers = 0; 25 | for (var i = 0; i < followArr.length; i++) { 26 | if(followArr[i].leader_id === id){ 27 | followers += 1; 28 | } 29 | } 30 | return followers; 31 | }; 32 | 33 | export const getFollowings = (follows, id) => { 34 | const followArr = Object.values(follows); 35 | let following = 0; 36 | for (var i = 0; i < followArr.length; i++) { 37 | if(followArr[i].follower_id === id){ 38 | following += 1; 39 | } 40 | } 41 | return following; 42 | }; 43 | 44 | export const parseUrl = (url) => { 45 | const newUrl = 'https://res.cloudinary.com/dbm56y2y/image/upload/c_scale,h_500/'; 46 | return newUrl.concat(url.slice(48)); 47 | }; 48 | 49 | export const parseUrlBig = (url) => { 50 | const newUrl = 'https://res.cloudinary.com/dbm56y2y/image/upload/c_scale,w_1000/'; 51 | return newUrl.concat(url.slice(48)); 52 | }; 53 | 54 | export const newNotifications = (notifications) => { 55 | const array = Object.values(notifications); 56 | if (array.length === 0) return []; 57 | let ids = []; 58 | let newArray = []; 59 | array.forEach((n) => { 60 | if (n.viewed === false && !ids.includes(n.initiator_id)){ 61 | ids.push(n.initiator_id); 62 | newArray.push(n); 63 | } 64 | }); 65 | return newArray.reverse(); 66 | }; 67 | 68 | export const notifyArray = (notifications) => { 69 | return Object.values(notifications).reverse(); 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signup = (user) => { 2 | return $.ajax({ 3 | method: 'POST', 4 | url: '/api/users', 5 | data: {user} 6 | }); 7 | }; 8 | 9 | export const login = (user) => { 10 | return $.ajax({ 11 | method: 'POST', 12 | url: '/api/session', 13 | data: {user} 14 | }); 15 | }; 16 | 17 | export const logout = () => { 18 | return $.ajax({ 19 | method: 'DELETE', 20 | url: '/api/session' 21 | }); 22 | }; 23 | 24 | export const fetchUser = (id) => { 25 | return $.ajax({ 26 | method: 'GET', 27 | url: `api/users/${id}` 28 | }); 29 | }; 30 | 31 | export const fetchUsers = () => { 32 | return $.ajax({ 33 | method: 'GET', 34 | url: 'api/users' 35 | }); 36 | }; 37 | 38 | export const updateUser = (user) => { 39 | return $.ajax({ 40 | method: 'PATCH', 41 | url: `/api/users/${user.id}`, 42 | data: {user} 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/lib/tasks/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": "8.9.4", 4 | "npm": "6.0.1" 5 | }, 6 | "scripts": { 7 | "webpack": "webpack --watch -d", 8 | "postinstall": "webpack --optimize-minimize" 9 | }, 10 | "name": "ThreeSixtyPx", 11 | "private": true, 12 | "dependencies": { 13 | "babel-core": "^6.26.3", 14 | "babel-loader": "^7.1.4", 15 | "babel-preset-env": "^1.7.0", 16 | "babel-preset-react": "^6.24.1", 17 | "link-react": "^3.0.0", 18 | "lodash": "^4.17.10", 19 | "react": "^16.4.0", 20 | "react-addons-css-transition-group": "^15.6.2", 21 | "react-animated-css": "^1.0.4", 22 | "react-dom": "^16.4.0", 23 | "react-redux": "^5.0.7", 24 | "react-router-dom": "^4.2.2", 25 | "react-slick": "^0.23.1", 26 | "redux": "^4.0.0", 27 | "redux-logger": "^3.0.6", 28 | "redux-thunk": "^2.3.0", 29 | "slick-carousel": "^1.8.1", 30 | "webpack": "^4.11.0", 31 | "webpack-cli": "^3.0.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/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 | -------------------------------------------------------------------------------- /test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 5 | end 6 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/fixtures/follows.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :bigint(8) not null, primary key 6 | # leader_id :integer not null 7 | # follower_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 13 | 14 | one: 15 | leader_id: 1 16 | follower_id: 1 17 | 18 | two: 19 | leader_id: 1 20 | follower_id: 1 21 | -------------------------------------------------------------------------------- /test/fixtures/notifications.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: notifications 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # initiator_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # viewed :boolean 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | one: 16 | user_id: 1 17 | initiator_id: 1 18 | 19 | two: 20 | user_id: 1 21 | initiator_id: 1 22 | -------------------------------------------------------------------------------- /test/fixtures/pictures.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: pictures 4 | # 5 | # id :bigint(8) not null, primary key 6 | # title :string 7 | # image_url :string not null 8 | # uploader_id :integer not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | one: 16 | id: 1 17 | title: MyString 18 | image_url: MyString 19 | uploader_id: 1 20 | 21 | two: 22 | id: 1 23 | title: MyString 24 | image_url: MyString 25 | uploader_id: 1 26 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint(8) not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # description :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # profile_url :string 13 | # cover_url :string 14 | # 15 | 16 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 17 | 18 | one: 19 | username: MyString 20 | password_digest: MyString 21 | session_token: MyString 22 | profile_url: MyString 23 | cover_url: MyString 24 | description: MyString 25 | 26 | two: 27 | username: MyString 28 | password_digest: MyString 29 | session_token: MyString 30 | profile_url: MyString 31 | cover_url: MyString 32 | description: MyString 33 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/models/.keep -------------------------------------------------------------------------------- /test/models/follow_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :bigint(8) not null, primary key 6 | # leader_id :integer not null 7 | # follower_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | require 'test_helper' 13 | 14 | class FollowTest < ActiveSupport::TestCase 15 | # test "the truth" do 16 | # assert true 17 | # end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/notification_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: notifications 4 | # 5 | # id :bigint(8) not null, primary key 6 | # user_id :integer not null 7 | # initiator_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # viewed :boolean 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class NotificationTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /test/models/picture_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: pictures 4 | # 5 | # id :bigint(8) not null, primary key 6 | # title :string 7 | # image_url :string not null 8 | # uploader_id :integer not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class PictureTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint(8) not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # description :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # profile_url :string 13 | # cover_url :string 14 | # 15 | 16 | require 'test_helper' 17 | 18 | class UserTest < ActiveSupport::TestCase 19 | # test "the truth" do 20 | # assert true 21 | # end 22 | end 23 | -------------------------------------------------------------------------------- /test/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/test/system/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FelixAlvarado/360px/5d6879e71e00c2a32ef7e151f0c72809f3e192bb/vendor/.keep -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: "./frontend/index.jsx", 6 | output: { 7 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 8 | filename: "bundle.js" 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: [/\.jsx?$/, /\.js?$/], 14 | exclude: /node_modules/, 15 | loader: 'babel-loader', 16 | query: { 17 | presets: ['env', 'react'] 18 | } 19 | } 20 | ] 21 | }, 22 | devtool: 'source-map', 23 | resolve: { 24 | extensions: [".js", ".jsx", "*"] 25 | } 26 | }; 27 | --------------------------------------------------------------------------------