├── .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 [](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 |
33 |
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 |
46 | );
47 | } else {
48 | heart = (
49 |
55 | );
56 | }
57 |
58 |
59 | return(
60 |
61 |
66 |
67 |
{this.props.comment.body}
68 |
69 |
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 |
6 | {errors.map((error, idx) => (- {error}
))}
7 |
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 |
7 | - Actions
8 | - Delete story
9 |
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 |
7 | - Actions
8 | - Delete story
9 |
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 |
--------------------------------------------------------------------------------