├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── logo.png │ │ └── logo2.png │ ├── javascripts │ │ ├── api │ │ │ └── stories.coffee │ │ ├── application.js │ │ ├── comments.coffee │ │ └── likes.coffee │ └── stylesheets │ │ ├── api │ │ └── stories.scss │ │ ├── application.css │ │ ├── auth │ │ └── auth.scss │ │ ├── comment │ │ ├── comment.scss │ │ ├── comment_show.scss │ │ └── new_comment.scss │ │ ├── footer.scss │ │ ├── header │ │ ├── _header.scss │ │ ├── edit_header.scss │ │ └── new_story_header.scss │ │ ├── likes.scss │ │ ├── static_pages.scss │ │ ├── stories │ │ ├── author_box.scss │ │ ├── blurb.scss │ │ ├── editor.scss │ │ ├── new_story.scss │ │ ├── share_bar.scss │ │ ├── stories.scss │ │ ├── story_author_box.scss │ │ └── story_show.scss │ │ └── user │ │ └── user_show.scss ├── controllers │ ├── api │ │ ├── comments_controller.rb │ │ ├── sessions_controller.rb │ │ ├── stories_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ └── static_pages_controller.rb ├── helpers │ ├── api │ │ └── stories_helper.rb │ ├── application_helper.rb │ ├── comments_helper.rb │ └── likes_helper.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── bookmark.rb │ ├── comment.rb │ ├── concerns │ │ └── .keep │ ├── follow.rb │ ├── like.rb │ ├── story.rb │ └── user.rb └── views │ ├── api │ ├── comments │ │ ├── _comment.json.jbuilder │ │ └── show.json.jbuilder │ ├── stories │ │ ├── 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 ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── session_store.rb │ ├── to_time_preserves_timezone.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── routes.rb └── secrets.yml ├── db ├── migrate │ ├── 20170620130854_create_users.rb │ ├── 20170620133029_users.rb │ ├── 20170620133334_populate_users.rb │ ├── 20170621073510_add_attachment_image_to_users.rb │ ├── 20170621153830_create_stories.rb │ ├── 20170621155254_add_attachment_image_to_stories.rb │ ├── 20170622202803_add_description_to_story.rb │ ├── 20170623181315_add_bio_to_users.rb │ ├── 20170627135328_create_comments.rb │ ├── 20170628183541_create_likes.rb │ ├── 20170628184610_adduseridtolikes.rb │ ├── 20170629150840_create_follows.rb │ └── 20170711033755_create_bookmarks.rb ├── schema.rb └── seeds.rb ├── docs ├── api-endpoints.md ├── component_hierarchy.md ├── images │ ├── edit.png │ └── show.png ├── production_timeline.md ├── readme.md ├── sample-state.md ├── schema.md └── wireframes │ ├── Auth.JPG │ ├── AuthorBox.JPG │ ├── AuthorShow.JPG │ ├── CommentBox.JPG │ ├── EditStory_NewStory.JPG │ ├── Header.JPG │ ├── Home.JPG │ ├── SocialMedia.JPG │ ├── StoryBlurb.JPG │ ├── StoryBlurbs.JPG │ └── StoryShow.JPG ├── frontend ├── actions │ ├── comment_actions.js │ ├── presentational_actions.js │ ├── session_actions.js │ ├── story_actions.js │ └── user_actions.js ├── components │ ├── App.jsx │ ├── comment │ │ ├── comment.jsx │ │ ├── comment_author_box.jsx │ │ ├── comment_list.jsx │ │ ├── comment_list_container.js │ │ ├── comment_show.jsx │ │ ├── comment_show_author_box.jsx │ │ ├── comment_show_container.jsx │ │ └── new_comment.jsx │ ├── errors_list.jsx │ ├── footer.jsx │ ├── greeting │ │ ├── greeting.jsx │ │ └── greeting_container.js │ ├── header.jsx │ ├── new_story_header.jsx │ ├── root.jsx │ ├── save_header.jsx │ ├── session │ │ ├── session_form.jsx │ │ └── session_form_container.jsx │ ├── story │ │ ├── author_box.jsx │ │ ├── blurb.jsx │ │ ├── edit_story_container.js │ │ ├── feed_index.jsx │ │ ├── feed_index_container.jsx │ │ ├── new_author_box.jsx │ │ ├── new_story_container.jsx │ │ ├── share_bar.jsx │ │ ├── stories_index.jsx │ │ ├── stories_index_container.js │ │ ├── stories_show.jsx │ │ ├── stories_show_container.jsx │ │ ├── story_author_box.jsx │ │ └── story_form.jsx │ └── users │ │ ├── user_show.jsx │ │ ├── user_show_container.js │ │ ├── users.jsx │ │ └── users_container.jsx ├── large.jsx ├── reducers │ ├── comments_reducer.js │ ├── presentational_reducer.js │ ├── root_reducer.js │ ├── selectors.js │ ├── session_reducer.js │ ├── stories_reducer.js │ └── users_reducer.js ├── store │ └── store.js └── util │ ├── comment_util.jsx │ ├── route_util.jsx │ ├── session_api_util.js │ └── story_util.js ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log ├── .keep ├── development.log └── test.log ├── package-lock.json ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── images │ ├── bookmark_clicked.png │ ├── bookmark_unclicked.png │ ├── default_user.png │ ├── large.png │ ├── logo.png │ └── pencil.png ├── robots.txt └── system │ └── users │ └── images │ └── 000 │ └── 000 │ ├── 003 │ ├── original │ │ └── cat.jpg │ └── thumb │ │ └── cat.png │ ├── 006 │ ├── original │ │ └── cat.jpg │ └── thumb │ │ └── cat.png │ ├── 007 │ ├── original │ │ └── cat.jpg │ └── thumb │ │ └── cat.png │ ├── 009 │ ├── original │ │ └── cat.jpg │ └── thumb │ │ └── cat.png │ ├── 010 │ ├── original │ │ └── cat.jpg │ └── thumb │ │ └── cat.png │ └── 011 │ ├── original │ └── cat.jpg │ └── thumb │ └── cat.png ├── spec ├── controllers │ ├── comments_controller_spec.rb │ ├── sessions_controller_spec.rb │ ├── static_pages_controller_spec.rb │ ├── stories_controller_spec.rb │ └── users_controller_spec.rb ├── factories │ ├── bookmarks.rb │ ├── comment.rb │ ├── follow.rb │ ├── likes.rb │ ├── stories.rb │ └── users.rb ├── helper_methods.rb ├── models │ ├── bookmark_spec.rb │ ├── comment_spec.rb │ ├── follow_spec.rb │ ├── like_spec.rb │ ├── story_spec.rb │ └── user_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── test ├── controllers │ ├── .keep │ ├── api │ │ └── stories_controller_test.rb │ ├── comments_controller_test.rb │ └── likes_controller_test.rb ├── fixtures │ ├── .keep │ ├── bookmarks.yml │ ├── comments.yml │ ├── follows.yml │ ├── likes.yml │ └── stories.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── bookmark_test.rb │ ├── comment_test.rb │ ├── follow_test.rb │ ├── like_test.rb │ └── story_test.rb └── test_helper.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | // .gitignore 9 | 10 | // ... a bunch of preloaded ignores 11 | 12 | // To add: 13 | node_modules/ 14 | bundle.js 15 | bundle.js.map 16 | .byebug_history 17 | .DS_Store 18 | npm-debug.log 19 | tmp/ 20 | 21 | # Ignore application configuration 22 | /config/application.yml 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format documentation -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - postgresql 3 | before_script: 4 | - psql -c 'create database travis_ci_test;' -U postgres -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '4.2.8' 6 | # Use postgresql as the database for Active Record 7 | gem 'pg', '~> 0.15' 8 | # Use SCSS for stylesheets 9 | gem 'sass-rails', '~> 5.0' 10 | # Use Uglifier as compressor for JavaScript assets 11 | gem 'uglifier', '>= 1.3.0' 12 | # Use CoffeeScript for .coffee assets and views 13 | gem 'coffee-rails', '~> 4.1.0' 14 | gem 'annotate' 15 | gem 'bcrypt' 16 | # See https://github.com/rails/execjs#readme for more supported runtimes 17 | # gem 'therubyracer', platforms: :ruby 18 | gem "paperclip", "~> 5.0.0.beta1" 19 | # Use jquery as the JavaScript library 20 | gem 'jquery-rails' 21 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 22 | gem 'jbuilder', '~> 2.0' 23 | # bundle exec rake doc:rails generates the API under doc/api. 24 | gem 'sdoc', '~> 0.4.0', group: :doc 25 | gem 'figaro' 26 | gem 'aws-sdk', '>= 2.0' 27 | 28 | # Use ActiveModel has_secure_password 29 | # gem 'bcrypt', '~> 3.1.7' 30 | 31 | # Use Unicorn as the app server 32 | # gem 'unicorn' 33 | 34 | # Use Capistrano for deployment 35 | # gem 'capistrano-rails', group: :development 36 | 37 | group :production, :development do 38 | gem 'rails_12factor' 39 | end 40 | 41 | group :development do 42 | gem 'web-console', '~> 2.0' 43 | end 44 | group :development, :test do 45 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 46 | gem 'byebug' 47 | gem 'pry-rails' 48 | gem 'spring' 49 | gem 'rspec-rails' 50 | gem 'factory_bot_rails' 51 | gem 'rails-controller-testing' 52 | end 53 | 54 | group :test do 55 | gem 'faker' 56 | gem 'guard-rspec' 57 | gem 'launchy' 58 | gem 'shoulda-matchers', '~> 3.1' 59 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Large [![Build Status](https://api.travis-ci.org/Th3Nathan/Large.svg?branch=master)](https://travis-ci.org/Th3Nathan/Large) 2 | 3 | Large is a blogging web app based on Medium. It was built using a Rails backend with a React/Redux frontend. 4 | 5 | Check it out at at [www.large-blog.site](http://large-blog.site/) 6 | 7 | ### User View: 8 | 9 | ![user] 10 | 11 | ### Edit View: 12 | 13 | ![edit] 14 | 15 | ### Technical Details: 16 | 17 | * Both stories and comments can be liked by users. In order to reduce duplicate code, I did using a polymorphic association, both stories and comments are able to update their corresponding like information through updating thier own associations. I did this by hitting the update action of the story or comment controller with nested attributes containing like associaion information. 18 | 19 | ``` 20 | #Story and Comment model 21 | has_many :likes, as: :likeable 22 | accepts_nested_attributes_for :likes, allow_destroy: true 23 | 24 | //API Util 25 | export const updateStoryLikes = (newLike, id) => { 26 | return $.ajax({ 27 | method: "PATCH", 28 | url: `/api/stories/${id}`, 29 | data: {story: { likes_attributes: newLike } }, 30 | }); 31 | }; 32 | 33 | ``` 34 | 35 | * I wanted users to be able to easily create stories with multiple images with minimal overhead, so I overwrote the Quill text editor I used for creating stories to accept image urls instead of image files. 36 | 37 | ``` 38 | linkHandler(value){ 39 | if (value) { 40 | let href = prompt('Enter the URL'); 41 | this.quillRef.getEditor().format('link', href); 42 | } else { 43 | this.quillRef.getEditor().format('link', false); 44 | } 45 | } 46 | ``` 47 | 48 | ### Features 49 | * Sign up/in with frontend/backend validations 50 | * Create stories using a text editor, supporting multiple images 51 | * Story draft will be saved during a users session 52 | * Stories and comments can be liked by users 53 | * Users can access a feed containing stories by users they follow 54 | * Image hosting on Amazon web services, users can upload and change thier avatar image 55 | 56 | 57 | [Original Design Docs](./docs/README.md) 58 | 59 | [user]: ./docs/images/edit.png 60 | [edit]: ./docs/images/show.png 61 | -------------------------------------------------------------------------------- /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 File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/assets/images/logo.png -------------------------------------------------------------------------------- /app/assets/images/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/assets/images/logo2.png -------------------------------------------------------------------------------- /app/assets/javascripts/api/stories.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/comments.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/likes.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/stories.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/stories controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/auth/auth.scss: -------------------------------------------------------------------------------- 1 | #session-form { 2 | width: 450px; 3 | justify-content: center; 4 | margin-top: 105px; 5 | text-align: center; 6 | background-color: white; 7 | height: 550px; 8 | z-index: 10; 9 | position: relative; 10 | margin: auto; 11 | background: #D7EFEE; 12 | top: 20px; 13 | box-shadow: 0 2px 10px rgba(0,0,0,.15); 14 | } 15 | 16 | #file-input { 17 | margin-top: 20px; 18 | margin-bottom: -20px; 19 | } 20 | 21 | #image-preview { 22 | position: absolute; 23 | width: 70px; 24 | height: 70px; 25 | bottom: 230px; 26 | right: 20px; 27 | border-radius: 50px; 28 | } 29 | 30 | #first-error { 31 | margin-top: -15px; 32 | } 33 | 34 | .logo { 35 | height: 85%; 36 | } 37 | 38 | #form-header-box { 39 | display: inline-block; 40 | color: black; 41 | background-color:#f0f0f0; 42 | width: 100%; 43 | height: 160px; 44 | cursor:default; 45 | } 46 | 47 | #form-header-box h1 { 48 | float: right; 49 | font-weight: 300; 50 | font-weight: 300; 51 | font-size: 25px; 52 | } 53 | 54 | #rainbow-title { 55 | margin-top: 40px; 56 | } 57 | 58 | #rainbow-title span { 59 | font-size: 50px; 60 | } 61 | 62 | .form-header { 63 | display: flex; 64 | justify-content: center; 65 | height: 90px; 66 | margin-bottom: 2px; 67 | font-family: Georgia,Cambria,"Times New Roman",Times,serif!important; 68 | } 69 | 70 | .form-header a div h1 { 71 | font-size: 32px; 72 | margin-top: 26px; 73 | margin-left: 1px; 74 | } 75 | #errors-list { 76 | list-style-type: none; 77 | color: darkred; 78 | padding: 0; 79 | } 80 | 81 | #pencil { 82 | position: absolute; 83 | width: 20px; 84 | right: 92px; 85 | top: 300px; 86 | cursor:pointer; 87 | } 88 | 89 | .error { 90 | margin: 0px; 91 | padding: 0px; 92 | font-size: 0.7em; 93 | color: darkred; 94 | height: 15px; 95 | } 96 | 97 | #error-item { 98 | height: 22px; 99 | margin: 0; 100 | } 101 | 102 | .input-label { 103 | color: rgba(0,0,0,.6); 104 | } 105 | 106 | .input-label input { 107 | margin: 14px; 108 | font-size: 1.3em; 109 | padding-top: 0px; 110 | margin-top: 0px; 111 | border: 0; 112 | text-align: center; 113 | outline: 0; 114 | background: transparent; 115 | border-bottom: 1px solid rgba(0,0,0,.5); 116 | color: rgba(0,0,0,.9); 117 | font-family: Georgia,Cambria,"Times New Roman",Times,serif!important; 118 | } 119 | 120 | 121 | #session-form input[type="submit"] { 122 | font-size: 1.2em; 123 | margin-bottom: 8px; 124 | } 125 | 126 | #file-input { 127 | padding-top: 5px; 128 | margin-bottom: 7px; 129 | } 130 | 131 | #toggle-sign-button { 132 | padding: 3px; 133 | color: black; 134 | font-size: 1.2em; 135 | color: #1c9963; 136 | text-decoration: none; 137 | } 138 | 139 | #overlay { 140 | width: 100%; 141 | height: 100%; 142 | transform-origin: bottom center; 143 | position: fixed; 144 | left: 0; 145 | right: 0; 146 | top: 0; 147 | bottom: 0; 148 | display: flex; 149 | justify-content: center; 150 | background-color: rgba(0,0,0,.6); 151 | } 152 | 153 | .modal-button { 154 | background: #03a87c; 155 | border-color: #03a87c; 156 | border-radius:5px; 157 | color:white; 158 | transition: all 0.3s ease 0s; 159 | width: 70px; 160 | font-size: 1.2em; 161 | height: 35px; 162 | cursor: pointer; 163 | margin-left: 3px; 164 | margin-right: 3px; 165 | } 166 | 167 | .modal-button:hover { 168 | background: #026147; 169 | border-color: #03a87c; 170 | border-radius:5px; 171 | } 172 | -------------------------------------------------------------------------------- /app/assets/stylesheets/comment/comment.scss: -------------------------------------------------------------------------------- 1 | .comment { 2 | width: 650px; 3 | padding: 20px 15px; 4 | border: 1px solid rgba(0, 0, 0, .1); 5 | cursor: pointer; 6 | } 7 | 8 | .comment-author-box { 9 | width:40%; 10 | } 11 | 12 | .comment-author-box-image { 13 | width: 0px !important; 14 | margin-right: 45px !important; 15 | } 16 | 17 | .comment-author-box-info {} 18 | .comment-author-box-info a { 19 | text-decoration: none; 20 | } 21 | .comment-author-box-info a:hover { 22 | text-decoration: none !important; 23 | } 24 | 25 | .comment-author-box-username { 26 | font-weight: 400 !important; 27 | font-style: normal !important; 28 | font-size: 14px !important; 29 | color: rgba(0,0,0,.54) !important; 30 | text-decoration: none; 31 | } 32 | 33 | .comment-author-box-datetime {} 34 | 35 | .comment-author-box-text { 36 | } 37 | 38 | .comment-body { 39 | font-size: 18px; 40 | color: rgba(0,0,0,.8); 41 | margin: 0; 42 | margin-top: 5px; 43 | } 44 | 45 | .comment-icon { 46 | color: rgba(0, 0, 0, 0.44); 47 | } 48 | 49 | .comment-footer { 50 | display: flex; 51 | justify-content: space-between; 52 | } 53 | 54 | .comment > .comment-footer > .comment-like-count { 55 | margin-left: -92% !important; 56 | } 57 | 58 | .comment-icon { 59 | 60 | } 61 | 62 | .comment-author-box-icons a:hover, 63 | .comment-author-box-icons a:focus, 64 | .comment-like-wrapper a:hover, 65 | .comment-like-wrapper a:focus { 66 | text-decoration: none !important; 67 | } 68 | 69 | .comment-author-box-icons, 70 | .comment-like-wrapper, 71 | { 72 | text-decoration: none !important; 73 | font-size: 20px; 74 | } 75 | 76 | .comment-like-wrapper a { 77 | padding: 3px; 78 | } 79 | 80 | .comment-num-likes { 81 | z-index: -10; 82 | } 83 | 84 | .comment-lock { 85 | padding: 3px; 86 | } 87 | 88 | .comment-num-likes { 89 | color: rgba(0, 0, 0, 0.44); 90 | } 91 | 92 | .comment-num-likes:hover { 93 | color: rgba(0, 0, 0, 0.44); 94 | } 95 | 96 | .comment-body-wrapper { 97 | cursor: pointer; 98 | } 99 | 100 | .comment-like-count{ 101 | margin-left: -86%; 102 | margin-top: 4px; 103 | text-decoration: none; 104 | color: rgba(0, 0, 0, 0.7); 105 | } 106 | -------------------------------------------------------------------------------- /app/assets/stylesheets/comment/comment_show.scss: -------------------------------------------------------------------------------- 1 | .comment-show-author-box { 2 | height: 40px; 3 | display: flex; 4 | width: 740px; 5 | margin: auto; 6 | margin-top: 29px; 7 | padding-left: 25px; 8 | } 9 | 10 | .comment-show-author-box-image { 11 | width: 15%; 12 | margin-right: -40px; 13 | } 14 | 15 | .comment-show-author-box-image img { 16 | border-radius: 50px; 17 | height: 60px; 18 | width: 60px; 19 | } 20 | 21 | .comment-show-author-box-info { 22 | width: 70%; 23 | position: relative; 24 | } 25 | 26 | .comment-show-author-box-info a { 27 | text-decoration: none; 28 | } 29 | 30 | .comment-show-author-box-username { 31 | color: rgba(0, 0, 0, 0.8); 32 | display: inline-block; 33 | margin: 0px; 34 | margin-left: 8px; 35 | margin-top: 1px; 36 | font-size: 15px; 37 | 38 | } 39 | 40 | .comment-show-author-box-username:hover { 41 | 42 | } 43 | 44 | .comment-show-author-box-follow { 45 | border-radius: 50px; 46 | color: #02B875; 47 | width: 59px; 48 | padding: 0px 8px 0px 8px; 49 | line-height: 1.35; 50 | font-size: 12px; 51 | border: 1px solid #02B875; 52 | cursor: pointer; 53 | text-align: center; 54 | position: absolute; 55 | top: 4px; 56 | left: 120px; 57 | } 58 | 59 | .comment-like-count-show { 60 | margin-left: -85%; 61 | margin-top: 4px; 62 | text-decoration: none; 63 | color: rgba(0, 0, 0, 0.7); 64 | } 65 | 66 | .comment-show-author-box-follow:hover { 67 | color: #02B875; 68 | border: 1px solid #1c9963; 69 | } 70 | 71 | .comment-show-author-box-datetime { 72 | display: flex; 73 | } 74 | 75 | .comment-show-author-box-bio { 76 | font-size: 13px; 77 | line-height: 1.4; 78 | padding-left: 12px; 79 | margin-bottom: 1px; 80 | width: 140%; 81 | color: rgba(0,0,0,.44); 82 | } 83 | 84 | .comment-show-author-box-text { 85 | font-size: 12px; 86 | margin-right: -2px; 87 | color: rgba(0, 0, 0, 0.44); 88 | line-height: 1.2; 89 | margin-top: 1px; 90 | padding-left: 7px; 91 | } 92 | 93 | .comment-show-author-box-dot { 94 | color: rgba(0, 0, 0, 0.44); 95 | position: relative; 96 | top: -7px; 97 | right: -4px; 98 | } 99 | 100 | .comment-show-author-box-icons { 101 | font-size: 20px; 102 | display: flex; 103 | padding: 7px; 104 | cursor: pointer; 105 | } 106 | 107 | .lock:hover .icon-unlock, 108 | .lock .icon-lock { 109 | display: none; 110 | } 111 | .lock:hover .icon-lock { 112 | display: inline; 113 | } 114 | 115 | 116 | .fa { 117 | padding: 4px; 118 | -webkit-transition: all 0.5s; 119 | transition: all 0.5s; 120 | } 121 | 122 | .fa-bookmark-o, .fa-angle-down { 123 | color: rgba(0, 0, 0, 0.44); 124 | -webkit-transition: all 0.5s; 125 | transition: all 0.5s; 126 | } 127 | 128 | .fa-bookmark-o:hover, .fa-angle-down:hover { 129 | color: black; 130 | -webkit-transition: all 0.5s; 131 | transition: all 0.5s; 132 | } 133 | 134 | .show-body { 135 | font-size: 21px; 136 | color: rbga(0, 0, 0, 0.8); 137 | border: none; 138 | outline: none; 139 | width: 100%; 140 | height: 150px; 141 | } 142 | 143 | .comment-wrap { 144 | width: 700px; 145 | margin: auto; 146 | margin-top: 50px; 147 | } 148 | 149 | .show-footer { 150 | width: 700px; 151 | margin: auto; 152 | margin-top: 20px; 153 | } 154 | 155 | .fa-pencil-square-o, .fa-trash-o { 156 | color: rgba(0, 0, 0, 0.44); 157 | } 158 | -------------------------------------------------------------------------------- /app/assets/stylesheets/comment/new_comment.scss: -------------------------------------------------------------------------------- 1 | .new-comment textarea { 2 | resize: none; 3 | outline: none; 4 | border: 1px solid rgba(0, 0, 0, .1); 5 | width: 81.2%; 6 | height: 120px; 7 | padding: 10px; 8 | color: rgba(0, 0, 0, .8); 9 | } 10 | 11 | .new-comment-submit { 12 | font-size: 15px; 13 | color: #1c9963; 14 | transition: all .3s; 15 | border: 1px solid #02b875; 16 | background: white; 17 | border-radius: 50px; 18 | outline: none; 19 | width: 65px; 20 | height: 40px; 21 | margin-bottom: 20px; 22 | margin-right: 10px; 23 | } 24 | 25 | .new-comment-submit:hover { 26 | border: 1px solid #00ab6b;; 27 | } 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | border-top: 2px solid rgba(0,0,0,.05); 3 | color: rgba(0, 0, 0, 0.44); 4 | font-size: 15px; 5 | font-weight: 500px; 6 | height: 75px; 7 | margin-top: 10px; 8 | padding-top: 5px; 9 | } 10 | 11 | footer a { 12 | transition: .3s background-color,.3s border-color,.3s color,.3s fill; 13 | font-size: 15px; 14 | cursor: pointer; 15 | padding: 10px; 16 | font-family: Verdana, sans-serif; 17 | text-decoration: none; 18 | } 19 | 20 | footer a:hover { 21 | color:rgba(0,0,0,.7); 22 | text-decoration: underline; 23 | } 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/header/_header.scss: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: 'Source Sans Pro', sans-serif; 4 | } 5 | 6 | #header-thumbnail { 7 | position: absolute; 8 | width: 50px; 9 | height: 50px; 10 | right: -69px; 11 | border-radius: 50px; 12 | top: -3px; 13 | border: 1px solid #02b875; 14 | cursor: pointer; 15 | object-fit: cover; 16 | } 17 | 18 | #title { 19 | display: inline-block; 20 | color: black; 21 | cursor: pointer; 22 | height: 65px; 23 | } 24 | 25 | .error h5 { 26 | 27 | } 28 | 29 | #root { 30 | max-width: 1000px; 31 | margin: 0 auto; 32 | } 33 | 34 | header { 35 | height: 65px; 36 | border-bottom: 1px solid rgba(0,0,0,.05); 37 | letter-spacing: 0.7px; 38 | text-decoration: none; 39 | } 40 | 41 | header h1 { 42 | float: left; 43 | font-weight: 300; 44 | font-size: 25px; 45 | margin-top: 17px; 46 | margin-left: 5px; 47 | } 48 | 49 | header section { 50 | float: right; 51 | position: relative; 52 | } 53 | 54 | header img { 55 | height: 85%; 56 | float: left; 57 | } 58 | 59 | header section { 60 | margin-top: 16px; 61 | margin-right: 20px; 62 | } 63 | 64 | header section a, header section span { 65 | color: #02b875; 66 | text-decoration: none; 67 | font-size: 15px; 68 | cursor: pointer; 69 | text-decoration: none; 70 | padding: 3px; 71 | font-weight: 200px; 72 | } 73 | 74 | #greeting { 75 | text-decoration: none; 76 | padding: 3px; 77 | font-weight: 200px; 78 | color: #02b875; 79 | text-decoration: none; 80 | font-size: 15px; 81 | margin-top: -3px; 82 | cursor: pointer; 83 | } 84 | 85 | #signout { 86 | top: -15px; 87 | position: relative; 88 | margin-left: 15px; 89 | font-size: 12px; 90 | } 91 | 92 | header section a:hover, span:hover { 93 | color: #1c9963; 94 | } 95 | 96 | #new-story-link, header section a { 97 | transition: .1s background-color,.1s border-color,.1s color,.1s fill; 98 | } 99 | 100 | #new-story-link { 101 | margin-right: 30px; 102 | color: rgba(0,0,0,.44); 103 | text-decoration: none; 104 | margin-top: 12px; 105 | font-size: 15px; 106 | float: right; 107 | padding: 3px; 108 | font-weight: 500px; 109 | } 110 | 111 | #new-story-link:hover { 112 | color:rgba(0,0,0,.7) 113 | } 114 | -------------------------------------------------------------------------------- /app/assets/stylesheets/header/edit_header.scss: -------------------------------------------------------------------------------- 1 | .edit-header {} 2 | .edit-button { 3 | cursor: pointer; 4 | font-size: 16px; 5 | } 6 | 7 | .edit-button:hover { 8 | color: #00ab6b; 9 | } 10 | .edit-button { 11 | color: #02b875; 12 | } 13 | -------------------------------------------------------------------------------- /app/assets/stylesheets/header/new_story_header.scss: -------------------------------------------------------------------------------- 1 | .new-story-header { 2 | color: rgba(0,0,0,.6); 3 | display: flex; 4 | } 5 | .new-story-share h5 { 6 | font-size: 14px; 7 | color: rgba(0,0,0,.4); 8 | margin-right: 20px; 9 | } 10 | .new-story-share h5:hover { 11 | color: rgba(0,0,0,.6); 12 | cursor: pointer; 13 | } 14 | .new-story-publish { 15 | cursor: pointer; 16 | color: #00ab6b; 17 | display: flex; 18 | margin-right: 15px; 19 | } 20 | .new-story-publish fa {} 21 | 22 | .new-story-publish h5:hover { 23 | cursor:pointer; 24 | color: #00ab6b; 25 | } 26 | .new-story-publish h5 { 27 | color: #02b875; 28 | font-size: 14px; 29 | } 30 | 31 | .new-story-publish .fa-angle-down:hover { 32 | color: #00ab6b; 33 | } 34 | .new-story-publish .fa-angle-down { 35 | color: #02b875; 36 | } 37 | 38 | .fa-ellipsis-h { 39 | cursor:pointer; 40 | } 41 | 42 | .fa-ellipsis-h:hover { 43 | cursor:pointer; 44 | color: rgba(0,0,0,.8); 45 | } 46 | 47 | .header-fa { 48 | transition: none !important; 49 | position: relative; 50 | top: 5px; 51 | font-size: 20px !important; 52 | } 53 | 54 | .click-listener { 55 | position: absolute; 56 | left: -1000px; 57 | top: 0; 58 | right: -1000px; 59 | bottom: -1000px; 60 | } 61 | 62 | .new-story-dropdown { 63 | position: absolute; 64 | list-style-type: none; 65 | border: 1px solid rgba(0,0,0,.2); 66 | padding: 15px; 67 | right: -95px; 68 | top: 34px; 69 | width: 210px; 70 | padding-bottom: 40px; 71 | margin-bottom: 10px; 72 | background: white; 73 | z-index: 2; 74 | } 75 | 76 | .new-story-dropdown-title { 77 | font-size: 12px; 78 | } 79 | 80 | .new-story-dropdown-item { 81 | position: absolute; 82 | z-index: 100; 83 | cursor: pointer; 84 | } 85 | -------------------------------------------------------------------------------- /app/assets/stylesheets/likes.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the likes controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | .hidden { 5 | display: none; 6 | } 7 | 8 | .collapse { 9 | height: 0px; 10 | } 11 | 12 | 13 | .lock:hover .icon-unlock, 14 | .lock .icon-lock { 15 | display: none; 16 | } 17 | .lock:hover .icon-lock { 18 | display: inline; 19 | } 20 | 21 | 22 | .fa { 23 | padding: 4px; 24 | -webkit-transition: all 0.5s; 25 | transition: all 0.5s; 26 | } 27 | 28 | .fa-bookmark-o, .fa-angle-down { 29 | color: rgba(0, 0, 0, 0.44); 30 | -webkit-transition: all 0.5s; 31 | transition: all 0.5s; 32 | } 33 | 34 | .fa-bookmark-o:hover, .fa-angle-down:hover { 35 | color: black; 36 | -webkit-transition: all 0.5s; 37 | transition: all 0.5s; 38 | } 39 | 40 | input { 41 | background: white; 42 | } 43 | 44 | textarea { 45 | background: white; 46 | resize:none; 47 | cols:5; 48 | } 49 | 50 | 51 | @-webkit-keyframes doit /* Safari and Chrome */ 52 | { 53 | 0% { 54 | 55 | transform:scale(1,1); 56 | -ms-transform:scale(1,1); 57 | -moz-transform:scale(1,1); 58 | -webkit-transform:scale(1,1); 59 | -o-transform:scale(1,1); 60 | 61 | } 62 | 63 | 50% { 64 | 65 | transform:scale(0.9,0.9); 66 | -ms-transform:scale(0.9,0.9); 67 | -moz-transform:scale(0.9,0.9); 68 | -webkit-transform:scale(0.9,0.9); 69 | -o-transform:scale(0.9,0.9); 70 | 71 | 72 | } 73 | 74 | 100% { 75 | 76 | transform:scale(1,1); 77 | -ms-transform:scale(1,1); 78 | -moz-transform:scale(1,1); 79 | -webkit-transform:scale(1,1); 80 | -o-transform:scale(1,1); 81 | 82 | } 83 | 84 | 85 | } 86 | 87 | 88 | .press 89 | { 90 | animation: doit 0.2s; 91 | -moz-animation: doit 1s; /* Firefox */ 92 | -webkit-animation: doit 0.2s; /* Safari and Chrome */ 93 | -webkit-animation-timing-function: linear; 94 | animation-fill-mode: forwards; 95 | -webkit-animation-fill-mode: forwards; 96 | } 97 | 98 | .like-wrapper:active, .lock:active, .fa:active, .comment-like-wrapper:active { 99 | animation: doit 0.2s; 100 | -moz-animation: doit 1s; /* Firefox */ 101 | -webkit-animation: doit 0.2s; /* Safari and Chrome */ 102 | -webkit-animation-timing-function: linear; 103 | animation-fill-mode: forwards; 104 | -webkit-animation-fill-mode: forwards; 105 | } 106 | 107 | 108 | 109 | // .share-icon:active { 110 | // animation: doit 0.2s !important; 111 | // -moz-animation: doit 1s !important; /* Firefox */ 112 | // -webkit-animation: doit 0.2s !important; /* Safari and Chrome */ 113 | // -webkit-animation-timing-function: linear !important; 114 | // animation-fill-mode: forwards !important; 115 | // -webkit-animation-fill-mode: forwards !important; 116 | // } 117 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/author_box.scss: -------------------------------------------------------------------------------- 1 | .author-box { 2 | height: 40px; 3 | display: flex; 4 | } 5 | 6 | .author-box-image { 7 | width: 15%; 8 | margin-right: 10px; 9 | } 10 | 11 | .author-box-image img { 12 | object-fit: cover; 13 | border-radius: 50px; 14 | height: 40px; 15 | width: 40px; 16 | } 17 | 18 | .author-box-info { 19 | width: 70%; 20 | } 21 | 22 | .author-box-username { 23 | color: rgba(0, 0, 0, 0.8); 24 | margin: 0px; 25 | margin-left: 8px; 26 | margin-top: 1px; 27 | font-size: 12px; 28 | } 29 | 30 | .author-box-username:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .author-box-datetime { 35 | display: flex; 36 | } 37 | 38 | .author-box-text { 39 | font-size: 12px; 40 | margin-right: -2px; 41 | color: rgba(0, 0, 0, 0.44); 42 | line-height: 1.2; 43 | margin-top: 1px; 44 | padding-left: 7px; 45 | } 46 | 47 | .author-box-dot { 48 | color: rgba(0, 0, 0, 0.44); 49 | position: relative; 50 | top: -7px; 51 | right: -4px; 52 | } 53 | 54 | .author-box-icons { 55 | font-size: 20px; 56 | display: flex; 57 | padding: 7px; 58 | cursor: pointer; 59 | } 60 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/blurb.scss: -------------------------------------------------------------------------------- 1 | .blurb { 2 | display: flex; 3 | width: 48%; 4 | height: 280px; 5 | border: 1px solid rgba(0,0,0,.1); 6 | margin: 10px; 7 | } 8 | 9 | .story-text-wrapper { 10 | height: 200px; 11 | } 12 | 13 | .blurb-image { 14 | width: 40%; 15 | height: 100%; 16 | background-position: center; 17 | background-size: cover; 18 | } 19 | 20 | .blurb-image img { 21 | width: 100%; 22 | } 23 | 24 | .blurb-text { 25 | padding: 20px; 26 | width: 300px; 27 | } 28 | 29 | .blurb-text a { 30 | text-decoration: none; 31 | } 32 | 33 | .blurb-title { 34 | font-size: 20px; 35 | font-weight: bold; 36 | } 37 | 38 | .blurb-description { 39 | font-weight: 400; 40 | font-size: 14px; 41 | color: rgba(0,0,0,.44); 42 | 43 | height: 0px; 44 | padding-bottom: 25px; 45 | } 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/new_story.scss: -------------------------------------------------------------------------------- 1 | #new-story { 2 | position: relative; 3 | } 4 | 5 | .new-story-title { 6 | overflow: visible; 7 | border: none; 8 | outline: none; 9 | font-size: 20px; 10 | color: rgba(0, 0, 0, 0.5); 11 | } 12 | 13 | .new-story-description { 14 | overflow: visible; 15 | border: none; 16 | outline: none; 17 | font-size: 20px; 18 | color: rgba(0, 0, 0, 0.5); 19 | } 20 | 21 | .fa-picture-o { 22 | position: relative; 23 | left: -425px; 24 | margin-bottom: 120px; 25 | top: 78px; 26 | font-size: 30px !important; 27 | color: rgba(0, 0, 0, 0.5); 28 | cursor: pointer; 29 | } 30 | 31 | #story-image-preview { 32 | max-height: 110px; 33 | position: absolute; 34 | left: 256px; 35 | top: 120px; 36 | } 37 | 38 | #text-editor { 39 | margin-top: 30px; 40 | } 41 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/share_bar.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | #share-bar { 4 | width: 50px; 5 | position: fixed; 6 | text-decoration: none; 7 | align-items: center; 8 | top: 90px; 9 | } 10 | 11 | #share-bar-title { 12 | font-size: 12px; 13 | font-weight: bold; 14 | color: rgba(0,0,0,.44); 15 | } 16 | 17 | #share-bar a { 18 | text-decoration: none; 19 | font-size: 25px; 20 | color: rgba(0,0,0,.44); 21 | padding-left: 4px; 22 | } 23 | 24 | .share-icon { 25 | width: 100%; 26 | cursor:pointer; 27 | color: rgba(0, 0, 0, 0.44); 28 | } 29 | 30 | .story-like-count { 31 | font-size: 15px !important; 32 | margin: auto; 33 | padding-left: 10px !important; 34 | top: -5px; 35 | position: relative; 36 | margin-left: 6.25px; 37 | } 38 | 39 | #heart-unclicked i { 40 | color: #02B875; 41 | width: 100% !important; 42 | } 43 | 44 | .like-wrapper { 45 | display: block; 46 | } 47 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/stories.scss: -------------------------------------------------------------------------------- 1 | .story-index { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .index-title{ 7 | font-size: 20px; 8 | text-decoration: none; 9 | color: rgba(0, 0, 0, 0.8); 10 | padding: 10px; 11 | margin-top: 20px; 12 | height: 500px !important; 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/story_author_box.scss: -------------------------------------------------------------------------------- 1 | .story-author-box { 2 | height: 40px; 3 | display: flex; 4 | width: 740px; 5 | margin: auto; 6 | margin-top: 29px; 7 | padding-left: 25px; 8 | } 9 | 10 | .story-author-box-image { 11 | width: 15%; 12 | margin-right: -40px; 13 | } 14 | 15 | .story-author-box-image img { 16 | border-radius: 50px; 17 | height: 60px; 18 | width: 60px; 19 | object-fit: cover; 20 | } 21 | 22 | .story-author-box-info { 23 | width: 70%; 24 | position: relative; 25 | } 26 | 27 | .story-author-box-info a { 28 | text-decoration: none; 29 | } 30 | 31 | .story-author-box-username { 32 | color: rgba(0, 0, 0, 0.8); 33 | display: inline-block; 34 | margin: 0px; 35 | margin-left: 8px; 36 | margin-top: 1px; 37 | font-size: 15px; 38 | 39 | } 40 | 41 | .story-author-box-username:hover { 42 | 43 | } 44 | 45 | .story-author-box-follow { 46 | border-radius: 50px; 47 | color: #02B875; 48 | width: 59px; 49 | padding: 0px 8px 0px 8px; 50 | line-height: 1.35; 51 | font-size: 12px; 52 | border: 1px solid #02B875; 53 | cursor: pointer; 54 | text-align: center; 55 | position: absolute; 56 | top: 4px; 57 | left: 120px; 58 | } 59 | 60 | .story-author-box-unfollow { 61 | border-radius: 50px; 62 | color: white; 63 | padding: 0px 8px 0px 8px; 64 | line-height: 1.35; 65 | font-size: 12px; 66 | border: 1px solid #02B875; 67 | cursor: pointer; 68 | text-align: center; 69 | position: absolute; 70 | top: 4px; 71 | left: 120px; 72 | background: #1c9963; 73 | border: 1px solid #02b875; 74 | } 75 | 76 | 77 | .story-author-box-follow:hover { 78 | border: 1px solid #1c9963; 79 | } 80 | 81 | .story-author-box-unfollow:hover { 82 | color: white; 83 | } 84 | 85 | 86 | 87 | .story-author-box-datetime { 88 | display: flex; 89 | } 90 | 91 | .story-author-box-bio { 92 | font-size: 13px; 93 | line-height: 1.4; 94 | padding-left: 12px; 95 | margin-bottom: 1px; 96 | width: 140%; 97 | color: rgba(0,0,0,.44); 98 | } 99 | 100 | .story-author-box-text { 101 | font-size: 12px; 102 | margin-right: -2px; 103 | color: rgba(0, 0, 0, 0.44); 104 | line-height: 1.2; 105 | margin-top: 1px; 106 | padding-left: 7px; 107 | } 108 | 109 | .story-author-box-dot { 110 | color: rgba(0, 0, 0, 0.44); 111 | position: relative; 112 | top: -7px; 113 | right: -4px; 114 | } 115 | 116 | .story-author-box-icons { 117 | font-size: 20px; 118 | display: flex; 119 | padding: 7px; 120 | cursor: pointer; 121 | } 122 | 123 | .lock:hover .icon-unlock, 124 | .lock .icon-lock { 125 | display: none; 126 | } 127 | .lock:hover .icon-lock { 128 | display: inline; 129 | } 130 | 131 | 132 | .fa { 133 | padding: 4px; 134 | -webkit-transition: all 0.5s; 135 | transition: all 0.5s; 136 | } 137 | 138 | .fa-bookmark-o, .fa-angle-down { 139 | color: rgba(0, 0, 0, 0.44); 140 | -webkit-transition: all 0.5s; 141 | transition: all 0.5s; 142 | } 143 | 144 | .fa-bookmark-o:hover, .fa-angle-down:hover { 145 | color: black; 146 | -webkit-transition: all 0.5s; 147 | transition: all 0.5s; 148 | } 149 | -------------------------------------------------------------------------------- /app/assets/stylesheets/stories/story_show.scss: -------------------------------------------------------------------------------- 1 | 2 | .story-show { 3 | width: 80%; 4 | margin: auto; 5 | } 6 | 7 | .story-title { 8 | color: rgba(0,0,0,.8); 9 | font-size: 40px; 10 | font-weight: bold; 11 | letter-spacing: -1.12px; 12 | margin-top: 40px; 13 | } 14 | 15 | .story-body { 16 | 17 | } 18 | 19 | .story-body img { 20 | max-width: 100%; 21 | height: auto; 22 | padding: 30px; 23 | } 24 | 25 | .story-show-image-container{ 26 | margin-top: 40px; 27 | } 28 | 29 | .stories-show-image { 30 | width: 100%; 31 | padding: 30px; 32 | } 33 | 34 | .edit-link { 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/assets/stylesheets/user/user_show.scss: -------------------------------------------------------------------------------- 1 | .user-show { 2 | 3 | } 4 | .user-show-profile-box { 5 | display: flex; 6 | width: 640px; 7 | margin: auto; 8 | padding: 20px; 9 | margin-top: 30px; 10 | } 11 | .user-show-info { 12 | width: 80%; 13 | } 14 | 15 | .user-show-info input { 16 | border: none; 17 | outline: none; 18 | } 19 | 20 | .user-show-name { 21 | font-size: 30px; 22 | padding-bottom: 0px; 23 | font-weight: 550; 24 | 25 | } 26 | 27 | .user-show-bio { 28 | font-size: 16px; 29 | color: rgba(0,0,0,.6); 30 | margin-left: 10px; 31 | width: 110%; 32 | border:none; 33 | outline:none; 34 | } 35 | 36 | .user-show-following { 37 | display: flex; 38 | margin-left: 10px; 39 | margin-top: 30px; 40 | } 41 | .follow-info { 42 | color: rgba(0,0,0,.44); 43 | font-size: 14px; 44 | } 45 | .user-show-not-editing {} 46 | .user-show-not-editing button { 47 | font-size: 15px; 48 | color: rgba(0,0,0,.44); 49 | transition: all .3s; 50 | border: 1px solid rgba(0,0,0,.14); 51 | background: white; 52 | border-radius: 50px; 53 | outline: none; 54 | width: 65px; 55 | height: 45px; 56 | margin-top: 15px; 57 | } 58 | 59 | .user-show-not-editing button:hover { 60 | border: 1px solid rgba(0,0,0,.64); 61 | } 62 | 63 | 64 | .user-show-editing { 65 | display: flex; 66 | } 67 | 68 | .user-show-editing-cancel { 69 | font-size: 15px; 70 | color: rgba(0,0,0,.44); 71 | transition: all .3s; 72 | border: 1px solid rgba(0,0,0,.14); 73 | background: white; 74 | border-radius: 50px; 75 | outline: none; 76 | width: 65px; 77 | height: 45px; 78 | margin-top: 15px; 79 | margin-right: 15px; 80 | } 81 | 82 | .user-show-editing-cancel:hover { 83 | border: 1px solid rgba(0,0,0,.64); 84 | } 85 | 86 | .user-show-editing-save { 87 | font-size: 15px; 88 | color: #1c9963; 89 | transition: all .3s; 90 | border: 1px solid #02b875; 91 | background: white; 92 | border-radius: 50px; 93 | outline: none; 94 | width: 80px; 95 | height: 45px; 96 | cursor: pointer; 97 | margin-top: 15px; 98 | margin-right: 10px; 99 | } 100 | 101 | .user-show-editing-save:hover { 102 | border: 1px solid #00ab6b;; 103 | } 104 | 105 | .user-show-unfollow { 106 | font-size: 15px; 107 | background: #1c9963; 108 | color: white; 109 | transition: all .3s; 110 | border: 1px solid #02b875; 111 | border-radius: 50px; 112 | outline: none; 113 | width: 80px; 114 | height: 45px; 115 | cursor: pointer; 116 | margin-top: 15px; 117 | margin-right: 10px; 118 | } 119 | 120 | .user-show-unfollow:hover { 121 | border: 1px solid #00ab6b;; 122 | } 123 | 124 | .user-show-editing button:hover { 125 | } 126 | 127 | 128 | .user-display-image { 129 | width: 100px; 130 | height: 100px; 131 | border-radius: 150px; 132 | position: relative; 133 | right: -80px; 134 | top: 20px; 135 | object-fit: cover; 136 | } 137 | 138 | .user-display-image img { 139 | border-radius: 150px; 140 | width: 110px; 141 | height: 110px; 142 | } 143 | 144 | .user-show-picture-overlay { 145 | color: rgba(0, 0, 0, 0.7); 146 | background: rgba(0, 0, 0, 0.1); 147 | width: 100px; 148 | height: 100px; 149 | cursor: pointer; 150 | text-align: center; 151 | padding-top: 20px; 152 | font-size: 60px !important; 153 | border-radius: 150px; 154 | left: 180px; 155 | top: 19px; 156 | z-index: 10; 157 | position: relative; 158 | } 159 | -------------------------------------------------------------------------------- /app/controllers/api/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CommentsController < ApplicationController 2 | 3 | def show 4 | @comment = Comment.includes(:likes).find(params[:id]) 5 | end 6 | 7 | def create 8 | @comment = Comment.new(comment_params) 9 | if @comment.save 10 | render :show 11 | else 12 | render json: @comment.errors.full_messages, status: 422 13 | end 14 | end 15 | 16 | def destroy 17 | comment = Comment.find(params[:id]) 18 | comment.destroy 19 | render json: {} 20 | end 21 | 22 | def update 23 | @comment = Comment.includes(:likes).find(params[:id]) 24 | if @comment.update(comment_params) 25 | render :show 26 | else 27 | render json: @comment.errors.full_messages, status: 422 28 | end 29 | end 30 | 31 | def comment_params 32 | params.require(:comment).permit(:author_id, :story_id, :body, likes_attributes: [:_destroy, :id, :user_id, :likeable_id, :likeable_type]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | def create 3 | @user = User.find_by_credentials( 4 | params[:user][:username], 5 | params[:user][:password] 6 | ) 7 | if @user 8 | login(@user) 9 | render "api/users/show" 10 | else 11 | render json: ["Invalid credentials"], status: 422 12 | end 13 | 14 | end 15 | 16 | def destroy 17 | logout 18 | render json: {} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/api/stories_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::StoriesController < ApplicationController 2 | helper_method :bookmark_id, :current_user_bookmarked? 3 | 4 | def current_user_bookmarked?(story) 5 | story.bookmarking_user_ids.include?(current_user.id) 6 | end 7 | 8 | def bookmark_id(story) 9 | ( current_user.bookmark_ids & story.bookmark_ids).first 10 | end 11 | 12 | def index 13 | @stories = Story.includes(:author, :likes) 14 | end 15 | 16 | def show 17 | @story = Story.includes(:comments, :author, :comment_authors).find(params[:id]) 18 | end 19 | 20 | def create 21 | @story = Story.new(story_params) 22 | if @story.save 23 | render :show 24 | else 25 | render json: @story.errors.full_messages, status: 422 26 | end 27 | end 28 | 29 | def destroy 30 | story = Story.find(params[:id]) 31 | story.destroy 32 | render json: {} 33 | end 34 | 35 | def update 36 | @story = Story.find(params[:id]) 37 | if @story.update(story_params) 38 | render :show 39 | else 40 | render json: @story.errors.full_messages, status: 422 41 | end 42 | end 43 | 44 | def feed 45 | @stories = current_user.feed_stories 46 | render :index 47 | end 48 | 49 | def bookmarked 50 | @stories = current_user.bookmarked_stories 51 | render :index 52 | end 53 | 54 | def story_params 55 | params.require(:story).permit( 56 | :id, 57 | :body, 58 | :title, 59 | :image, 60 | :description, 61 | :date, 62 | :author_id, 63 | likes_attributes: 64 | [:_destroy, :id, :user_id, :likeable_id, :likeable_type], 65 | bookmarks_attributes: [:_destroy, :id, :user_id, :story_id] 66 | ) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | def index 3 | @users = User.all 4 | end 5 | 6 | def show 7 | @user = User.find(params[:id]) 8 | end 9 | 10 | def create 11 | @user = User.new(user_params) 12 | if @user.save 13 | login(@user) 14 | render :show 15 | else 16 | render json: @user.errors.full_messages, status: 422 17 | end 18 | end 19 | 20 | def follow 21 | @user = User.includes(:comments, :followees, :followers).find(params[:id]) 22 | 23 | follow = Follow.new({author_id: @user.id, follower_id: current_user.id}) 24 | if follow.save 25 | 26 | render :show 27 | else 28 | render json: "Already follows" 29 | end 30 | end 31 | 32 | def unfollow 33 | @user = User.includes(:comments, :followees, :followers).find(params[:id]) 34 | @follow = Follow.where({author_id: @user.id, follower_id: current_user.id })[0] 35 | Follow.destroy(@follow.id) 36 | render :show 37 | end 38 | 39 | 40 | def update 41 | @user = User.find(params[:id]) 42 | if @user.username === "Guest" 43 | render :show 44 | return 45 | end 46 | if @user.update(user_params) 47 | render :show 48 | else 49 | render json: @user.errors.full_messages, status: 422 50 | end 51 | end 52 | 53 | def user_params 54 | params.require(:user).permit(:username, :password, :image, :bio, followed_author_follows_attributes: [:_destroy, :id, :follower_id, :author_id]) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | helper_method :login, :current_user, :logged_in?, :current_user_liked?, :like_id, :current_user_followed?, :follow_id 6 | 7 | def current_user_liked?(item) 8 | item.liker_ids.include?(current_user.id) 9 | end 10 | 11 | def current_user_followed?(author) 12 | current_user.followee_ids.include?(author.id) 13 | end 14 | 15 | def like_id(item) 16 | ( current_user.like_ids & item.like_ids).first 17 | end 18 | 19 | def follow_id(showed_user) 20 | # current_user.followees.find_by({author_id: showed_user.id}) 21 | current_user.followee_follows.where({author_id: showed_user.id}) 22 | end 23 | 24 | def login(user) 25 | user.reset_session_token! 26 | session[:session_token] = user.session_token 27 | @current_user = user 28 | end 29 | 30 | def current_user 31 | return nil unless session[:session_token] 32 | @current_user ||= User.find_by(session_token: session[:session_token]) 33 | end 34 | 35 | def logged_in? 36 | !!current_user 37 | end 38 | 39 | def logout 40 | current_user.reset_session_token! 41 | session[:session_token] = nil 42 | @current_user = nil 43 | end 44 | 45 | def require_logged_in 46 | render json: {base: ['invalid credentials']}, status: 401 if !current_user 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/api/stories_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::StoriesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module CommentsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/likes_helper.rb: -------------------------------------------------------------------------------- 1 | module LikesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/models/.keep -------------------------------------------------------------------------------- /app/models/bookmark.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookmarks 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # story_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | class Bookmark < ActiveRecord::Base 13 | validates :user_id, :story_id, presence: true 14 | 15 | # validates :user, :uniqueness => { 16 | # scope: [:user_id, :story_id] 17 | # } 18 | 19 | validates :user_id, :uniqueness => { 20 | scope: [:story_id] 21 | } 22 | 23 | belongs_to :story, 24 | primary_key: :id, 25 | foreign_key: :story_id, 26 | class_name: 'Story' 27 | 28 | belongs_to :user, 29 | primary_key: :id, 30 | foreign_key: :user_id, 31 | class_name: 'User' 32 | 33 | 34 | end 35 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string not null 7 | # date :date not null 8 | # author_id :integer not null 9 | # story_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | class Comment < ActiveRecord::Base 15 | validates :date, :body, :author_id, :story_id, presence: true 16 | after_initialize :add_date 17 | 18 | has_many :likes, as: :likeable 19 | accepts_nested_attributes_for :likes, allow_destroy: true 20 | 21 | has_many :likers, 22 | through: :likes, 23 | source: :user 24 | 25 | belongs_to :author, 26 | primary_key: :id, 27 | foreign_key: :author_id, 28 | class_name: 'User' 29 | 30 | belongs_to :story, 31 | primary_key: :id, 32 | foreign_key: :story_id, 33 | class_name: 'Story' 34 | 35 | def add_date 36 | self.date = Date.today 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/follow.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer not null 7 | # author_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | 13 | 14 | class Follow < ActiveRecord::Base 15 | validates :follower_id, :author_id, presence: true 16 | 17 | validates :follower_id, :uniqueness => { 18 | scope: [:author_id] 19 | } 20 | 21 | belongs_to :follower, 22 | primary_key: :id, 23 | foreign_key: :follower_id, 24 | class_name: 'User' 25 | 26 | belongs_to :author, 27 | primary_key: :id, 28 | foreign_key: :author_id, 29 | class_name: 'User' 30 | 31 | 32 | end 33 | -------------------------------------------------------------------------------- /app/models/like.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # likeable_id :integer 7 | # likeable_type :string 8 | # created_at :datetime 9 | # updated_at :datetime 10 | # user_id :integer not null 11 | # 12 | 13 | class Like < ActiveRecord::Base 14 | validates :user_id, :likeable_type, :likeable_id, presence: true 15 | validates :likeable_type, :uniqueness => { 16 | scope: [:likeable_id] 17 | } 18 | 19 | belongs_to :likeable, polymorphic: true 20 | belongs_to :user 21 | end 22 | -------------------------------------------------------------------------------- /app/models/story.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: stories 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # body :string not null 8 | # author_id :integer not null 9 | # date :date not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # description :string 17 | # 18 | 19 | class Story < ActiveRecord::Base 20 | validates :title, :date, :body, :author_id, :description, presence: true 21 | has_attached_file :image, default_url: "/images/logo.png" 22 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/ 23 | after_initialize :add_date 24 | 25 | 26 | has_many :likes, as: :likeable 27 | accepts_nested_attributes_for :likes, allow_destroy: true 28 | 29 | 30 | has_many :likers, 31 | through: :likes, 32 | source: :user 33 | 34 | belongs_to :author, 35 | primary_key: :id, 36 | foreign_key: :author_id, 37 | class_name: 'User' 38 | 39 | has_many :comments, 40 | primary_key: :id, 41 | foreign_key: :story_id, 42 | class_name: 'Comment' 43 | 44 | has_many :comment_authors, 45 | through: :comments, 46 | source: :author 47 | 48 | has_many :bookmarks, 49 | primary_key: :id, 50 | foreign_key: :story_id, 51 | class_name: 'Bookmark' 52 | 53 | accepts_nested_attributes_for :bookmarks, allow_destroy: true 54 | 55 | has_many :bookmarking_users, 56 | through: :bookmarks, 57 | source: :user 58 | 59 | def add_date 60 | self.date = Date.today 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # image_file_name :string 10 | # image_content_type :string 11 | # image_file_size :integer 12 | # image_updated_at :datetime 13 | # bio :string 14 | # 15 | 16 | class User < ActiveRecord::Base 17 | validates :password_digest, presence: true 18 | validates :password, length: { minimum: 6, allow_nil: true } 19 | after_initialize :ensure_session_token, :ensure_bio 20 | 21 | validates :session_token, presence: true, uniqueness: true 22 | validates :username, presence: true, uniqueness: true 23 | 24 | has_attached_file :image, styles: { thumb: ["32x32#", :png] }, default_url: "/images/logo.png" 25 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/ 26 | 27 | has_many :likes, 28 | primary_key: :id, 29 | foreign_key: :user_id, 30 | class_name: 'Like' 31 | 32 | has_many :liked_stories, through: :likes, source: :likeable, source_type: 'Story' 33 | has_many :liked_comments, through: :likes, source: :likeable, source_type: 'Comment' 34 | 35 | has_many :stories, 36 | primary_key: :id, 37 | foreign_key: :author_id, 38 | class_name: 'Story' 39 | 40 | has_many :comments, 41 | primary_key: :id, 42 | foreign_key: :author_id, 43 | class_name: 'Comment' 44 | 45 | has_many :followee_follows, 46 | primary_key: :id, 47 | foreign_key: :follower_id, 48 | class_name: 'Follow' 49 | 50 | has_many :followees, 51 | through: :followee_follows, 52 | source: :author 53 | 54 | has_many :feed_stories, 55 | through: :followees, 56 | source: :stories 57 | 58 | #these are the follows that follow us, self 59 | has_many :follower_follows, 60 | primary_key: :id, 61 | foreign_key: :author_id, 62 | class_name: 'Follow' 63 | 64 | has_many :followers, 65 | through: :follower_follows, 66 | source: :follower 67 | 68 | has_many :bookmarks, 69 | primary_key: :id, 70 | foreign_key: :user_id, 71 | class_name: 'Bookmark' 72 | 73 | has_many :bookmarked_stories, 74 | through: :bookmarks, 75 | source: :story 76 | # accepts_nested_attributes_for :followed_author_follows, allow_destroy: true 77 | 78 | attr_reader :password 79 | 80 | def self.find_by_credentials(username, password) 81 | user = User.find_by(username: username) 82 | return nil unless user 83 | return user if user && user.is_password?(password) 84 | nil 85 | end 86 | 87 | def password=(pw) 88 | @password = pw 89 | self.password_digest = BCrypt::Password.create(pw) 90 | end 91 | 92 | def is_password?(pw) 93 | BCrypt::Password.new(self.password_digest).is_password?(pw) 94 | end 95 | 96 | def reset_session_token! 97 | self.session_token = SecureRandom.urlsafe_base64 98 | self.save! 99 | self.session_token 100 | end 101 | 102 | def ensure_session_token 103 | self.session_token ||= SecureRandom.urlsafe_base64 104 | end 105 | 106 | def ensure_bio 107 | self.bio ||= "" 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /app/views/api/comments/_comment.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.set! comment.id do 2 | json.extract! comment, :id, :body, :date, :author, :story_id 3 | json.author_image comment.author.image.url 4 | 5 | json.like_count comment.likes.count 6 | json.liked_by_current_user current_user_liked?(comment) 7 | json.like_id like_id(comment) 8 | 9 | end 10 | -------------------------------------------------------------------------------- /app/views/api/comments/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "/api/comments/comment", comment: @comment 2 | -------------------------------------------------------------------------------- /app/views/api/stories/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | @stories.each do |story| 3 | json.set! story.id do 4 | json.extract! story, :id, :title, :body, :author_id, :description 5 | json.date story.date.strftime("%B %d") 6 | json.image_url asset_path(story.image.url) 7 | json.author do 8 | json.username story.author.username 9 | json.image_url story.author.image.url 10 | json.followed_by_current_user current_user_followed?(story.author) 11 | json.id story.author.id 12 | end 13 | json.like_count story.likes.count 14 | json.liked_by_current_user current_user_liked?(story) 15 | json.like_id like_id(story) 16 | json.bookmark_id bookmark_id(story) 17 | json.bookmarked_by_current_user current_user_bookmarked?(story) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/views/api/stories/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.extract! @story, :id, :title, :body, :author_id, :description 3 | json.date @story.date.strftime("%B %d") 4 | json.image_url asset_path(@story.image.url) 5 | json.author do 6 | json.username @story.author.username 7 | json.image_url @story.author.image.url 8 | json.bio @story.author.bio 9 | json.followed_by_current_user current_user_followed?(@story.author) 10 | json.id @story.author.id 11 | end 12 | json.comments do 13 | @story.comments.each do |comment| 14 | json.partial! "/api/comments/comment", comment: comment 15 | end 16 | end 17 | json.like_count @story.likes.count 18 | json.liked_by_current_user current_user_liked?(@story) 19 | json.bookmarked_by_current_user current_user_bookmarked?(@story) 20 | json.like_id like_id(@story) 21 | json.bookmark_id bookmark_id(@story) 22 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.extract! user, :username, :bio, :id 3 | json.image_url user.image.url 4 | json.story_ids user.story_ids 5 | json.comment_ids user.comment_ids 6 | json.following_count user.followees.count #works 7 | json.followed_by_count user.followers.count #works 8 | json.followed_by_current_user current_user_followed?(user) 9 | json.follow_id follow_id(user) 10 | 11 | 12 | #custom route to togle like 13 | -------------------------------------------------------------------------------- /app/views/api/users/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | @users.each do |user| 3 | 4 | json.set! user.id do 5 | json.partial! "/api/users/user", user: user 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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 | Large 5 | <%= csrf_meta_tags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | 9 | <%= javascript_include_tag 'application' %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= yield %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 | 7 |
8 | -------------------------------------------------------------------------------- /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', __FILE__) 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 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Medium 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | config.paperclip_defaults = { 19 | :storage => :s3, 20 | :s3_credentials => { 21 | :bucket => ENV["s3_bucket"], 22 | :access_key_id => ENV["s3_access_key_id"], 23 | :secret_access_key => ENV["s3_secret_access_key"], 24 | :s3_region => ENV["s3_region"] 25 | } 26 | } 27 | 28 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 29 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 30 | # config.i18n.default_locale = :de 31 | 32 | # Do not swallow errors in after_commit/after_rollback callbacks. 33 | config.active_record.raise_in_transactional_callbacks = true 34 | config.generators do |g| 35 | g.test_framework :rspec, 36 | :fixtures => true, 37 | :view_specs => false, 38 | :helper_specs => false, 39 | :routing_specs => false, 40 | :controller_specs => true, 41 | :request_specs => false 42 | g.fixture_replacement :factory_girl, dir: 'spec/factories' 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 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: 5 23 | 24 | development: 25 | <<: *default 26 | database: Medium_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: Medium 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: Medium_test 61 | test: 62 | adapter: postgresql 63 | database: travis_ci_test 64 | 65 | # As with config/secrets.yml, you never want to store sensitive information, 66 | # like your database password, in your source code. If your source code is 67 | # ever seen by anyone, they now have access to your database. 68 | # 69 | # Instead, provide the password as a unix environment variable when you boot 70 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 71 | # for a full rundown on how to provide these environment variables in a 72 | # production deployment. 73 | # 74 | # On Heroku and other platform providers, you may have a full connection URL 75 | # available as an environment variable. For example: 76 | # 77 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 78 | # 79 | # You can use this database configuration with: 80 | # 81 | # production: 82 | # url: <%= ENV['DATABASE_URL'] %> 83 | # 84 | production: 85 | <<: *default 86 | database: Medium_production 87 | username: Medium 88 | password: <%= ENV['MEDIUM_DATABASE_PASSWORD'] %> 89 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 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 and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /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 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /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 static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 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/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /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/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_Medium_session' 4 | -------------------------------------------------------------------------------- /config/initializers/to_time_preserves_timezone.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Preserve the timezone of the receiver when calling to `to_time`. 4 | # Ruby 2.4 will change the behavior of `to_time` to preserve the timezone 5 | # when converting to an instance of `Time` instead of the previous behavior 6 | # of converting to the local system timezone. 7 | # 8 | # Rails 5.0 introduced this config option so that apps made with earlier 9 | # versions of Rails are not affected when upgrading. 10 | ActiveSupport.to_time_preserves_timezone = true 11 | -------------------------------------------------------------------------------- /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] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: 'static_pages#root' 3 | namespace :api, defaults: {format: :json} do 4 | resources :stories 5 | resources :users, only: [:create, :index, :show, :update] 6 | resources :comments 7 | resource :session, only: [:create, :destroy] 8 | post "users/:id/follow", to: "users#follow" 9 | post "users/:id/unfollow", to: "users#unfollow" 10 | post "stories/feed", to: "stories#feed" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: b478fe085341cb734a4dfd8ef55b8a988e940e7d530c41e36af9adf4012602fb90737186b3121d30ba6aa8514bdc8ea2357f37c8a68f36ce218fdc5d3c9d845e 15 | 16 | test: 17 | secret_key_base: 44682aed6878695532275f287e9f0f34b6f5360e2994a9f021830c309f039cd2bc977b1fb3cabb8189e4d05cf8b6263a0818515112f71bdc1f3279ca4bbacf38 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /db/migrate/20170620130854_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :password_digest, null: false 6 | t.string :session_token, null: false 7 | 8 | t.timestamps 9 | end 10 | add_index :users, :session_token, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170620133029_users.rb: -------------------------------------------------------------------------------- 1 | class Users < ActiveRecord::Migration 2 | def change 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /db/migrate/20170620133334_populate_users.rb: -------------------------------------------------------------------------------- 1 | class PopulateUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :username, :string, null: false 4 | add_column :users, :password_digest, :string, null: false 5 | add_column :users, :session_token, :string, null: false 6 | add_index :users, :session_token, unique: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20170621073510_add_attachment_image_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToUsers < ActiveRecord::Migration 2 | def self.up 3 | change_table :users do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :users, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170621153830_create_stories.rb: -------------------------------------------------------------------------------- 1 | class CreateStories < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :stories do |t| 4 | t.string :title, null: false 5 | t.string :body, null: false 6 | t.integer :author_id, null: false 7 | t.date :date, null: false 8 | t.timestamps null: false 9 | end 10 | add_index :stories, :author_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170621155254_add_attachment_image_to_stories.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToStories < ActiveRecord::Migration 2 | def self.up 3 | change_table :stories do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :stories, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170622202803_add_description_to_story.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToStory < ActiveRecord::Migration 2 | def change 3 | add_column :stories, :description, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170623181315_add_bio_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddBioToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :bio, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170627135328_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration 2 | def change 3 | create_table :comments do |t| 4 | t.string :body, null: false 5 | t.date :date, null: false 6 | t.integer :author_id, null: false 7 | t.integer :story_id, null: false 8 | t.timestamps null: false 9 | end 10 | add_index :comments, :author_id 11 | add_index :comments, :story_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170628183541_create_likes.rb: -------------------------------------------------------------------------------- 1 | 2 | class CreateLikes < ActiveRecord::Migration 3 | def change 4 | create_table :likes do |t| 5 | t.integer :likeable_id 6 | t.string :likeable_type 7 | t.timestamps 8 | end 9 | 10 | add_index :likes, [:likeable_type, :likeable_id] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170628184610_adduseridtolikes.rb: -------------------------------------------------------------------------------- 1 | class Adduseridtolikes < ActiveRecord::Migration 2 | def change 3 | add_column :likes, :user_id, :integer, null: false 4 | add_index :likes, :user_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170629150840_create_follows.rb: -------------------------------------------------------------------------------- 1 | class CreateFollows < ActiveRecord::Migration 2 | def change 3 | create_table :follows do |t| 4 | t.integer :follower_id, null: false 5 | t.integer :author_id, null: false 6 | t.timestamps null: false 7 | end 8 | add_index :follows, :follower_id 9 | add_index :follows, :author_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170711033755_create_bookmarks.rb: -------------------------------------------------------------------------------- 1 | class CreateBookmarks < ActiveRecord::Migration 2 | def change 3 | create_table :bookmarks do |t| 4 | t.integer :user_id, null: false 5 | t.integer :story_id, null: false 6 | t.timestamps null: false 7 | end 8 | add_index :bookmarks, :user_id 9 | add_index :bookmarks, :story_id 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints # 2 | 3 | ## HTML API ## 4 | 5 | ### Root ### 6 | 7 | - `GET /` - loads React web app 8 | 9 | ## JSON API ## 10 | 11 | ### Users ### 12 | 13 | - `POST /api/users` 14 | - `GET /api/users/:user_id` 15 | 16 | ### Session ### 17 | 18 | - `POST /api/session` 19 | - `DELETE /api/session` 20 | 21 | ### Stories ### 22 | 23 | - `GET /api/stories` 24 | - `POST /api/stories` 25 | - `GET /api/stories/:story_id` 26 | - `PATCH /api/stories/:story_id` 27 | - `DELETE /api/stories/:story_id` 28 | 29 | ### Comments ### 30 | 31 | - `GET /api/stories/:story_id/comments` 32 | - In comments controller index action, check for params.has_key?(:story_id) 33 | - In routes, next a comments :index action under stories 34 | - `POST /api/comments` 35 | - Create will need to get story_id via hidden input 36 | - `GET /api/comments/:comment_id` 37 | - `DELETE /api/comments/:comment_id` 38 | 39 | ### Likes ### 40 | 41 | - `POST /api/likes` 42 | - Will need to pass a user id and (either a story or comment id)?how via hidden input. 43 | - `DELETE /api/likes/:like_id` 44 | 45 | ### Follows ### 46 | - `POST /api/follows` 47 | - `Will need a follower_id and a followed_id, creates a instance in join table` 48 | - `DELETE /api/follows/:follow_id` 49 | 50 | ## Bonus ## 51 | 52 | ### Tags ### 53 | - `Tags can be associated with more than one story, stories can be associated with more than one tag. Join table for taggings required` 54 | - `GET /api/tags` 55 | - `GET /api/tags/tag_id/stories` 56 | - Gets all the stories with a given tag 57 | - In stories controller index action, check for params.has_key?(:tag_id) 58 | - In routes, next a stories :index action under tags 59 | - `GET /api/stories/:story_id/tags` 60 | - Gets all the tags with a given story 61 | - In tags controller index action, check for params.has_key?(:story_id) 62 | - In routes, next a tags :index action under stories 63 | - `DELETE /api/tags/:tag_id` 64 | 65 | ### Publications ### 66 | 67 | - `GET /api/publications` 68 | - `GET /api/publications/:publication_id` 69 | - `GET /api/publications/:publication_id/stories` 70 | - `GET /api/tags/tag_id/stories` 71 | - Gets all the stories in a publication 72 | - In stories controller index action, check for params.has_key?(:publication_id) 73 | - In routes, next a stories :index action under publications 74 | 75 | ### Bookmarks ### 76 | - Data Stored in a bookmarking table 77 | - `POST /api/bookmarks/` 78 | - `Will need a user_id and a story_id from hidden input` 79 | - `DELETE /api/bookmarks/:bookmark_id` 80 | ??? need assistance on the following, want to get all stories for user that are bookmarked 81 | - `GET /api/users/:user_id/bookmarks` 82 | - Gets all the bookmarks for a user 83 | 84 | ### Blocked User ### 85 | - `PATCH /api/users/:user_id` 86 | - `The blocked user will be in an updated blocked users array in params` 87 | 88 | ### Reply ### 89 | - `GET /api/replies/:reply_id` 90 | - `POST /api/replies/:reply_id` 91 | - `GET /api/comments/:comment_id/replies` 92 | - `DELETE /api/replies/:reply_id` 93 | 94 | ### Nested Replies ??? ### 95 | -------------------------------------------------------------------------------- /docs/component_hierarchy.md: -------------------------------------------------------------------------------- 1 | ## Components ## 2 | ### Auth ### 3 | - AuthForm 4 | ### Home ### 5 | - Header 6 | - StoryBlurbs 7 | ### Header ### 8 | - Auth 9 | ### StoryBlurbs ### 10 | - StoryBlurb 11 | ### StoryBlurb ### 12 | - AuthorBox 13 | ### StoryShow ### 14 | - AuthorBox 15 | - SocialMedia 16 | ### AuthorShow ### 17 | - StoryBlurbs 18 | ### EditStory ### 19 | - AuthorBox 20 | ### NewStory ### 21 | - AuthorBox 22 | ### CommentBox ### 23 | - NewComment 24 | - AuthorBox 25 | - Comment 26 | ### AuthorBox ### 27 | 28 | 29 | ## Routes 30 | Route | Method | Component | 31 | ------------|-----------|--| 32 | api/users | POST | Auth 33 | api/users/:user_id | GET | Auth / AuthorBox 34 | api/session | POST | Auth 35 | api/session | DELETE | Auth 36 | api/stories | GET | StoryBlurbs 37 | api/stories | POST | NewStory 38 | api/stories/:story_id | GET | StoryShow 39 | api/stories/:story_id | GET | EditStory 40 | api/stories/:story_id | DELETE | EditStory 41 | api/stories/:story_id/comments | GET | StoryShow 42 | api/comments | POST | StoryShow 43 | api/comments/:comment_id | GET | Comment 44 | api/comments/:comment_id | DELETE | Comment 45 | api/likes | POST | StoryShow, StoryBlurb 46 | api/likes | DELETE | StoryShow, StoryBlurb 47 | api/follows | POST | AuthorBox 48 | api/follows/:follow_id | DELETE | AuthorBox 49 | -------------------------------------------------------------------------------- /docs/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/images/edit.png -------------------------------------------------------------------------------- /docs/images/show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/images/show.png -------------------------------------------------------------------------------- /docs/production_timeline.md: -------------------------------------------------------------------------------- 1 | 2 | ## Production 3 | 4 | ### Phase 1: Backend setup and User Authentication (2 days) 5 | 6 | **Objective:** Functioning rails project with front-end Authentication 7 | 8 | ### Phase 2: Stories Model, API, and components (2 days) 9 | 10 | **Objective:** Stories can be created, read, edited and destroyed through 11 | the API and through user action. 12 | 13 | ### Phase 3: Commenting on Stories (1 days) 14 | 15 | **Objective:** Will implement create read edit destroy functionality for comments. 16 | 17 | ### Phase 4: Follows and Feed (2 day) 18 | 19 | **Objective:** Users will be followable, and authenticated users will see stories from followed users on thier homepage. 20 | 21 | ### Phase 5: Likes (1 day) 22 | 23 | **Objective:** Stories and comments will be likeable, with a toggleable like button. Like count will be displayed. 24 | 25 | ### Phase 6: - Styling (1 day) 26 | 27 | **Objective:** Final day should be mostly devoted to stylying. Any free time before that will be used to implement bonus features. 28 | 29 | ### Bonus Features (TBD) 30 | - Tags 31 | - Story Images 32 | - Publications 33 | - Replies 34 | - Bookmarks 35 | - Search 36 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ### { 2 | ### currentUser: { 3 | ###   id: 90, 4 | ###   username: "th3Nathan", 5 | ###   followed_users: [1, 2, 4, 5, 6] 6 | ### }, 7 | ### forms: { 8 | ###   signUp: {errors: []}, 9 | ###   logIn: {errors: []}, 10 | ###   createNote: {errors: ["body can't be blank"]} 11 | ### }, 12 | ### current_story: { 13 | ###   title: { 14 | ###    title: "The Best Foods", 15 | ###    body: "There are a lot of really good ones to choose from, but the best is...", 16 | ###    author_id: 4, 17 | ###    comments: [1, 4, 6], 18 | ###    likers: [2, 4, 5] 19 | ###    } 20 | ###   } 21 | ### }, 22 | ### stories: { 23 | ###   1: { 24 | ###    title: "The Best Foods", 25 | ###    body: "There are a lot of really good ones to choose from, but the best is...", 26 | ###    author_id: 4, 27 | ###    comments: [1, 4, 6], 28 | ###    likers: [2, 4, 5] 29 | ###    } 30 | ###   } 31 | ###   2: { 32 | ###    title: "Super story", 33 | ###    body: "Here is a super stories body..." 34 | ###    author_id: 9, 35 | ###    comments: [], 36 | ###    likers: [] 37 | ###   } 38 | ### }, 39 | ### comments: { 40 | ###   1: { 41 | ###    title: "Really?", 42 | ###    author_id: 1, 43 | ###    description: "... is not even a food" 44 | ###    }, 45 | ###   4: { 46 | ###    title: "Pacman", 47 | ###    author_id: 7, 48 | ###    description: "My name is Pacman and I beg do differ" 49 | ###    } 50 | ###   6: { 51 | ###    title: "I'm a ghost", 52 | ###    author_id: 4, 53 | ###    description: "BOO! my trap worked!" 54 | ###    } 55 | ###   } 56 | ### } 57 | 58 | A state like this could arise on a story show page, when other stories have either been fetched and cached or are being displayed in 59 | blurb form with the current story. 60 | -------------------------------------------------------------------------------- /docs/wireframes/Auth.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/Auth.JPG -------------------------------------------------------------------------------- /docs/wireframes/AuthorBox.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/AuthorBox.JPG -------------------------------------------------------------------------------- /docs/wireframes/AuthorShow.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/AuthorShow.JPG -------------------------------------------------------------------------------- /docs/wireframes/CommentBox.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/CommentBox.JPG -------------------------------------------------------------------------------- /docs/wireframes/EditStory_NewStory.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/EditStory_NewStory.JPG -------------------------------------------------------------------------------- /docs/wireframes/Header.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/Header.JPG -------------------------------------------------------------------------------- /docs/wireframes/Home.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/Home.JPG -------------------------------------------------------------------------------- /docs/wireframes/SocialMedia.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/SocialMedia.JPG -------------------------------------------------------------------------------- /docs/wireframes/StoryBlurb.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/StoryBlurb.JPG -------------------------------------------------------------------------------- /docs/wireframes/StoryBlurbs.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/StoryBlurbs.JPG -------------------------------------------------------------------------------- /docs/wireframes/StoryShow.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/docs/wireframes/StoryShow.JPG -------------------------------------------------------------------------------- /frontend/actions/comment_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/comment_util'; 2 | 3 | 4 | export const CREATE_COMMENT = 'CREATE_COMMENT'; 5 | export const DELETE_COMMENT = 'DELETE_COMMENT'; 6 | export const UPDATE_COMMENT = 'UPDATE_COMMENT'; 7 | export const RECEIVE_COMMENT = 'RECEIVE_COMMENT'; 8 | export const RECEIVE_COMMENTS = 'RECEIVE_COMMENTS'; 9 | 10 | 11 | 12 | export const addComment = comment => { 13 | return { 14 | type: CREATE_COMMENT, 15 | comment 16 | }; 17 | }; 18 | 19 | export const receiveComments = comments => { 20 | return { 21 | type: RECEIVE_COMMENTS, 22 | comments 23 | }; 24 | }; 25 | 26 | export const deleteComment = comment => { 27 | return { 28 | type: DELETE_COMMENT, 29 | comment 30 | }; 31 | }; 32 | 33 | export const update = comment => { 34 | return { 35 | type: UPDATE_COMMENT, 36 | comment 37 | }; 38 | }; 39 | 40 | export const receiveSingleComment = comment => { 41 | return { 42 | type: RECEIVE_COMMENT, 43 | comment 44 | }; 45 | }; 46 | 47 | export const updateCommentLikes = (newComment, id) => dispatch => { 48 | return APIUtil.updateCommentLikes(newComment, id) 49 | .then(comment => { 50 | return dispatch(update(comment)); 51 | }); 52 | }; 53 | 54 | export const fetchSingleComment = (id) => dispatch => { 55 | return APIUtil.fetchSingleComment(id) 56 | .then(comment => { 57 | return dispatch(receiveSingleComment(comment)); 58 | }); 59 | }; 60 | 61 | export const updateComment = (newComment, id) => dispatch => { 62 | return APIUtil.updateComment(newComment, id) 63 | .then(comment => { 64 | return dispatch(update(comment)); 65 | }); 66 | }; 67 | 68 | export const createComment = (comment) => dispatch => { 69 | return APIUtil.createComment(comment) 70 | .then(comment => {return dispatch(addComment(comment)); 71 | }); 72 | }; 73 | 74 | export const destroyComment = id => dispatch => { 75 | return APIUtil.removeComment(id).then((comment) => { 76 | return dispatch(deleteComment(comment)); 77 | }); 78 | }; 79 | -------------------------------------------------------------------------------- /frontend/actions/presentational_actions.js: -------------------------------------------------------------------------------- 1 | export const TURN_OFF_MODAL_ANIMATION = "TURN_OFF_MODAL_ANIMATION"; 2 | export const TURN_ON_MODAL_ANIMATION = "TURN_ON_MODAL_ANIMATION"; 3 | 4 | export const turnOffModalAnimation = () => { 5 | return { 6 | type: TURN_OFF_MODAL_ANIMATION, 7 | }; 8 | }; 9 | 10 | export const turnOnModalAnimation = () => { 11 | return { 12 | type: TURN_ON_MODAL_ANIMATION, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | 4 | export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER'; 5 | export const RECEIVE_ERRORS = 'RECEIVE_ERRORS'; 6 | export const SCRUB_ERRORS = 'SCRUB_ERRORS'; 7 | export const UPDATE_DRAFT = 'UPDATE_DRAFT'; 8 | export const REMOVE_DRAFT = 'REMOVE_DRAFT'; 9 | export const UPDATE_SHOWED_USER = "UPDATE_USER"; 10 | 11 | export const updateDraft = newDraft => { 12 | return { 13 | type: UPDATE_DRAFT, 14 | newDraft 15 | }; 16 | }; 17 | 18 | export const removeDraft = () => { 19 | return { 20 | type: REMOVE_DRAFT 21 | }; 22 | }; 23 | 24 | export const receiveCurrentUser = currentUser => { 25 | return { 26 | type: RECEIVE_CURRENT_USER, 27 | currentUser 28 | }; 29 | }; 30 | 31 | export const receiveErrors = errors => { 32 | return { 33 | type: RECEIVE_ERRORS, 34 | errors 35 | }; 36 | }; 37 | 38 | export const scrubErrors = () => { 39 | return { 40 | type: SCRUB_ERRORS 41 | }; 42 | }; 43 | 44 | //Thunk actions 45 | 46 | export const updateUser = (user, id) => dispatch => { 47 | return APIUtil.updateUser(user, id) 48 | .then(user => { 49 | return dispatch(receiveCurrentUser(user)); 50 | }); 51 | }; 52 | 53 | export const refresh = (id) => dispatch => { 54 | return APIUtil.fetchSingleUser(id) 55 | .then(user => { 56 | return dispatch(receiveCurrentUser(user)); 57 | }); 58 | }; 59 | 60 | export const signIn = user => dispatch => { 61 | return APIUtil.signIn(user) 62 | .then(user => { 63 | return dispatch(receiveCurrentUser(user)); 64 | }, err => dispatch(receiveErrors(err.responseJSON))); 65 | }; 66 | 67 | export const signUp = user => dispatch => { 68 | return APIUtil.signUp(user) 69 | .then(user => { 70 | return dispatch(receiveCurrentUser(user)); 71 | }, err => dispatch(receiveErrors(err.responseJSON))); 72 | }; 73 | 74 | export const logOut = () => dispatch => { 75 | return APIUtil.logOut() 76 | .then(() => { 77 | return dispatch(receiveCurrentUser(null)); 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /frontend/actions/story_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/story_util'; 2 | 3 | export const RECEIVE_STORIES = 'RECEIVE_STORIES'; 4 | export const RECEIVE_SINGLE_STORY = 'RECEIVE_SINGLE_STORY'; 5 | export const UPDATE_STORY = 'UPDATE_STORY'; 6 | export const DELETE_STORY = 'DELETE_STORY'; 7 | export const CREATE_STORY = 'CREATE_STORY'; 8 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS"; 9 | export const RECEIVE_FEED = "RECEIVE_FEED"; 10 | export const RECEIVE_SINGLE_FEED = "RECEIVE_SINGLE_FEED"; 11 | 12 | export const receiveStories = (stories) => { 13 | return { 14 | type: RECEIVE_STORIES, 15 | stories 16 | }; 17 | }; 18 | 19 | export const receiveFeed = stories => { 20 | return { 21 | type: RECEIVE_FEED, 22 | stories 23 | }; 24 | }; 25 | 26 | export const receiveSingleStory = story => { 27 | return { 28 | type: RECEIVE_SINGLE_STORY, 29 | story 30 | }; 31 | }; 32 | 33 | export const receiveSingleFeed = story => { 34 | return { 35 | type: RECEIVE_SINGLE_FEED, 36 | story 37 | }; 38 | }; 39 | 40 | export const receiveErrors = errors => { 41 | return { 42 | type: RECEIVE_ERRORS, 43 | errors 44 | }; 45 | }; 46 | 47 | export const addStory = story => { 48 | return { 49 | type: CREATE_STORY, 50 | story 51 | }; 52 | }; 53 | 54 | export const deleteStory = id => { 55 | return { 56 | type: DELETE_STORY, 57 | id 58 | }; 59 | }; 60 | 61 | export const update = story => { 62 | return { 63 | type: UPDATE_STORY, 64 | story 65 | }; 66 | }; 67 | 68 | export const fetchStories = () => dispatch => { 69 | return APIUtil.fetchStories() 70 | .then(stories => { 71 | return dispatch(receiveStories(stories)); 72 | }); 73 | }; 74 | 75 | export const fetchSingleStory = (id) => dispatch => { 76 | return APIUtil.fetchSingleStory(id) 77 | .then(story => { 78 | return dispatch(receiveSingleStory(story)); 79 | }); 80 | }; 81 | 82 | export const fetchSingleFeed = (id) => dispatch => { 83 | return APIUtil.fetchSingleStory(id) 84 | .then(story => { 85 | return dispatch(receiveSingleFeed(story)); 86 | }); 87 | }; 88 | 89 | export const updateStory = (newStory, id) => dispatch => { 90 | return APIUtil.updateStory(newStory, id) 91 | .then(story => { 92 | return dispatch(update(story)); 93 | }); 94 | }; 95 | 96 | export const updateStoryLikes = (newStory, id) => dispatch => { 97 | return APIUtil.updateStoryLikes(newStory, id) 98 | .then(story => { 99 | return dispatch(update(story)); 100 | }); 101 | }; 102 | 103 | export const updateStoryBookmarks = (newStory, id) => dispatch => { 104 | return APIUtil.updateStoryBookmarks(newStory, id) 105 | .then(story => { 106 | return dispatch(update(story)); 107 | }); 108 | }; 109 | 110 | export const createStory = (story) => dispatch => { 111 | return APIUtil.createStory(story) 112 | .then(story => {return dispatch(addStory(story)); 113 | }); 114 | }; 115 | 116 | export const destroyStory = id => dispatch => { 117 | return APIUtil.removeStory(id).then((story) => { 118 | return dispatch(deleteStory(id)); 119 | }); 120 | }; 121 | 122 | export const feed = () => dispatch => { 123 | return APIUtil.feed().then((stories) => { 124 | return dispatch(receiveFeed(stories)); 125 | }); 126 | }; 127 | 128 | export const bookmarked = () => dispatch => { 129 | return APIUtil.bookmarked().then((stories) => { 130 | return dispatch(receiveBookmarked(stories)); 131 | }); 132 | }; 133 | -------------------------------------------------------------------------------- /frontend/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util'; 2 | 3 | export const RECEIVE_USERS = 'RECEIVE_USERS'; 4 | export const RECEIVE_SINGLE_USER = 'RECEIVE_SINGLE_USER'; 5 | export const UPDATE_USER = 'UPDATE_USER'; 6 | import { receiveCurrentUser } from './session_actions'; 7 | 8 | export const receiveUsers = users => { 9 | return { 10 | type: RECEIVE_USERS, 11 | users 12 | }; 13 | }; 14 | 15 | export const receiveSingleUser = user => { 16 | return { 17 | type: RECEIVE_SINGLE_USER, 18 | user 19 | }; 20 | }; 21 | 22 | //receive follows takes follower user and author user 23 | // receive follows goes in both user and session reducer 24 | 25 | export const updateUserFollows = (userAttributes, id) => dispatch => { 26 | return APIUtil.updateUserFollows(userAttributes, id) 27 | .then(user => { 28 | return dispatch(receiveSingleUser(user)); 29 | }); 30 | }; 31 | 32 | export const fetchUsers = () => dispatch => { 33 | return APIUtil.fetchUsers() 34 | .then(users => { 35 | return dispatch(receiveUsers(users)); 36 | }); 37 | }; 38 | 39 | export const fetchSingleUser = (id) => dispatch => { 40 | return APIUtil.fetchSingleUser(id) 41 | .then(user => { 42 | return dispatch(receiveSingleUser(user)); 43 | }); 44 | }; 45 | 46 | export const follow = (id) => dispatch => { 47 | return APIUtil.follow(id) 48 | .then(user => { 49 | return dispatch(receiveSingleUser(user)); 50 | }); 51 | }; 52 | 53 | export const unFollow = (id) => dispatch => { 54 | return APIUtil.unFollow(id) 55 | .then(user => { 56 | return dispatch(receiveSingleUser(user)); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /frontend/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch, Link } from 'react-router-dom'; 3 | import { AuthRoute, ProtectedRoute } from '../util/route_util'; 4 | import Header from './header'; 5 | import GreetingContainer from './greeting/greeting_container'; 6 | import SessionFormContainer from './session/session_form_container'; 7 | import UsersContainer from './users/users_container.jsx'; 8 | import StoriesIndexContainer from './story/stories_index_container'; 9 | import StoriesShowContainer from './story/stories_show_container'; 10 | import NewStory from './story/new_story_container'; 11 | import EditStory from './story/edit_story_container'; 12 | import UserShow from './users/user_show_container'; 13 | import CommentShow from './comment/comment_show_container'; 14 | import FeedContainer from './story/feed_index_container'; 15 | import Footer from './footer'; 16 | 17 | const App = () => { 18 | return ( 19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
34 | ); 35 | }; 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /frontend/components/comment/comment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthorBox from './comment_author_box'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | class Comment extends React.Component { 6 | constructor(props){ 7 | super(props); 8 | this.routeToShow = this.routeToShow.bind(this); 9 | this.addLike = this.addLike.bind(this); 10 | this.removeLike = this.removeLike.bind(this); 11 | } 12 | 13 | addLike(e){ 14 | e.preventDefault(); 15 | e.stopPropagation(); 16 | const newAttributes = [{user_id: this.props.userId, likeable_id: this.props.comment.id, likeable_type: "Comment"}]; 17 | this.props.updateCommentLikes(newAttributes, this.props.comment.id) 18 | .then(() => this.props.fetchSingleComment(this.props.comment.id)); 19 | } 20 | 21 | removeLike(e){ 22 | e.preventDefault(); 23 | e.stopPropagation(); 24 | const newAttributes = [{id: this.props.comment.like_id, _destroy: true}]; 25 | this.props.updateCommentLikes(newAttributes, this.props.comment.id) 26 | .then(() => this.props.fetchSingleComment(this.props.comment.id)); 27 | } 28 | routeToShow(e){ 29 | e.preventDefault(); 30 | if (e.target.className.slice(0, 10) === "author-box") 31 | return; 32 | this.props.history.push(`/comments/${this.props.comment.id}`); 33 | } 34 | render(){ 35 | 36 | 37 | let heart; 38 | if (this.props.comment.liked_by_current_user){ 39 | heart = ( 40 |
41 | 42 | 43 | 44 | 45 |
46 | ); 47 | } else { 48 | heart = ( 49 |
50 | 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | 58 | 59 | return( 60 |
61 | 66 |
67 |

{this.props.comment.body}

68 |
69 |
70 | { heart } 71 | 72 | {`${this.props.comment.like_count}`} 73 | 74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | } 81 | // 82 | // 83 | // 84 | // 85 | // 86 | // 87 | // 88 | // 89 | 90 | export default withRouter(Comment); 91 | -------------------------------------------------------------------------------- /frontend/components/comment/comment_author_box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class AuthorBox extends React.Component { 5 | 6 | render (){ 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 |

{this.props.author.username}

15 | 16 |
17 |

{this.props.date}

18 |
19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default AuthorBox; 26 | -------------------------------------------------------------------------------- /frontend/components/comment/comment_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthorBox from './comment_author_box'; 3 | import Comment from './comment'; 4 | import NewComment from './new_comment'; 5 | 6 | class CommentList extends React.Component { 7 | render(){ 8 | const comments = this.props.comments.map(comment => { 9 | return ( 10 | 17 | ); 18 | }); 19 | 20 | 21 | return( 22 |
23 | < NewComment 24 | createComment={this.props.createComment} 25 | currentUser={this.props.currentUser} 26 | storyId={parseInt(this.props.match.params.story_id)} 27 | /> 28 | { comments } 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default CommentList; 35 | -------------------------------------------------------------------------------- /frontend/components/comment/comment_list_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import CommentList from './comment_list'; 4 | import { 5 | createComment, 6 | updateComment, 7 | updateCommentLikes, 8 | fetchSingleComment 9 | } from '../../actions/comment_actions'; 10 | 11 | const mapStateToProps = (state, ownProps) => { 12 | 13 | return { 14 | currentUser: state.session.currentUser, 15 | comments: Object.keys(state.comments).map(k => state.comments[k]) 16 | .reverse(), 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = dispatch => { 21 | return { 22 | createComment: (comment) => dispatch(createComment(comment)), 23 | updateComment: (comment, id) => dispatch(updateComment(comment, id)), 24 | updateCommentLikes: (comment, id) => dispatch(updateCommentLikes(comment, id)), 25 | fetchSingleComment: (id) => dispatch(fetchSingleComment(id)) 26 | }; 27 | }; 28 | 29 | export default withRouter( 30 | connect(mapStateToProps, mapDispatchToProps)(CommentList) 31 | ); 32 | -------------------------------------------------------------------------------- /frontend/components/comment/comment_show_author_box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class CommentShowAuthorBox extends React.Component { 5 | 6 | readTime(){ 7 | if (!this.props.comment) return null; 8 | return `${Math.floor(this.props.comment.body.length / 1375)} minute read`; 9 | } 10 | 11 | truncateDescription(){ 12 | if (!this.props.comment.author.bio) return null; 13 | if (this.props.comment.author.bio.length > 60){ 14 | return this.props.comment.author.bio.slice(0, 57) + "..."; 15 | } 16 | else { 17 | return this.props.comment.author.bio; 18 | } 19 | } 20 | 21 | 22 | render (){ 23 | let followComponent; 24 | if (this.props.followPresent){ 25 | followComponent = Follow; 26 | } 27 | 28 | return ( 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 |

{this.props.comment.author.username} 38 |

39 | 40 | { followComponent } 41 |
{this.truncateDescription()}
42 |
43 |

{this.props.comment.date}

44 | . 45 |

{this.readTime()}

46 |
47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default CommentShowAuthorBox; 54 | -------------------------------------------------------------------------------- /frontend/components/comment/comment_show_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { withRouter } from 'react-router-dom'; 3 | import CommentShow from './comment_show'; 4 | import { 5 | fetchSingleComment, 6 | destroyComment, 7 | updateComment, 8 | updateCommentLikes 9 | } from '../../actions/comment_actions'; 10 | 11 | const mapStateToProps = (state, ownProps) => { 12 | const commentId = parseInt(ownProps.match.params.comment_id); 13 | return { 14 | isLoggedUser: state.session.currentUser.comment_ids.includes(commentId), 15 | currentUser: state.session.currentUser, 16 | comment: state.comments[commentId], 17 | }; 18 | }; 19 | 20 | 21 | const mapDispatchToProps = (dispatch, ownProps) => { 22 | const commentId = ownProps.match.params.comment_id; 23 | return { 24 | fetchSingleComment: (id) => dispatch(fetchSingleComment(id)), 25 | destroyComment: (id) => dispatch(destroyComment(id)), 26 | updateComment: (comment, id) => dispatch(updateComment(comment, id)), 27 | updateCommentLikes: (comment, id) => dispatch(updateCommentLikes(comment, id)), 28 | }; 29 | }; 30 | 31 | export default withRouter( 32 | connect(mapStateToProps, mapDispatchToProps)(CommentShow) 33 | ); 34 | -------------------------------------------------------------------------------- /frontend/components/comment/new_comment.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class NewComment extends React.Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = { 7 | body: "", 8 | focused: false, 9 | }; 10 | this.handleChange = this.handleChange.bind(this); 11 | this.handleFirstFocus = this.handleFirstFocus.bind(this); 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | } 14 | 15 | handleChange(e){ 16 | e.preventDefault(); 17 | this.setState({body: e.target.value}); 18 | } 19 | 20 | handleFirstFocus(e){ 21 | e.preventDefault(); 22 | this.setState({focused: true}); 23 | } 24 | 25 | handleSubmit(e){ 26 | e.preventDefault(); 27 | let comment = {}; 28 | comment.body = this.state.body; 29 | comment.story_id = this.props.storyId; 30 | comment.author_id = this.props.currentUser.id; 31 | this.setState( 32 | { 33 | focused: false, 34 | body: "" 35 | }); 36 | this.props.createComment(comment); 37 | } 38 | 39 | render(){ 40 | let submitButton; 41 | if (this.state.focused) { 42 | submitButton = ( 43 | 49 | ); 50 | } 51 | return( 52 |
53 | 62 |
63 | { submitButton } 64 |
65 | ); 66 | } 67 | } 68 | 69 | export default NewComment; 70 | -------------------------------------------------------------------------------- /frontend/components/errors_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ErrorsList = ({errors}) => { 4 | return ( 5 | 8 | ); 9 | }; 10 | 11 | export default ErrorsList; 12 | -------------------------------------------------------------------------------- /frontend/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class Footer extends React.Component { 5 | 6 | 7 | render(){ 8 | return ( 9 | 14 | ); 15 | } 16 | } 17 | 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /frontend/components/greeting/greeting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class Greeting extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | this.toProfile = this.toProfile.bind(this); 8 | } 9 | 10 | toProfile(e){ 11 | e.preventDefault(); 12 | this.props.history.push(`/users/${this.props.currentUser.id}`); 13 | } 14 | 15 | render(){ 16 | if (this.props.currentUser){ 17 | let thumbnail = (); 18 | return ( 19 |
20 |

Hello, {this.props.currentUser.username}!

21 | {thumbnail} 22 | 23 |
24 | ); 25 | } else { 26 | return ( 27 |
28 | 29 | Sign Up 30 | 31 |  /  32 | 33 | Sign In 34 | 35 |
36 | ); 37 | } 38 | } 39 | } 40 | 41 | export default Greeting; 42 | -------------------------------------------------------------------------------- /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 | import { withRouter } from 'react-router-dom'; 5 | import { turnOffModalAnimation, turnOnModalAnimation } from '../../actions/presentational_actions'; 6 | 7 | const mapStateToProps = ({ session }) => { 8 | return { 9 | currentUser: session.currentUser 10 | }; 11 | }; 12 | 13 | 14 | const mapDispatchToProps = (dispatch, ownProps) => { 15 | return { 16 | logOut: () => dispatch(logOut()) 17 | .then(() => turnOnModalAnimation()) 18 | .then(() => ownProps.history.push('/signin')), 19 | turnOnModalAnimation: () => dispatch(turnOnModalAnimation()) 20 | }; 21 | }; 22 | 23 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Greeting)); 24 | -------------------------------------------------------------------------------- /frontend/components/new_story_header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const NewStoryDropdown = (props) => { 5 | return ( 6 | 10 | ); 11 | }; 12 | 13 | class NewStoryHeader extends React.Component { 14 | constructor(props){ 15 | super(props); 16 | this.state = { 17 | showDropdown: false 18 | }; 19 | 20 | this.toggleDropdown = this.toggleDropdown.bind(this); 21 | } 22 | 23 | toggleDropdown(e){ 24 | this.setState({ 25 | showDropdown: this.state.showDropdown ? false : true 26 | }); 27 | } 28 | 29 | render(){ 30 | return ( 31 |
32 |
Share
33 |
34 |
Publish
35 | 36 |
37 | 38 | 39 | {this.state.showDropdown ? 40 | : null 41 | } 42 |
43 | ); 44 | } 45 | } 46 | 47 | export default NewStoryHeader; 48 | -------------------------------------------------------------------------------- /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/save_header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const NewStoryDropdown = (props) => { 5 | return ( 6 | 10 | ); 11 | }; 12 | 13 | class SaveHeader extends React.Component { 14 | constructor(props){ 15 | super(props); 16 | this.state = { 17 | showDropdown: false 18 | }; 19 | 20 | this.toggleDropdown = this.toggleDropdown.bind(this); 21 | } 22 | 23 | toggleDropdown(e){ 24 | this.setState({ 25 | showDropdown: this.state.showDropdown ? false : true 26 | }); 27 | } 28 | 29 | render(){ 30 | return ( 31 |
32 |
33 |
Save Changes
34 | 35 |
36 | 37 | 38 | {this.state.showDropdown ? 39 | : null 40 | } 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default SaveHeader; 47 | -------------------------------------------------------------------------------- /frontend/components/session/session_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { signUp, signIn, scrubErrors } from '../../actions/session_actions'; 3 | import { turnOffModalAnimation, turnOnModalAnimation } from '../../actions/presentational_actions'; 4 | 5 | import SessionForm from './session_form'; 6 | import { withRouter } from 'react-router-dom'; 7 | 8 | const mapStateToProps = (state, ownProps) => { 9 | return { 10 | loggedIn: !!state.session.currentUser, 11 | errors: state.session.errors, 12 | formType: ownProps.location.pathname.slice(1), 13 | modalAnimation: state.presentation.modalAnimation 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch, ownProps) => { 18 | let action; 19 | const processForm = (ownProps.location.pathname.slice(1) === 'signin') 20 | ? signIn : signUp; 21 | 22 | return { 23 | processForm: user => dispatch(processForm(user)), 24 | signIn: user => dispatch(signIn(user)), 25 | scrubErrors: () => dispatch(scrubErrors()), 26 | turnOnModalAnimation: () => dispatch(turnOnModalAnimation()), 27 | turnOffModalAnimation: () => dispatch(turnOffModalAnimation()) 28 | }; 29 | }; 30 | 31 | export default withRouter( 32 | connect(mapStateToProps, mapDispatchToProps)(SessionForm) 33 | ); 34 | -------------------------------------------------------------------------------- /frontend/components/story/author_box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class AuthorBox extends React.Component { 5 | 6 | constructor(props){ 7 | super(props); 8 | this.addBookmark = this.addBookmark.bind(this); 9 | this.removeBookmark = this.removeBookmark.bind(this); 10 | } 11 | 12 | readTime(){ 13 | return `${Math.floor(this.props.story.body.length / 1075)} minutes`; 14 | } 15 | 16 | addBookmark(e){ 17 | e.preventDefault(); 18 | const newAttributes = [{user_id: this.props.user_id, story_id: this.props.story.id}]; 19 | this.props.updateStoryBookmarks(newAttributes, this.props.story.id) 20 | .then(() => this.props.fetchSingleStory(this.props.story.id)); 21 | } 22 | 23 | removeBookmark(e){ 24 | e.preventDefault(); 25 | const newAttributes = [{id: this.props.story.bookmark_id, _destroy: true}]; 26 | this.props.updateStoryBookmarks(newAttributes, this.props.story.id) 27 | .then(() => this.props.fetchSingleStory(this.props.story.id)); 28 | } 29 | 30 | 31 | render (){ 32 | if (!this.props.story) return null; 33 | 34 | let bookmark; 35 | if (this.props.story.bookmarked_by_current_user){ 36 | bookmark = ( 37 | 38 | 39 | 40 | ); 41 | } else { 42 | bookmark = ( 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 |
51 |
52 | 53 |
54 |
55 | 56 |

{this.props.story.author.username}

57 | 58 |
59 |

{this.props.story.date}

60 | . 61 |

{this.readTime()}

62 |
63 |
64 |
65 | {bookmark} 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | // 73 | // 74 | // 75 | // 76 | export default AuthorBox; 77 | -------------------------------------------------------------------------------- /frontend/components/story/blurb.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthorBox from './author_box'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | class Blurb extends React.Component { 6 | readTime(){ 7 | return `${Math.floor(this.props.story.body.length / 1375)} minutes`; 8 | } 9 | 10 | render (){ 11 | const backgroundImage = {"backgroundImage": `url(${this.props.story.image_url})`}; 12 | return ( 13 |
14 |
15 |
16 | 17 |
18 |

{this.props.story.title}

19 |

{this.props.story.description}

20 |
21 | 22 | 29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | export default Blurb; 36 | -------------------------------------------------------------------------------- /frontend/components/story/edit_story_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addStory, updateStory, fetchSingleStory } from '../../actions/story_actions'; 3 | import { withRouter } from 'react-router-dom'; 4 | import StoryForm from './story_form'; 5 | import { updateDraft } from '../../actions/session_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | currentUser: state.session.currentUser, 10 | draft: state.session.draft, 11 | formType: "edit", 12 | storyId: parseInt(ownProps.match.params.story_id), 13 | 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = dispatch => { 18 | return { 19 | updateStory: (story, id) => dispatch(updateStory(story, id)), 20 | updateDraft: (draft) => dispatch(updateDraft(draft)), 21 | fetchSingleStory: (id) => dispatch(fetchSingleStory(id)), 22 | }; 23 | }; 24 | 25 | export default withRouter( 26 | connect(mapStateToProps, mapDispatchToProps)(StoryForm) 27 | ); 28 | -------------------------------------------------------------------------------- /frontend/components/story/feed_index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StoryBlurb from "./blurb"; 3 | import {Link} from "react-router-dom"; 4 | 5 | class FeedIndex extends React.Component { 6 | componentDidMount(){ 7 | this.props.feed(); 8 | } 9 | 10 | render(){ 11 | if (!this.props.stories) return null; 12 | const stories = this.props.stories.map(story => { 13 | return ( 14 | 19 | ); 20 | }); 21 | return ( 22 |
23 | To Featured Stories 24 |
25 | {stories} 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | export default FeedIndex; 33 | -------------------------------------------------------------------------------- /frontend/components/story/feed_index_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { feed, updateStoryBookmarks, fetchSingleFeed } from '../../actions/story_actions'; 3 | import { storiesSelector } from "../../reducers/selectors"; 4 | import { withRouter } from 'react-router-dom'; 5 | import FeedIndex from './feed_index'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | stories: storiesSelector(state.stories.feed), 10 | currentUser: state.session.currentUser, 11 | user_id: state.session.currentUser.id 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = dispatch => { 16 | return { 17 | feed: () => dispatch(feed()), 18 | updateStoryBookmarks: (story, id) => dispatch (updateStoryBookmarks(story, id)), 19 | fetchSingleStory: (id) => dispatch (fetchSingleFeed(id)) 20 | }; 21 | }; 22 | 23 | export default withRouter( 24 | connect(mapStateToProps, mapDispatchToProps)(FeedIndex) 25 | ); 26 | -------------------------------------------------------------------------------- /frontend/components/story/new_author_box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class NewAuthorBox extends React.Component { 5 | 6 | truncateDescription(){ 7 | if (!this.props.currentUser.bio) return ""; 8 | if (this.props.currentUser.bio.length > 100){ 9 | return this.props.currentUser.bio.slice(0, 97) + "..."; 10 | } 11 | else { 12 | return this.props.currentUser.bio; 13 | } 14 | } 15 | render (){ 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 |

{this.props.currentUser.username} 26 |

27 | 28 |
{this.truncateDescription()}
29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | export default NewAuthorBox; 36 | -------------------------------------------------------------------------------- /frontend/components/story/new_story_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { addStory } from '../../actions/story_actions'; 3 | import { withRouter } from 'react-router-dom'; 4 | import StoryForm from './story_form'; 5 | import { updateDraft } from '../../actions/session_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | currentUser: state.session.currentUser, 10 | draft: state.session.draft, 11 | formType: "new" 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = dispatch => { 16 | return { 17 | addStory: () => dispatch(addStory()), 18 | updateDraft: (draft) => dispatch(updateDraft(draft)), 19 | }; 20 | }; 21 | 22 | export default withRouter( 23 | connect(mapStateToProps, mapDispatchToProps)(StoryForm) 24 | ); 25 | -------------------------------------------------------------------------------- /frontend/components/story/stories_index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import StoryBlurb from "./blurb"; 3 | import {Link} from "react-router-dom"; 4 | 5 | class StoriesIndex extends React.Component { 6 | componentDidMount(){ 7 | if (this.props.location.pathname === "/stories" || this.props.location.pathname === "/") 8 | this.props.fetchStories(); 9 | } 10 | 11 | render(){ 12 | const stories = this.props.stories.map(story => { 13 | return ( 14 | 19 | ); 20 | }); 21 | return ( 22 |
23 | To Feed 24 |
25 | {stories} 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | export default StoriesIndex; 33 | -------------------------------------------------------------------------------- /frontend/components/story/stories_index_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchStories, updateStoryBookmarks, fetchSingleStory } from '../../actions/story_actions'; 3 | import { storiesSelector } from "../../reducers/selectors"; 4 | import { withRouter } from 'react-router-dom'; 5 | import StoriesIndex from './stories_index'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | stories: storiesSelector(state.stories.all), 10 | user_id: state.session.currentUser.id 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = dispatch => { 15 | return { 16 | fetchStories: user => dispatch(fetchStories()), 17 | updateStoryBookmarks: (story, id) => dispatch (updateStoryBookmarks(story, id)), 18 | fetchSingleStory: (id) => dispatch (fetchSingleStory(id)) 19 | }; 20 | }; 21 | 22 | export default withRouter( 23 | connect(mapStateToProps, mapDispatchToProps)(StoriesIndex) 24 | ); 25 | -------------------------------------------------------------------------------- /frontend/components/story/stories_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import StoryAuthorBox from './story_author_box'; 4 | import ShareBar from './share_bar'; 5 | import { StickyContainer, Sticky } from 'react-sticky'; 6 | import Comments from '../comment/comment_list_container'; 7 | 8 | class StoriesShow extends React.Component { 9 | constructor(props){ 10 | super(props); 11 | } 12 | 13 | componentDidMount(){ 14 | this.props.fetchSingleStory(this.props.match.params.story_id); 15 | } 16 | 17 | 18 | render(){ 19 | if (!this.props.story) return null; 20 | const createMarkup = () => { 21 | return {__html: `${this.props.story.body}`}; 22 | }; 23 | 24 | const backgroundImage = {"backgroundImage": `url(${this.props.story.image_url})`}; 25 | return( 26 | 27 | 28 | 29 | { 30 | ({ style, isSticky, wasSticky, distanceFromTop, distanceFromBottom, calculatedHeight }) => { 31 | if (distanceFromTop < 10) 32 | return ( 33 |
34 | 41 |
42 | ); 43 | else 44 | return
; 45 | } 46 | } 47 |
48 |
49 | this.props.fetchSingleStory(this.props.match.params.story_id)} 58 | /> 59 | 60 | 61 |

{this.props.story.title}

62 | 63 |
64 | 65 |
66 | 67 | 68 |
69 | 70 | 74 |
75 |
76 | ); 77 | } 78 | 79 | } 80 | 81 | export default StoriesShow; 82 | -------------------------------------------------------------------------------- /frontend/components/story/stories_show_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchSingleStory, updateStoryLikes, updateStoryBookmarks } from '../../actions/story_actions'; 3 | import { refresh } from '../../actions/session_actions'; 4 | import { storiesSelector } from "../../reducers/selectors"; 5 | import { withRouter } from 'react-router-dom'; 6 | import StoriesShow from './stories_show'; 7 | import { follow, unFollow } from './../../actions/user_actions'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | story: state.stories.all[ownProps.match.params.story_id], 12 | currentUser: state.session.currentUser, 13 | comments: Object.keys(state.comments).map(k => state.comments[k]), 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = dispatch => { 18 | return { 19 | updateStoryLikes: (story, id) => dispatch (updateStoryLikes(story, id)), 20 | updateStoryBookmarks: (story, id) => dispatch (updateStoryBookmarks(story, id)), 21 | fetchSingleStory: (id) => dispatch(fetchSingleStory(id)), 22 | follow: (id) => dispatch(follow(id)), 23 | unFollow: (id) => dispatch(unFollow(id)), 24 | }; 25 | }; 26 | 27 | export default withRouter( 28 | connect(mapStateToProps, mapDispatchToProps)(StoriesShow) 29 | ); 30 | -------------------------------------------------------------------------------- /frontend/components/story/story_author_box.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | class StoryAuthorBox extends React.Component { 5 | 6 | constructor(props){ 7 | super(props); 8 | this.follow = this.follow.bind(this); 9 | this.unFollow = this.unFollow.bind(this); 10 | } 11 | 12 | readTime(){ 13 | return `${Math.floor(this.props.story.length / 1375)} minute read`; 14 | } 15 | 16 | truncateDescription(){ 17 | if (!this.props.author.bio) return null; 18 | if (this.props.author.bio.length > 67){ 19 | return this.props.author.bio.slice(0, 67) + "..."; 20 | } 21 | else { 22 | return this.props.author.bio; 23 | } 24 | } 25 | 26 | follow(e){ 27 | e.preventDefault(); 28 | this.props.follow(this.props.author_id).then(() => this.props.refetch()); 29 | } 30 | 31 | unFollow(e){ 32 | e.preventDefault(); 33 | this.props.unFollow(this.props.author_id).then(() => this.props.refetch()); 34 | } 35 | 36 | render (){ 37 | 38 | let followButton; 39 | if (this.props.author_id !== this.props.currentUser.id){ 40 | if (this.props.author.followed_by_current_user){ 41 | followButton = Unfollow; 42 | } 43 | else { 44 | 45 | followButton = Follow; 46 | } 47 | } 48 | 49 | return ( 50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 | 58 |

{this.props.author.username} 59 |

60 | 61 | { followButton } 62 |
{this.truncateDescription()}
63 |
64 |

{this.props.date}

65 | . 66 |

{this.readTime()}

67 |
68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | export default StoryAuthorBox; 75 | -------------------------------------------------------------------------------- /frontend/components/users/user_show_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logOut } from '../../actions/session_actions'; 3 | import { withRouter } from 'react-router-dom'; 4 | import UserShow from './user_show'; 5 | import { fetchSingleUser, follow, unFollow } from '../../actions/user_actions'; 6 | import { updateUser, refresh } from '../../actions/session_actions'; 7 | import { fetchStories, fetchSingleStory, updateStoryBookmarks } from '../../actions/story_actions'; 8 | 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | const storiesArray = Object.keys(state.stories.all).map(key => state.stories.all[key]); 12 | return { 13 | currentUser: state.session.currentUser, 14 | showedUser: state.users.showed, 15 | storiesByUser: storiesArray.filter(story => story.author_id === parseInt(ownProps.match.params.id)), 16 | user_id: state.session.currentUser.id, 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = dispatch => { 21 | return { 22 | updateUser: (user, id) => dispatch(updateUser(user, id)), 23 | fetchSingleUser: (id) => dispatch(fetchSingleUser(id)), 24 | fetchStories: () => dispatch(fetchStories()), 25 | fetchSingleStory: (id) => dispatch(fetchSingleStory(id)), 26 | updateStoryBookmarks: (newStory, id) => dispatch(updateStoryBookmarks(newStory, id)), 27 | updateUserFollows: (attributes, id) => dispatch(updateUserFollows(attributes, id)), 28 | refresh: (id) => dispatch(refresh(id)), 29 | follow: (id) => dispatch(follow(id)), 30 | unFollow: (id) => dispatch(unFollow(id)), 31 | }; 32 | }; 33 | 34 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(UserShow)); 35 | -------------------------------------------------------------------------------- /frontend/components/users/users.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | class Users extends React.Component { 4 | componentWillMount(){ 5 | this.props.fetchUsers(); 6 | } 7 | 8 | render(){ 9 | const users = this.props.users.map(user => { 10 | return ( 11 |
  • {user.username}
  • 12 | ); 13 | }); 14 | return ( 15 |
    16 |

    Hello from the users list

    17 | {users} 18 |
    19 | ); 20 | } 21 | } 22 | 23 | export default Users; 24 | -------------------------------------------------------------------------------- /frontend/components/users/users_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchUsers } from '../../actions/user_actions'; 3 | import { usersSelector } from '../../reducers/selectors.js'; 4 | import Users from './users'; 5 | 6 | const mapStateToProps = ({users}) => { 7 | return { 8 | users: usersSelector(users) 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch, ownProps) => { 13 | return { 14 | fetchUsers: () => dispatch(fetchUsers()) 15 | }; 16 | }; 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(Users); 19 | -------------------------------------------------------------------------------- /frontend/large.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 | // window.signUp = APIUtil.signUp; 6 | // window.logIn = APIUtil.logIn; 7 | // window.logOut = APIUtil.logOut; 8 | 9 | document.addEventListener('DOMContentLoaded', () => { 10 | let store; 11 | if (window.currentUser) { 12 | const preloadedState = { session: { currentUser: window.currentUser } }; 13 | store = configureStore(preloadedState); 14 | delete window.currentUser; 15 | } else { 16 | store = configureStore(); 17 | } 18 | // window.store = store; 19 | // window.dispatch = store.dispatch; 20 | const rootEl = document.getElementById('root'); 21 | ReactDOM.render(, rootEl); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/reducers/comments_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_COMMENT, 3 | UPDATE_COMMENT, 4 | DELETE_COMMENT, 5 | CREATE_COMMENT 6 | } from '../actions/comment_actions'; 7 | 8 | import { 9 | RECEIVE_SINGLE_STORY 10 | } from '../actions/story_actions'; 11 | 12 | import { merge } from 'lodash'; 13 | 14 | const defaultState = {}; 15 | 16 | 17 | const commentsReducer = (state = defaultState, action) => { 18 | Object.freeze(state); 19 | let newState = merge({}, state); 20 | switch (action.type) { 21 | case RECEIVE_SINGLE_STORY: 22 | return action.story.comments || {}; 23 | 24 | case RECEIVE_COMMENT: 25 | return merge(newState, action.comment ); 26 | 27 | case UPDATE_COMMENT: 28 | merge(newState, action.comment); 29 | return newState; 30 | 31 | case DELETE_COMMENT: 32 | delete newState[action.comment.id]; 33 | return newState; 34 | 35 | case CREATE_COMMENT: 36 | return merge(newState, action.comment); 37 | 38 | 39 | default: 40 | return newState; 41 | } 42 | }; 43 | 44 | export default commentsReducer; 45 | -------------------------------------------------------------------------------- /frontend/reducers/presentational_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | TURN_OFF_MODAL_ANIMATION, 3 | TURN_ON_MODAL_ANIMATION, 4 | } from '../actions/presentational_actions'; 5 | import { merge } from 'lodash'; 6 | 7 | const defaultState = { 8 | modalAnimation: true, 9 | }; 10 | 11 | const presentationalReducer = (state = defaultState, action) => { 12 | Object.freeze(state); 13 | let newState = merge({}, state); 14 | switch (action.type) { 15 | case TURN_ON_MODAL_ANIMATION: 16 | newState.modalAnimation = true 17 | return newState; 18 | case TURN_OFF_MODAL_ANIMATION: 19 | newState.modalAnimation = false 20 | return newState; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export default presentationalReducer; 27 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { combineReducers } from "redux"; 3 | import session from './session_reducer'; 4 | import presentation from './presentational_reducer'; 5 | import users from './users_reducer'; 6 | import stories from './stories_reducer'; 7 | import comments from './comments_reducer'; 8 | 9 | const rootReducer = combineReducers({ 10 | session, 11 | presentation, 12 | users, 13 | stories, 14 | comments, 15 | }); 16 | 17 | export default rootReducer; 18 | -------------------------------------------------------------------------------- /frontend/reducers/selectors.js: -------------------------------------------------------------------------------- 1 | export const usersSelector = (users) => { 2 | return Object.keys(users).map(key => users[key] ); 3 | }; 4 | 5 | export const storiesSelector = (stories) => { 6 | return Object.keys(stories).map(key => stories[key] ); 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ERRORS, 3 | RECEIVE_CURRENT_USER, 4 | SCRUB_ERRORS, 5 | UPDATE_DRAFT, 6 | REMOVE_DRAFT 7 | } from '../actions/session_actions'; 8 | import { 9 | receiveCurrentUser, 10 | receiveErrors, 11 | updateDraft, 12 | removeDraft 13 | } from '../actions/session_actions'; 14 | import { 15 | RECEIVE_COMMENT 16 | } from '../actions/comment_actions' 17 | 18 | import { merge } from 'lodash'; 19 | 20 | const defaultState = { 21 | currentUser: null, 22 | errors: [], 23 | draft: {} 24 | }; 25 | 26 | const sessionReducer = (state = defaultState, action) => { 27 | Object.freeze(state); 28 | let newState = merge({}, state); 29 | switch (action.type) { 30 | 31 | case RECEIVE_CURRENT_USER: 32 | newState.currentUser = action.currentUser; 33 | newState.errors = []; 34 | return newState; 35 | 36 | case RECEIVE_COMMENT: 37 | newState.currentUser.comment_ids.push(parseInt(Object.keys(action.comment))) 38 | return newState; 39 | 40 | case RECEIVE_ERRORS: 41 | newState.errors = action.errors; 42 | return newState; 43 | 44 | case SCRUB_ERRORS: 45 | newState.errors = []; 46 | return newState; 47 | 48 | case UPDATE_DRAFT: 49 | newState.draft = merge({}, newState.draft, action.newDraft); 50 | return newState; 51 | 52 | case REMOVE_DRAFT: 53 | newState.draft = {}; 54 | return newState; 55 | 56 | default: 57 | return state; 58 | } 59 | }; 60 | 61 | export default sessionReducer; 62 | -------------------------------------------------------------------------------- /frontend/reducers/stories_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_STORIES, 3 | RECEIVE_SINGLE_STORY, 4 | UPDATE_STORY, 5 | DELETE_STORY, 6 | CREATE_STORY, 7 | RECEIVE_FEED, 8 | RECEIVE_SINGLE_FEED 9 | } from '../actions/story_actions'; 10 | import { merge } from 'lodash'; 11 | import { receiveComments } from '../actions/comment_actions'; 12 | 13 | 14 | const defaultState = { 15 | all: {}, 16 | feed: {} 17 | }; 18 | 19 | const storiesReducer = (state = defaultState, action) => { 20 | Object.freeze(state); 21 | let newState = merge({}, state); 22 | switch (action.type) { 23 | 24 | case RECEIVE_FEED: 25 | newState.feed = action.stories 26 | return newState; 27 | 28 | case RECEIVE_STORIES: 29 | return merge({}, newState, { all : action.stories }); 30 | 31 | case RECEIVE_SINGLE_STORY: 32 | return merge( 33 | newState, 34 | { all: { [action.story.id]: action.story } } 35 | ); 36 | 37 | case RECEIVE_SINGLE_FEED: 38 | return merge( 39 | newState, 40 | { feed: { [action.story.id]: action.story } } 41 | ); 42 | 43 | case UPDATE_STORY: 44 | newState[action.story.id] = action.story; 45 | return newState; 46 | 47 | case DELETE_STORY: 48 | delete newState.all[action.id]; 49 | return newState; 50 | 51 | case CREATE_STORY: 52 | newState[action.story.id] = action.story; 53 | return newState; 54 | 55 | default: 56 | return newState; 57 | } 58 | }; 59 | 60 | export default storiesReducer; 61 | -------------------------------------------------------------------------------- /frontend/reducers/users_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_USERS, 3 | RECEIVE_SINGLE_USER 4 | } from '../actions/user_actions'; 5 | import { merge } from 'lodash'; 6 | 7 | const defaultState = {all: {}, showed: {}}; 8 | 9 | const usersReducer = (state = defaultState, action) => { 10 | Object.freeze(state); 11 | let newState = merge({}, state); 12 | 13 | switch (action.type) { 14 | case RECEIVE_USERS: 15 | return merge(newState, {all: action.users}); 16 | 17 | case RECEIVE_SINGLE_USER: 18 | return merge(newState, {showed: action.user}); 19 | default: 20 | return newState; 21 | } 22 | }; 23 | 24 | export default usersReducer; 25 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | applyMiddleware 4 | } from 'redux'; 5 | import thunk from 'redux-thunk'; 6 | import rootReducer from '../reducers/root_reducer'; 7 | 8 | const middlewares = [thunk]; 9 | 10 | if (process.env.NODE_ENV !== 'production') { 11 | const {createLogger} = require('redux-logger'); 12 | middlewares.push(createLogger()); 13 | } 14 | 15 | 16 | const configureStore = (preloadedState = {}) => { 17 | return createStore( 18 | rootReducer, 19 | preloadedState, 20 | applyMiddleware(...middlewares) 21 | ); 22 | }; 23 | 24 | export default configureStore; 25 | -------------------------------------------------------------------------------- /frontend/util/comment_util.jsx: -------------------------------------------------------------------------------- 1 | export const createComment = (comment) => { 2 | return $.ajax({ 3 | method: "POST", 4 | url: "/api/comments", 5 | data: { comment } 6 | }); 7 | }; 8 | 9 | export const updateComment = (comment, id) => { 10 | return $.ajax({ 11 | method: "PATCH", 12 | url: `/api/comments/${id}`, 13 | data: { comment } 14 | }); 15 | }; 16 | 17 | export const removeComment = (id) => { 18 | return $.ajax({ 19 | method: "DELETE", 20 | url: `/api/comments/${id}`, 21 | }); 22 | }; 23 | 24 | export const fetchSingleComment = (id) => { 25 | return $.ajax({ 26 | method: "GET", 27 | url: `/api/comments/${id}`, 28 | }); 29 | }; 30 | 31 | export const updateCommentLikes = (newLike, id) => { 32 | return $.ajax({ 33 | method: "PATCH", 34 | url: `/api/comments/${id}`, 35 | data: {comment: { likes_attributes: newLike } }, 36 | }); 37 | }; 38 | 39 | // addLike() { 40 | // this.props.updateComment({ likes_attributes: {likeable_id: this.props.comment.id, likeable_type: "Comment"}}) 41 | // } 42 | -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withRouter, Route, Redirect } from "react-router-dom" 3 | import { connect } from "react-redux"; 4 | 5 | const Auth = ({ component: Component, path, loggedIn }) => ( 6 | ( 7 | !loggedIn ? ( 8 | 9 | ) : ( 10 | 11 | ) 12 | )} /> 13 | ); 14 | 15 | const Protected = ({ component: Component, path, loggedIn }) => ( 16 | ( 17 | loggedIn ? ( 18 | 19 | ) : ( 20 | 21 | ) 22 | )} /> 23 | ); 24 | 25 | const mapStateToProps = state => { 26 | return { loggedIn: Boolean(state.session.currentUser) }; 27 | }; 28 | 29 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth)); 30 | export const ProtectedRoute = withRouter(connect(mapStateToProps, null)(Protected)); 31 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signUp = formData => { 2 | return $.ajax({ 3 | method: "POST", 4 | url: "/api/users", 5 | contentType: false, 6 | processData: false, 7 | data: formData, 8 | }); 9 | }; 10 | 11 | 12 | export const signIn = formData => { 13 | return $.ajax({ 14 | method: "POST", 15 | url: "/api/session", 16 | contentType: false, 17 | processData: false, 18 | data: formData, 19 | }); 20 | }; 21 | 22 | export const logOut = () => { 23 | return $.ajax({ 24 | method: "DELETE", 25 | url: "/api/session", 26 | }); 27 | }; 28 | 29 | export const fetchUsers = () => { 30 | return $.ajax({ 31 | method: 'GET', 32 | url: "api/users", 33 | }); 34 | }; 35 | 36 | export const fetchSingleUser = (id) => { 37 | return $.ajax({ 38 | method: 'GET', 39 | url: `api/users/${id}`, 40 | }); 41 | }; 42 | 43 | export const updateUser = (formData, id) => { 44 | return $.ajax({ 45 | method: 'PATCH', 46 | url: `api/users/${id}`, 47 | contentType: false, 48 | processData: false, 49 | data: formData 50 | }); 51 | }; 52 | 53 | export const updateUserFollows = (userAttributes, id) => { 54 | return $.ajax({ 55 | method: "PATCH", 56 | url: `/api/users/${id}`, 57 | data: {user: { followed_author_follows_attributes: userAttributes } }, 58 | }); 59 | }; 60 | 61 | export const follow = (id) => { 62 | return $.ajax({method: "POST", url: `api/users/${id}/follow`}); 63 | }; 64 | 65 | export const unFollow = (id) => { 66 | return $.ajax({method: "POST", url: `api/users/${id}/unfollow`}); 67 | }; 68 | -------------------------------------------------------------------------------- /frontend/util/story_util.js: -------------------------------------------------------------------------------- 1 | export const createStory = formData => { 2 | return $.ajax({ 3 | method: "POST", 4 | url: "/api/stories", 5 | contentType: false, 6 | processData: false, 7 | data: formData, 8 | }); 9 | }; 10 | 11 | export const updateStory = (formData, id) => { 12 | return $.ajax({ 13 | method: "PATCH", 14 | url: `/api/stories/${id}`, 15 | contentType: false, 16 | processData: false, 17 | data: formData, 18 | }); 19 | }; 20 | 21 | export const updateStoryLikes = (newLike, id) => { 22 | return $.ajax({ 23 | method: "PATCH", 24 | url: `/api/stories/${id}`, 25 | data: {story: { likes_attributes: newLike } }, 26 | }); 27 | }; 28 | 29 | export const updateStoryBookmarks = (newStory, id) => { 30 | return $.ajax({ 31 | method: "PATCH", 32 | url: `/api/stories/${id}`, 33 | data: {story: { bookmarks_attributes: newStory } }, 34 | }); 35 | }; 36 | 37 | export const removeStory = (id) => { 38 | return $.ajax({ 39 | method: "DELETE", 40 | url: `/api/stories/${id}`, 41 | }); 42 | }; 43 | 44 | export const fetchStories = () => { 45 | return $.ajax({ 46 | method: 'GET', 47 | url: "api/stories", 48 | }); 49 | }; 50 | 51 | export const fetchSingleStory = (id) => { 52 | return $.ajax({ 53 | method: 'GET', 54 | url: `api/stories/${id}`, 55 | }); 56 | }; 57 | 58 | export const feed = () => { 59 | return $.ajax({ 60 | method: 'POST', 61 | url: 'api/stories/feed' 62 | }); 63 | }; 64 | 65 | export const bookmarked = () => { 66 | return $.ajax({ 67 | method: 'POST', 68 | url: 'api/stories/bookmarked' 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "large", 3 | "version": "1.0.0", 4 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "webpack --watch", 12 | "postinstall": "webpack" 13 | }, 14 | "engines": { 15 | "node": "4.1.1", 16 | "npm": "2.1.x" 17 | }, 18 | "keywords": [], 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "babel-core": "^6.25.0", 23 | "babel-loader": "^7.1.0", 24 | "babel-preset-es2015": "^6.24.1", 25 | "babel-preset-react": "^6.24.1", 26 | "html-loader": "^0.4.5", 27 | "jquery": "^3.2.1", 28 | "lodash": "^4.17.4", 29 | "react": "^15.5.4", 30 | "react-dom": "^15.5.4", 31 | "react-quill": "^1.0.0-rc.3", 32 | "react-redux": "^5.0.5", 33 | "react-render-html": "^0.2.0", 34 | "react-router-dom": "^4.1.1", 35 | "react-sticky": "^6.0.1", 36 | "redux": "^3.6.0", 37 | "redux-thunk": "^2.2.0", 38 | "ts-loader": "^2.2.0", 39 | "typescript": "^2.3.4", 40 | "webpack": "^2.6.1" 41 | }, 42 | "devDependencies": { 43 | "redux-logger": "^3.0.6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/favicon.ico -------------------------------------------------------------------------------- /public/images/bookmark_clicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/images/bookmark_clicked.png -------------------------------------------------------------------------------- /public/images/bookmark_unclicked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/images/bookmark_unclicked.png -------------------------------------------------------------------------------- /public/images/default_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/images/default_user.png -------------------------------------------------------------------------------- /public/images/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/images/large.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/images/logo.png -------------------------------------------------------------------------------- /public/images/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/images/pencil.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /public/system/users/images/000/000/003/original/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/003/original/cat.jpg -------------------------------------------------------------------------------- /public/system/users/images/000/000/003/thumb/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/003/thumb/cat.png -------------------------------------------------------------------------------- /public/system/users/images/000/000/006/original/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/006/original/cat.jpg -------------------------------------------------------------------------------- /public/system/users/images/000/000/006/thumb/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/006/thumb/cat.png -------------------------------------------------------------------------------- /public/system/users/images/000/000/007/original/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/007/original/cat.jpg -------------------------------------------------------------------------------- /public/system/users/images/000/000/007/thumb/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/007/thumb/cat.png -------------------------------------------------------------------------------- /public/system/users/images/000/000/009/original/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/009/original/cat.jpg -------------------------------------------------------------------------------- /public/system/users/images/000/000/009/thumb/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/009/thumb/cat.png -------------------------------------------------------------------------------- /public/system/users/images/000/000/010/original/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/010/original/cat.jpg -------------------------------------------------------------------------------- /public/system/users/images/000/000/010/thumb/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/010/thumb/cat.png -------------------------------------------------------------------------------- /public/system/users/images/000/000/011/original/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/011/original/cat.jpg -------------------------------------------------------------------------------- /public/system/users/images/000/000/011/thumb/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/public/system/users/images/000/000/011/thumb/cat.png -------------------------------------------------------------------------------- /spec/controllers/comments_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'helper_methods' 3 | 4 | RSpec.describe Api::CommentsController, type: :controller do 5 | render_views 6 | include Helpers 7 | let(:user) { create :user } 8 | subject(:comment) { create :comment, author_id: user.id } 9 | 10 | describe 'GET #show' do 11 | before(:each) { login } 12 | context 'with valid comment id' do 13 | it 'sends the comment' do 14 | get :show, format: :json, id: comment.id 15 | expect(response).to be_success 16 | expect(json.keys.first.to_i).to eq(comment.id) 17 | end 18 | end 19 | context 'with invalid comment id' do 20 | it 'will not render show' do 21 | begin 22 | get :show, format: :json, id: 999 23 | rescue 24 | expect(response).to_not render_template :show 25 | end 26 | end 27 | end 28 | end 29 | 30 | describe 'POST #create' do 31 | before(:each) { login } 32 | let(:comment) { build :comment } 33 | context 'with a valid params' do 34 | it 'saves comment' do 35 | post :create, format: :json, comment: valid_comment_params 36 | expect(Comment.find_by_body('valid comment body')).to_not be nil 37 | end 38 | it 'sends comment' do 39 | post :create, format: :json, comment: valid_comment_params 40 | expect(response).to render_template :show 41 | end 42 | end 43 | context 'with invalid params' do 44 | it 'sends 422 status' do 45 | begin 46 | post :create, format: :json, comment: invalid_params 47 | rescue 48 | expect(response).to have_http_status 422 49 | end 50 | end 51 | end 52 | end 53 | 54 | describe 'PATCH #update' do 55 | before(:each) { login } 56 | it 'updates comment with valid params' do 57 | patch :update, format: :json, :id => comment.id, :comment => { body: "Updated Body"} 58 | expect(comment.reload.body).to eq("Updated Body") 59 | end 60 | it 'sends 422 with invalid params' do 61 | patch :update, format: :json, :id => comment.id, :comment => { body: nil} 62 | expect(response).to have_http_status 422 63 | end 64 | end 65 | 66 | describe 'DELETE #destroy' do 67 | it 'removes a comment' do 68 | delete :destroy, format: :json, :id => comment.id 69 | expect(Comment.find_by_id(comment.id)).to be nil 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'helper_methods' 3 | 4 | RSpec.describe Api::SessionsController, type: :controller do 5 | render_views 6 | include Helpers 7 | let(:user) { create :user, password: "password123" } 8 | 9 | describe 'POST #create' do 10 | context 'with a valid user' do 11 | it 'logs in user' do 12 | post :create, format: :json, user: {username: user.username, password: user.password} 13 | expect(controller.view_context.current_user).to eq user 14 | end 15 | it 'renders show' do 16 | post :create, format: :json, user: {username: user.username, password: user.password} 17 | expect(response).to render_template :show 18 | end 19 | end 20 | context 'with invalid params' do 21 | it 'sends 422 status' do 22 | begin 23 | post :create, format: :json, user: invalid_params 24 | rescue 25 | expect(response).to have_http_status 422 26 | end 27 | end 28 | it 'Adds invalid credentials message to users errors' do 29 | begin 30 | post :create, format: :json, user: invalid_params 31 | rescue 32 | expect(response.body).to include("invalid credentials") 33 | end 34 | end 35 | end 36 | end 37 | 38 | describe 'DELETE #destroy' do 39 | it 'logs out current user' do 40 | login 41 | delete :destroy, format: :json, :id => user.id 42 | expect(controller.view_context.current_user).to be nil 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/controllers/static_pages_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'helper_methods' 3 | 4 | RSpec.describe StaticPagesController, type: :controller do 5 | describe 'GET #root' do 6 | it 'renders root' do 7 | get :root 8 | expect(response).to render_template :root 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/controllers/stories_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | require 'helper_methods' 3 | 4 | RSpec.describe Api::StoriesController, type: :controller do 5 | render_views 6 | include Helpers 7 | let(:user) { create :user } 8 | subject(:story) { create :story, author_id: user.id } 9 | describe 'GET #index' do 10 | before(:each) { login } 11 | it 'sends a list of stories' do 12 | create_list(:story, 10, author_id: user.id) 13 | get :index, format: :json 14 | expect(response).to be_success 15 | expect(json.length).to eq(10) 16 | end 17 | end 18 | 19 | describe 'GET #show' do 20 | before(:each) { login } 21 | context 'with valid story id' do 22 | it 'sends the story' do 23 | get :show, format: :json, id: story.id 24 | expect(response).to be_success 25 | expect(json['id']).to eq(story.id) 26 | end 27 | end 28 | context 'with invalid story id' do 29 | it 'will not render show' do 30 | begin 31 | get :show, format: :json, id: 999 32 | rescue 33 | expect(response).to_not render_template :show 34 | end 35 | end 36 | end 37 | end 38 | 39 | describe 'POST #create' do 40 | before(:each) { login } 41 | let(:story) { build :story } 42 | context 'with a valid params' do 43 | it 'saves story' do 44 | post :create, format: :json, story: valid_story_params(user.id) 45 | expect(Story.find_by_title(valid_story_params(user.id)[:title])).to_not be nil 46 | end 47 | it 'sends story' do 48 | post :create, format: :json, story: valid_story_params(user.id) 49 | expect(response).to render_template :show 50 | end 51 | end 52 | context 'with invalid params' do 53 | invalid_params = { bad: 'justhaxingyaserver' } 54 | it 'sends 422 status' do 55 | begin 56 | post :create, format: :json, story: invalid_params 57 | rescue 58 | expect(response).to have_http_status 422 59 | end 60 | end 61 | end 62 | end 63 | 64 | describe 'PATCH #update' do 65 | before(:each) { login } 66 | it 'updates story with valid params' do 67 | patch :update, format: :json, :id => story.id, :story => { title: "New Title"} 68 | expect(story.reload.title).to eq("New Title") 69 | end 70 | it 'sends 422 with invalid params' do 71 | patch :update, format: :json, :id => story.id, :story => { title: nil} 72 | expect(response).to have_http_status 422 73 | end 74 | end 75 | 76 | describe 'DELETE #destroy' do 77 | it 'removes a story' do 78 | delete :destroy, format: :json, :id => story.id 79 | expect(Story.find_by_id(story.id)).to be nil 80 | end 81 | end 82 | 83 | describe 'POST #feed' do 84 | it 'only sends stories written by followed users' do 85 | login 86 | followed_user = create :user 87 | unfollowed_user = create :user 88 | feed_story = create :story, author_id: followed_user.id 89 | notfeed_story = create :story, author_id: unfollowed_user.id 90 | user.followees = [followed_user] 91 | user.save 92 | post :feed, format: :json 93 | expect(json.keys).to include(feed_story.id.to_s) 94 | expect(json.keys).to_not include(notfeed_story.id.to_s) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /spec/factories/bookmarks.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookmarks 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # story_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | FactoryBot.define do 13 | factory :bookmark do 14 | user_id 1 15 | story_id 1 16 | end 17 | end 18 | 19 | -------------------------------------------------------------------------------- /spec/factories/comment.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :comment do 3 | body { Faker::Hipster.paragraph } 4 | author_id 1 5 | story_id 1 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/follow.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :follow do 3 | follower_id 1 4 | author_id 1 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/likes.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # likeable_id :integer 7 | # likeable_type :string 8 | # created_at :datetime 9 | # updated_at :datetime 10 | # user_id :integer not null 11 | # 12 | 13 | FactoryBot.define do 14 | factory :like do 15 | user_id 1 16 | likeable_type "Story" 17 | likeable_id 1 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /spec/factories/stories.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: stories 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # body :string not null 8 | # author_id :integer not null 9 | # date :date not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # description :string 17 | # 18 | 19 | FactoryBot.define do 20 | factory :story do 21 | title { Faker::Hipster.words.first } 22 | body { Faker::Hipster.paragraph(15, true) } 23 | description { Faker::Hipster.sentence } 24 | author_id 1 25 | date { Date.new } 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # image_file_name :string 10 | # image_content_type :string 11 | # image_file_size :integer 12 | # image_updated_at :datetime 13 | # bio :string 14 | # 15 | 16 | FactoryBot.define do 17 | factory :user do 18 | username { Faker::GameOfThrones.character } 19 | password { Faker::Internet.password(6, 12) } 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /spec/helper_methods.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | def json 3 | JSON.parse(response.body) 4 | end 5 | 6 | def login 7 | controller.view_context.login(user) 8 | end 9 | 10 | def valid_story_params(author_id) 11 | { 12 | title: "sample Story", 13 | description: "sample story description", 14 | body: "I am a story blah blah blah", 15 | author_id: author_id, 16 | date: Date.new, 17 | } 18 | end 19 | 20 | def valid_comment_params 21 | {author_id: user.id, story_id: 1, body: "valid comment body"} 22 | end 23 | 24 | def invalid_params 25 | { bad: 'justhaxingyaserver' } 26 | end 27 | end -------------------------------------------------------------------------------- /spec/models/bookmark_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookmarks 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # story_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | require 'rails_helper' 13 | 14 | RSpec.describe Bookmark, type: :model do 15 | subject(:bookmark) { create :bookmark} 16 | describe 'validations' do 17 | it { should validate_presence_of :user_id } 18 | it { should validate_presence_of :story_id } 19 | it { should validate_uniqueness_of(:user_id).scoped_to(:story_id) } 20 | end 21 | 22 | describe 'associations' do 23 | it { should belong_to :story } 24 | it { should belong_to :user } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/models/comment_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string not null 7 | # date :date not null 8 | # author_id :integer not null 9 | # story_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | require 'rails_helper' 15 | 16 | RSpec.describe Comment, type: :model do 17 | describe 'validations' do 18 | it { should validate_presence_of :body } 19 | it { should validate_presence_of :story_id } 20 | it { should validate_presence_of :date } 21 | end 22 | 23 | describe 'associations' do 24 | it { should accept_nested_attributes_for :likes } 25 | it { should belong_to :author } 26 | it { should belong_to :story } 27 | it { should have_many :likers } 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/models/follow_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer not null 7 | # author_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | require 'rails_helper' 13 | 14 | RSpec.describe Follow, type: :model do 15 | subject(:follow) { create :follow} 16 | describe 'validations' do 17 | it { should validate_presence_of :follower_id } 18 | it { should validate_presence_of :author_id } 19 | it { should validate_uniqueness_of(:follower_id).scoped_to(:author_id) } 20 | end 21 | 22 | describe 'associations' do 23 | it { should belong_to :follower } 24 | it { should belong_to :author } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/models/like_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # likeable_id :integer 7 | # likeable_type :string 8 | # created_at :datetime 9 | # updated_at :datetime 10 | # user_id :integer not null 11 | # 12 | 13 | require 'rails_helper' 14 | 15 | RSpec.describe Like, type: :model do 16 | subject(:like) { create :like} 17 | describe 'validations' do 18 | it { should validate_presence_of :user_id } 19 | it { should validate_presence_of :likeable_type } 20 | it { should validate_presence_of :likeable_id } 21 | it { should validate_uniqueness_of(:likeable_type).scoped_to(:likeable_id) } 22 | end 23 | 24 | describe 'associations' do 25 | it { should belong_to :likeable } 26 | it { should belong_to :user } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/models/story_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: stories 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # body :string not null 8 | # author_id :integer not null 9 | # date :date not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # description :string 17 | # 18 | 19 | require 'rails_helper' 20 | 21 | RSpec.describe Story, type: :model do 22 | subject(:story) { build :story } 23 | describe 'validations' do 24 | it { should validate_presence_of :title } 25 | it { should validate_presence_of :body } 26 | it { should validate_presence_of :author_id } 27 | it { should validate_presence_of :description } 28 | it { should validate_presence_of :date } 29 | it 'should have a default image if none set' do 30 | expect(story.image.url).to_not be nil 31 | end 32 | end 33 | 34 | describe 'associations' do 35 | it { should accept_nested_attributes_for :likes } 36 | it { should accept_nested_attributes_for :bookmarks } 37 | it { should have_many :likers } 38 | it { should have_many :comments } 39 | it { should have_many :comment_authors } 40 | it { should have_many :bookmarks } 41 | it { should have_many :bookmarking_users } 42 | it { should belong_to :author } 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # image_file_name :string 10 | # image_content_type :string 11 | # image_file_size :integer 12 | # image_updated_at :datetime 13 | # bio :string 14 | # 15 | 16 | require 'rails_helper' 17 | 18 | RSpec.describe User, type: :model do 19 | subject(:user) { create :user } 20 | describe 'validations' do 21 | it { should validate_presence_of :username } 22 | it { should validate_presence_of :password_digest } 23 | context 'no password digest explicitly set' do 24 | it 'should validate presence of password_digest' do 25 | expect(user.password_digest).to_not be nil 26 | end 27 | end 28 | it 'should have a default image if none set' do 29 | expect(user.image.url).to_not be nil 30 | end 31 | it 'should have a session token set automatically at validation' do 32 | expect(user.session_token).to_not be nil 33 | end 34 | it 'should have a bio set automatically at validation' do 35 | expect(user.bio).to_not be nil 36 | end 37 | 38 | it 'should not save passwords to the database' do 39 | saved_user = User.find(user.id) 40 | expect(saved_user.password).to be nil 41 | end 42 | end 43 | 44 | describe 'associations' do 45 | it {should have_many :followees } 46 | it { should have_many :followers } 47 | it { should have_many :bookmarks } 48 | it { should have_many :bookmarked_stories } 49 | it { should have_many :liked_stories } 50 | it { should have_many :liked_comments } 51 | it { should have_many :stories } 52 | it { should have_many :comments } 53 | it { should have_many :followee_follows } 54 | it { should have_many :feed_stories } 55 | it { should have_many :follower_follows } 56 | end 57 | 58 | describe 'class methods' do 59 | describe '::find_by_credentials' do 60 | it 'finds user when given correct password' do 61 | with_correct_password = User.find_by_credentials(user.username, user.password) 62 | expect(with_correct_password).to_not be nil 63 | end 64 | it 'finds nothing when given the wrong password' do 65 | with_incorrect_password = User.find_by_credentials(user.username, "imahaxor") 66 | expect(with_incorrect_password).to be nil 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require File.expand_path('../../config/environment', __FILE__) 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'rspec/rails' 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | 10 | # Requires supporting ruby files with custom matchers and macros, etc, in 11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 12 | # run as spec files by default. This means that files in spec/support that end 13 | # in _spec.rb will both be required and run as specs, causing the specs to be 14 | # run twice. It is recommended that you do not name files matching this glob to 15 | # end with _spec.rb. You can configure this pattern with the --pattern 16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 17 | # 18 | # The following line is provided for convenience purposes. It has the downside 19 | # of increasing the boot-up time by auto-requiring all files in the support 20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 21 | # require only the support files necessary. 22 | # 23 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 24 | 25 | # Checks for pending migrations and applies them before tests are run. 26 | # If you are not using ActiveRecord, you can remove this line. 27 | ActiveRecord::Migration.maintain_test_schema! 28 | 29 | RSpec.configure do |config| 30 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 31 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 32 | config.include FactoryBot::Syntax::Methods 33 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 34 | # examples within a transaction, remove the following line or assign false 35 | # instead of true. 36 | config.use_transactional_fixtures = true 37 | 38 | # RSpec Rails can automatically mix in different behaviours to your tests 39 | # based on their file location, for example enabling you to call `get` and 40 | # `post` in specs under `spec/controllers`. 41 | # 42 | # You can disable this behaviour by removing the line below, and instead 43 | # explicitly tag your specs with their type, e.g.: 44 | # 45 | # RSpec.describe UsersController, :type => :controller do 46 | # # ... 47 | # end 48 | # 49 | # The different available types are documented in the features, such as in 50 | # https://relishapp.com/rspec/rspec-rails/docs 51 | config.infer_spec_type_from_file_location! 52 | 53 | # Filter lines from Rails gems in backtraces. 54 | config.filter_rails_from_backtrace! 55 | # arbitrary gems may also be filtered via: 56 | # config.filter_gems_from_backtrace("gem name") 57 | 58 | end 59 | Shoulda::Matchers.configure do |config| 60 | config.integrate do |with| 61 | with.test_framework :rspec 62 | with.library :rails 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/test/controllers/.keep -------------------------------------------------------------------------------- /test/controllers/api/stories_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::StoriesControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/comments_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CommentsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/likes_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class LikesControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/test/fixtures/.keep -------------------------------------------------------------------------------- /test/fixtures/bookmarks.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookmarks 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # story_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 13 | 14 | # This model initially had no columns defined. If you add columns to the 15 | # model remove the '{}' from the fixture names and add the columns immediately 16 | # below each fixture, per the syntax in the comments below 17 | # 18 | one: {} 19 | # column: value 20 | # 21 | two: {} 22 | # column: value 23 | -------------------------------------------------------------------------------- /test/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string not null 7 | # date :date not null 8 | # author_id :integer not null 9 | # story_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 15 | 16 | # This model initially had no columns defined. If you add columns to the 17 | # model remove the '{}' from the fixture names and add the columns immediately 18 | # below each fixture, per the syntax in the comments below 19 | # 20 | one: {} 21 | # column: value 22 | # 23 | two: {} 24 | # column: value 25 | -------------------------------------------------------------------------------- /test/fixtures/follows.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer not null 7 | # author_id :integer not null 8 | # created_at :datetime not null 9 | # updated_at :datetime not null 10 | # 11 | 12 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 13 | 14 | # This model initially had no columns defined. If you add columns to the 15 | # model remove the '{}' from the fixture names and add the columns immediately 16 | # below each fixture, per the syntax in the comments below 17 | # 18 | one: {} 19 | # column: value 20 | # 21 | two: {} 22 | # column: value 23 | -------------------------------------------------------------------------------- /test/fixtures/likes.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # likeable_id :integer 7 | # likeable_type :string 8 | # created_at :datetime 9 | # updated_at :datetime 10 | # user_id :integer not null 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | # This model initially had no columns defined. If you add columns to the 16 | # model remove the '{}' from the fixture names and add the columns immediately 17 | # below each fixture, per the syntax in the comments below 18 | # 19 | one: {} 20 | # column: value 21 | # 22 | two: {} 23 | # column: value 24 | -------------------------------------------------------------------------------- /test/fixtures/stories.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: stories 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # body :string not null 8 | # author_id :integer not null 9 | # date :date not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # description :string 17 | # 18 | 19 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 20 | 21 | # This model initially had no columns defined. If you add columns to the 22 | # model remove the '{}' from the fixture names and add the columns immediately 23 | # below each fixture, per the syntax in the comments below 24 | # 25 | one: {} 26 | # column: value 27 | # 28 | two: {} 29 | # column: value 30 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/test/models/.keep -------------------------------------------------------------------------------- /test/models/bookmark_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: bookmarks 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # story_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 BookmarkTest < ActiveSupport::TestCase 15 | # test "the truth" do 16 | # assert true 17 | # end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/comment_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # body :string not null 7 | # date :date not null 8 | # author_id :integer not null 9 | # story_id :integer not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | require 'test_helper' 15 | 16 | class CommentTest < ActiveSupport::TestCase 17 | # test "the truth" do 18 | # assert true 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /test/models/follow_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: follows 4 | # 5 | # id :integer not null, primary key 6 | # follower_id :integer not null 7 | # author_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/like_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: likes 4 | # 5 | # id :integer not null, primary key 6 | # likeable_id :integer 7 | # likeable_type :string 8 | # created_at :datetime 9 | # updated_at :datetime 10 | # user_id :integer not null 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class LikeTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /test/models/story_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: stories 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # body :string not null 8 | # author_id :integer not null 9 | # date :date not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # description :string 17 | # 18 | 19 | require 'test_helper' 20 | 21 | class StoryTest < ActiveSupport::TestCase 22 | # test "the truth" do 23 | # assert true 24 | # end 25 | end 26 | -------------------------------------------------------------------------------- /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/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Th3Nathan/Large/fbbfc93b18b2e1301dd3f5e5875a538cae8171b3/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require("path"); 3 | var webpack = require("webpack"); 4 | 5 | var plugins = []; // if using any plugins for both dev and production 6 | var devPlugins = []; // if using any plugins for development 7 | 8 | var prodPlugins = [ 9 | new webpack.DefinePlugin({ 10 | 'process.env': { 11 | 'NODE_ENV': JSON.stringify('production') 12 | } 13 | }), 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compress: { 16 | warnings: true 17 | } 18 | }) 19 | ]; 20 | 21 | plugins = plugins.concat( 22 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins 23 | ) 24 | 25 | 26 | module.exports = { 27 | context: __dirname, 28 | entry: './frontend/large.jsx', 29 | output: { 30 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 31 | filename: 'bundle.js' 32 | }, 33 | plugins: plugins, 34 | resolve: { 35 | extensions: ['.js', '.jsx', '*'] 36 | }, 37 | module: { 38 | loaders: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /(node_modules|bower_components)/, 42 | loader: 'babel-loader', 43 | query: { 44 | presets: ['react', 'es2015'] 45 | } 46 | } 47 | ] 48 | }, 49 | devtool: 'source-map', 50 | }; 51 | --------------------------------------------------------------------------------