├── .gitignore
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── assets
│ ├── config
│ │ └── manifest.js
│ ├── images
│ │ ├── .keep
│ │ ├── bad_logo.png
│ │ ├── first_logo.png
│ │ ├── fluffy.jpg
│ │ ├── little_camera.png
│ │ ├── mag-glass.png
│ │ ├── missing.png
│ │ └── story_default.png
│ ├── javascripts
│ │ ├── application.js
│ │ ├── cable.js
│ │ ├── channels
│ │ │ └── .keep
│ │ ├── sessions.coffee
│ │ └── users.coffee
│ └── stylesheets
│ │ ├── application.css
│ │ ├── bottom.css
│ │ ├── follow.css
│ │ ├── forms.css
│ │ ├── loader.css
│ │ ├── nav.css
│ │ ├── profile.css
│ │ ├── publish_options.css
│ │ ├── quill.bubble.css
│ │ ├── quill.core.css
│ │ ├── quill.snow.css
│ │ ├── reset.css
│ │ ├── responses.css
│ │ ├── sessions.scss
│ │ ├── stories_feed.css
│ │ ├── story.css
│ │ ├── story_input.css
│ │ ├── story_sidebar.css
│ │ └── users.scss
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── controllers
│ ├── api
│ │ ├── followings_controller.rb
│ │ ├── likes_controller.rb
│ │ ├── responses_controller.rb
│ │ ├── sessions_controller.rb
│ │ ├── stories_controller.rb
│ │ ├── topics_controller.rb
│ │ └── users_controller.rb
│ ├── application_controller.rb
│ ├── concerns
│ │ └── .keep
│ └── static_pages_controller.rb
├── helpers
│ ├── application_helper.rb
│ ├── sessions_helper.rb
│ └── users_helper.rb
├── jobs
│ └── application_job.rb
├── mailers
│ └── application_mailer.rb
├── models
│ ├── application_record.rb
│ ├── concerns
│ │ └── .keep
│ ├── following.rb
│ ├── like.rb
│ ├── response.rb
│ ├── story.rb
│ ├── topic.rb
│ └── user.rb
└── views
│ ├── api
│ ├── followings
│ │ ├── _following.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── likes
│ │ ├── _like.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── responses
│ │ ├── _response.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── stories
│ │ ├── _story.json.jbuilder
│ │ ├── index.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── topics
│ │ ├── _topic.json.jbuilder
│ │ └── index.json.jbuilder
│ └── users
│ │ ├── _user.json.jbuilder
│ │ ├── session_user.json.jbuilder
│ │ └── show.json.jbuilder
│ ├── layouts
│ ├── application.html.erb
│ ├── mailer.html.erb
│ └── mailer.text.erb
│ └── static_pages
│ └── root.html.erb
├── bin
├── bundle
├── rails
├── rake
├── setup
├── spring
└── update
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── application_controller_renderer.rb
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── cookies_serializer.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ ├── new_framework_defaults.rb
│ ├── session_store.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
├── puma.rb
├── routes.rb
├── secrets.yml
└── spring.rb
├── db
├── migrate
│ ├── 20170418152702_create_users.rb
│ ├── 20170420000222_add_attachment_photo_to_users.rb
│ ├── 20170420175634_create_stories.rb
│ ├── 20170420180234_add_attachment_main_image_to_stories.rb
│ ├── 20170422234929_create_responses.rb
│ ├── 20170425133845_create_likes.rb
│ ├── 20170425133900_create_followings.rb
│ ├── 20170427015509_add_null_to_likes.rb
│ └── 20170428111440_create_topics.rb
├── schema.rb
└── seeds.rb
├── docs
├── api-endpoints.md
├── caps
│ ├── m-cap-home.jpg
│ ├── m-cap-profile.jpg
│ ├── m-cap-story-input.jpg
│ └── m-cap-story.jpg
├── component-hierarchy.md
├── readme.md
├── sample-state.md
├── schema.md
└── wireframes
│ ├── auth-form.png
│ ├── filtered-feed.png
│ ├── home-logged-in.png
│ ├── search-page.png
│ ├── story-input.png
│ ├── story.png
│ └── user-profile.png
├── frontend
├── actions
│ ├── following_actions.js
│ ├── like_actions.js
│ ├── response_actions.js
│ ├── session_actions.js
│ ├── story_actions.js
│ ├── topic_actions.js
│ └── user_actions.js
├── components
│ ├── app.jsx
│ ├── auth_form
│ │ ├── auth_form.jsx
│ │ └── auth_form_container.js
│ ├── follow_user
│ │ └── follow_user.jsx
│ ├── home
│ │ ├── home.jsx
│ │ └── home_container.js
│ ├── home_feed
│ │ ├── home_feed.jsx
│ │ └── home_feed_item.jsx
│ ├── home_nav
│ │ ├── home_nav.jsx
│ │ └── home_nav_container.js
│ ├── input_nav
│ │ ├── input_nav.jsx
│ │ └── input_nav_container.js
│ ├── interior_nav
│ │ ├── interior_nav.jsx
│ │ └── interior_nav_container.js
│ ├── loading_icon
│ │ └── loading_icon.jsx
│ ├── mixed_feed
│ │ ├── mixed_feed.jsx
│ │ └── mixed_feed_item.jsx
│ ├── publish_options
│ │ └── publish_options.jsx
│ ├── response_input
│ │ ├── response_input.jsx
│ │ └── response_input_container.js
│ ├── response_section
│ │ ├── response.jsx
│ │ ├── response_section.jsx
│ │ └── response_section_container.js
│ ├── root.jsx
│ ├── signup_form
│ │ ├── signup_form.jsx
│ │ └── signup_form_container.js
│ ├── stories_feed
│ │ ├── stories_feed.jsx
│ │ └── stories_feed_item.jsx
│ ├── story
│ │ ├── story.jsx
│ │ └── story_container.js
│ ├── story_input
│ │ ├── story_input.jsx
│ │ └── story_input_container.js
│ ├── story_sidebar
│ │ └── story_sidebar.jsx
│ └── user_profile
│ │ ├── following.jsx
│ │ ├── latest.jsx
│ │ ├── recommended.jsx
│ │ ├── user_profile.jsx
│ │ └── user_profile_container.js
├── message_entry.jsx
├── reducers
│ ├── followings_reducer.js
│ ├── likes_reducer.js
│ ├── loading_reducer.js
│ ├── responses_reducer.js
│ ├── root_reducer.js
│ ├── sessions_reducer.js
│ ├── stories_reducer.js
│ ├── topics_reducer.js
│ └── users_reducer.js
├── store
│ └── store.js
└── util
│ ├── following_api_util.js
│ ├── like_api_util.js
│ ├── m_util.js
│ ├── response_api_util.js
│ ├── session_api_util.js
│ ├── story_api_util.js
│ ├── topic_api_util.js
│ └── user_api_util.js
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── log
└── .keep
├── package.json
├── public
├── 404.html
├── 422.html
├── 500.html
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── favicon.ico
└── robots.txt
├── test
├── controllers
│ ├── .keep
│ ├── sessions_controller_test.rb
│ └── users_controller_test.rb
├── fixtures
│ ├── .keep
│ ├── files
│ │ └── .keep
│ ├── followings.yml
│ ├── likes.yml
│ ├── responses.yml
│ ├── stories.yml
│ └── users.yml
├── helpers
│ └── .keep
├── integration
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── following_test.rb
│ ├── like_test.rb
│ ├── response_test.rb
│ ├── story_test.rb
│ └── user_test.rb
└── test_helper.rb
├── vendor
└── assets
│ ├── javascripts
│ └── .keep
│ └── stylesheets
│ └── .keep
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bundle.js
3 | bundle.js*
4 | bundle.js.map
5 | .byebug_history
6 | .DS_Store
7 | npm-debug.log
8 |
9 | # Ignore all logfiles and tempfiles.
10 | /log/*
11 | !/log/.keep
12 | /tmp
13 |
14 | # Ignore application configuration
15 | /config/application.yml
16 |
17 | # Ignore local terminal color profile
18 | .bash_profile
19 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | git_source(:github) do |repo_name|
4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
5 | "https://github.com/#{repo_name}.git"
6 | end
7 |
8 |
9 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
10 | gem 'rails', '~> 5.0.2'
11 | # Use postgresql as the database for Active Record
12 | gem 'pg', '~> 0.18'
13 | # Use Puma as the app server
14 | gem 'puma', '~> 3.0'
15 | # Use SCSS for stylesheets
16 | gem 'sass-rails', '~> 5.0'
17 | # Use Uglifier as compressor for JavaScript assets
18 | gem 'uglifier', '>= 1.3.0'
19 | # Use CoffeeScript for .coffee assets and views
20 | gem 'coffee-rails', '~> 4.2'
21 | # See https://github.com/rails/execjs#readme for more supported runtimes
22 | # gem 'therubyracer', platforms: :ruby
23 |
24 | # Use jquery as the JavaScript library
25 | gem 'jquery-rails'
26 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
27 | gem 'jbuilder', '~> 2.5'
28 | # Use Redis adapter to run Action Cable in production
29 | # gem 'redis', '~> 3.0'
30 | # Use ActiveModel has_secure_password
31 | gem 'bcrypt', '~> 3.1.7'
32 |
33 | gem 'paperclip', '~> 5.0.0'
34 | gem 'figaro'
35 | gem 'aws-sdk', '>= 2.0'
36 |
37 | # Use Capistrano for deployment
38 | # gem 'capistrano-rails', group: :development
39 |
40 | group :development, :test do
41 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
42 | gem 'byebug', platform: :mri
43 |
44 | gem 'pry-rails'
45 | gem 'annotate'
46 | gem 'better_errors'
47 | gem 'binding_of_caller'
48 | gem 'faker'
49 | gem 'seed_dump'
50 | end
51 |
52 | group :development do
53 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
54 | gem 'web-console', '>= 3.3.0'
55 | gem 'listen', '~> 3.0.5'
56 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
57 | gem 'spring'
58 | gem 'spring-watcher-listen', '~> 2.0.0'
59 |
60 | end
61 |
62 | group :production do
63 | gem 'rails_12factor'
64 | end
65 |
66 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
67 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Message
2 |
3 | [Message live][app]
4 |
5 | [app]: http://www.message-medium.net/
6 |
7 | 
8 |
9 | **Message** is a full-stack single-page web application inspired by the blog/news site Medium. It utilizes Ruby on Rails (back-end), PostgreSQL (database), and React.js/Redux (front-end). The goal of Message is to provide an elegant and inviting experience for both the reading and writing of stories.
10 |
11 | ## Features & Implementation
12 |
13 | ### Writing and editing stories and responses
14 |
15 | 
16 |
17 | Users may write their own stories via a spare, clean interface - the almost-entirely white page is meant to emulate the feeling of a blank sheet of paper. The more-complex functionality only becomes evident after some interaction: image uploading and linked topics via database associations, and rich text editing using tools adapted from the QuillJS library. In order to achieve the desired experience, I had to heavily edit the Quill interface. The database stores stories in specially-formatted HTML, which can then be rendered via React.
18 |
19 | 
20 |
21 | Users may leave responses on stories or on other responses. The chain of responses (sorted by time and by comment thread) is managed through JavaScript algorithms; using the Redux architecture, users may seamlessly leave responses or edit their own responses.
22 |
23 | Below is an initial wireframe drawing of the individual story page (note that the sidebar component is used to display that story's likes):
24 |
25 | 
26 |
27 | ### Follows
28 |
29 | 
30 |
31 | Users may 'follow' other users by clicking on a modular React component. In addition to the user's information, each user profile page contains three feeds: stories and responses written by that user, stories and responses which have been liked by that user, and stories and responses by users followed by that user. This information is quickly retrieved from the server thanks to a series of associations between multiple database tables (`users`, `stories`, `responses`, `likes`, `follows`, and `followings`). Users may view these feeds by clicking on a custom SVG icon menu.
32 |
33 | Below is an initial wireframe drawing of the user profile page:
34 |
35 | 
36 |
37 | ### Likes
38 |
39 | Through the Rails backend, stories are associated with topics and can be 'liked' by users. Database associations allow for a record of all stories and responses that any user has liked. By comparing data from the `likes` table with user information, various React components can detect whether or not the logged-in user has liked a story or response, updating their styles accordingly.
40 |
41 | The goal was to seamlessly integrate the experience of 'liking' into the site, i.e. by showing a story's 'likes' in a sidebar that appears and disappears according to the user's position in the story (via JavaScript and CSS).
42 |
43 | ## Future Directions for the Project
44 |
45 | In addition to the features already implemented, I plan to continue work on this project.
46 |
47 | ### Search
48 |
49 | By utilizing the pg-search Ruby gem, I can integrate text-based search into the site.
50 |
51 | ### Highlights
52 |
53 | Medium allows users to highlight sections of stories that they particularly like. It would be possible to achieve a similar functionality on Message by storing information about each highlighted passage in the database and rendering the highlights via CSS.
54 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../javascripts .js
3 | //= link_directory ../stylesheets .css
4 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/bad_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/bad_logo.png
--------------------------------------------------------------------------------
/app/assets/images/first_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/first_logo.png
--------------------------------------------------------------------------------
/app/assets/images/fluffy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/fluffy.jpg
--------------------------------------------------------------------------------
/app/assets/images/little_camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/little_camera.png
--------------------------------------------------------------------------------
/app/assets/images/mag-glass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/mag-glass.png
--------------------------------------------------------------------------------
/app/assets/images/missing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/missing.png
--------------------------------------------------------------------------------
/app/assets/images/story_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/images/story_default.png
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require_tree .
16 |
--------------------------------------------------------------------------------
/app/assets/javascripts/cable.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the rails generate channel command.
3 | //
4 | //= require action_cable
5 | //= require_self
6 | //= require_tree ./channels
7 |
8 | (function() {
9 | this.App || (this.App = {});
10 |
11 | App.cable = ActionCable.createConsumer();
12 |
13 | }).call(this);
14 |
--------------------------------------------------------------------------------
/app/assets/javascripts/channels/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/assets/javascripts/channels/.keep
--------------------------------------------------------------------------------
/app/assets/javascripts/sessions.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://coffeescript.org/
4 |
--------------------------------------------------------------------------------
/app/assets/javascripts/users.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://coffeescript.org/
4 |
--------------------------------------------------------------------------------
/app/assets/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/bottom.css:
--------------------------------------------------------------------------------
1 | .bottom{
2 | width: 100%;
3 | height: 52px;
4 | background-color:white;
5 | box-shadow: 0 2px 2px 3px rgba(0,0,0,.1);
6 | text-align: center;
7 | }
8 |
9 | .bottom img{
10 | height: 28px;
11 | padding-top: 14px;
12 | }
13 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/follow.css:
--------------------------------------------------------------------------------
1 | .followButtonContainer{
2 | width: 200px;
3 | position: relative;
4 | right: 180px;
5 | /*right: 110px;*/
6 | right: -5px;
7 | top: 30px;
8 | height: 40px;
9 | }
10 |
11 | .followButton-follow {
12 | border: 1px solid #e1bd5b;
13 | border-radius: 20px;
14 | height: 25px;
15 | text-align: center;
16 | width: 110px;
17 | padding-top: 6px;
18 | letter-spacing: .06em;
19 | color: #e1bd5b;
20 | cursor: pointer;
21 | background-color: white;
22 | transition: background-color 300ms;
23 | }
24 |
25 | .followButton-follow:hover {
26 | background-color: #e1bd5b;
27 | color: white;
28 | }
29 |
30 | .followButton-follow a:hover {
31 | color: white;
32 | }
33 |
34 | .followButton-unfollow {
35 | border: 1px solid #e1bd5b;
36 | border-radius: 20px;
37 | height: 25px;
38 | text-align: center;
39 | width: 110px;
40 | padding-top: 6px;
41 | letter-spacing: .06em;
42 | background-color: #e1bd5b;
43 | color: white;
44 | cursor: pointer;
45 | transition: background-color 300ms;
46 | }
47 |
48 | .followButton-unfollow:hover {
49 | background-color: white;
50 | color: #e1bd5b;
51 | }
52 |
53 | .followButton-unfollow a:hover {
54 | color: #e1bd5b;
55 | }
56 |
57 | .followAppear-appear {
58 | opacity: 0.01;
59 | }
60 |
61 | .followAppear-appear.followAppear-appear-active {
62 | opacity: 1;
63 | transition: opacity 1s ease-in;
64 | }
65 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/forms.css:
--------------------------------------------------------------------------------
1 | .errors-list {
2 | list-style: none;
3 | font-size: 12px;
4 | letter-spacing: 1px;
5 | color: #a50000;
6 | justify-content: center;
7 | padding: 0px;
8 | }
9 |
10 | .errors-list-response-input {
11 | list-style: none;
12 | font-size: 12px;
13 | letter-spacing: 1px;
14 | color: #a50000;
15 | justify-content: center;
16 | padding: 0px;
17 | padding-top: 15px;
18 | margin-bottom: 0px;
19 | padding-left: 4px;
20 | }
21 |
22 | .errors-list li {
23 | display: block;
24 | margin: 0;
25 | padding: 0;
26 | padding-top: 4px;
27 | }
28 |
29 | .auth-form {
30 | background-color: white;
31 | position: fixed;
32 | top: 60px;
33 | width: 400px;
34 | left: 50%;
35 | transform: translateX(-50%);
36 | z-index: 201;
37 | box-shadow: 0 2px 2px -2px rgba(0,0,0,.15);
38 | text-align: center;
39 | border-radius: 5px;
40 | }
41 |
42 | .inner-auth {
43 | width: 70%;
44 | margin: auto;
45 | text-align: left;
46 | padding-left: 21px;
47 | color: #999;
48 | font-size: 13px;
49 | letter-spacing: 1.2px;
50 | margin-top: 12px;
51 | position: relative;
52 | }
53 |
54 | .inner-auth input {
55 | background-color: white;
56 | border: 0px;
57 | border-bottom: 1px solid #aaa;
58 | width: 90%;
59 | height: 20px;
60 | outline: none;
61 | color: #999;
62 | }
63 |
64 | .inner_auth input[type='text']:focus {
65 | }
66 |
67 | .inner-auth input[type='submit'] {
68 | margin-top: 18px;
69 | border: 0px;
70 | border-bottom: 1px solid #aaa;
71 | width: 50%;
72 | position: relative;
73 | left: 20%;
74 | color: #777;
75 | cursor: pointer;
76 | }
77 |
78 | .inner-auth input[type='submit']:active {
79 | background-color: bisque;
80 | }
81 |
82 | .small-message {
83 | font-size: 12px;
84 | letter-spacing:.05em;
85 | }
86 |
87 | .signup-form {
88 | background-color: white;
89 | position: fixed;
90 | top: 60px;
91 | width: 400px;
92 | left: 50%;
93 | transform: translateX(-50%);
94 | z-index: 202;
95 | box-shadow: 0 2px 2px -2px rgba(0,0,0,.15);
96 | text-align: center;
97 | border-radius: 5px;
98 | }
99 |
100 | .inner-signup {
101 | width: 70%;
102 | margin: auto;
103 | text-align: left;
104 | padding-left: 20px;
105 | color: #999;
106 | font-size: 13px;
107 | letter-spacing: 1.2px;
108 | margin-top: 12px;
109 | position: relative;
110 | }
111 |
112 | .inner-signup input {
113 | background-color: white;
114 | border: 0px;
115 | border-bottom: 1px solid #aaa;
116 | width: 90%;
117 | height: 20px;
118 | margin-bottom: 18px;
119 | outline: none;
120 | color: #999;
121 | }
122 |
123 | .inner-signup img {
124 | float: right;
125 | margin-right: 30px;
126 | width: 50px;
127 | }
128 |
129 | .image-upload {
130 | background-color: bisque;
131 | display:inline-block;
132 | margin-top: 6px;
133 | cursor: pointer;
134 | font-size: 11px;
135 | }
136 |
137 | .image-upload:active {
138 | background-color: sandybrown;
139 | }
140 |
141 | .hidden-upload {
142 | display: none;
143 | }
144 |
145 | .inner-signup input[type='submit'] {
146 | margin-top: 8px;
147 | border: 0px;
148 | border-bottom: 1px solid #aaa;
149 | width: 50%;
150 | position: relative;
151 | left: 20%;
152 | color: #777;
153 | cursor: pointer;
154 | margin-bottom: 4px;
155 | }
156 |
157 | .inner-signup input[type='submit']:active {
158 | background-color: bisque;
159 | }
160 |
161 | .form-wrapper {
162 | float: left;
163 | position: fixed;
164 | top: 0;
165 | width: 100%;
166 | height: 100%;
167 | background-color: rgba(70, 70, 70, 0.3);
168 | z-index: 200;
169 | opacity: .1;
170 | }
171 |
172 | .authGuestLink{
173 | margin-top:6px;
174 | margin-bottom:3px;
175 | color: #e6a822;
176 | }
177 |
178 | .authGuestLinkColor a{
179 | color: #e6a822;
180 | }
181 |
182 | .authGuestLinkColor a:link{
183 | color: #e6a822;
184 | }
185 |
186 | .authGuestLinkColor a:visited{
187 | color: #e6a822;
188 | }
189 |
190 | .authGuestLinkColor a:active{
191 | color: #e6a822;
192 | }
193 | .authGuestLinkColor a:hover{
194 | color: #e1bd5b;
195 | }
196 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/loader.css:
--------------------------------------------------------------------------------
1 | .loader-container {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | /*background-color:yellow;*/
8 | /*opacity: .5;*/
9 | }
10 |
11 | .loader {
12 | color: #e1bd5b;
13 | opacity: .5;
14 | /*color: #ffffff;*/
15 | font-size: 20px;
16 | margin: 100px auto;
17 | width: 1em;
18 | height: 1em;
19 | border-radius: 50%;
20 | position: relative;
21 | text-indent: -9999em;
22 | -webkit-animation: load4 1.3s infinite linear;
23 | animation: load4 1.3s infinite linear;
24 | -webkit-transform: translateZ(0);
25 | -ms-transform: translateZ(0);
26 | transform: translateZ(0);
27 | }
28 | @-webkit-keyframes load4 {
29 | 0%,
30 | 100% {
31 | box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
32 | }
33 | 12.5% {
34 | box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
35 | }
36 | 25% {
37 | box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
38 | }
39 | 37.5% {
40 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
41 | }
42 | 50% {
43 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
44 | }
45 | 62.5% {
46 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
47 | }
48 | 75% {
49 | box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;
50 | }
51 | 87.5% {
52 | box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
53 | }
54 | }
55 | @keyframes load4 {
56 | 0%,
57 | 100% {
58 | box-shadow: 0 -3em 0 0.2em, 2em -2em 0 0em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 0;
59 | }
60 | 12.5% {
61 | box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em, 3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
62 | }
63 | 25% {
64 | box-shadow: 0 -3em 0 -0.5em, 2em -2em 0 0, 3em 0 0 0.2em, 2em 2em 0 0, 0 3em 0 -1em, -2em 2em 0 -1em, -3em 0 0 -1em, -2em -2em 0 -1em;
65 | }
66 | 37.5% {
67 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em, -2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
68 | }
69 | 50% {
70 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em, -2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
71 | }
72 | 62.5% {
73 | box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0, -2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
74 | }
75 | 75% {
76 | box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em, 3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;
77 | }
78 | 87.5% {
79 | box-shadow: 0em -3em 0 0, 2em -2em 0 -1em, 3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em, -2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/profile.css:
--------------------------------------------------------------------------------
1 | .profileAuthorPhotoContainer {
2 | margin-top: 12px;
3 | margin-left: 12px;
4 | margin-right: 18px;
5 | width: 120px;
6 | height: 120px;
7 | overflow: hidden;
8 | border-radius: 50%;
9 | opacity: .8;
10 | position: relative;
11 | padding: 0px;
12 | margin: 0 auto;
13 | }
14 |
15 | .profileAuthorPhotoContainer img {
16 | max-width: 122px;
17 | }
18 |
19 | .profile-userName{
20 | margin: 0 auto;
21 | width: 500px;
22 | text-align: center;
23 | font-size: 25px;
24 | letter-spacing: 1.3px;
25 | color: #999;
26 | padding-top: 10px;
27 | }
28 |
29 | .orangeBack{
30 | background-color: bisque;
31 | padding-left:6px;
32 | padding-right:6px;
33 | }
34 |
35 | .profile-userBio{
36 | margin: 0 auto;
37 | width: 500px;
38 | text-align: center;
39 | color: #888;
40 | padding-top: 8px;
41 | letter-spacing: .6px;
42 | border-bottom: 1px solid #ddd;
43 | padding-bottom: 20px;
44 | }
45 |
46 | .lighterText{
47 | opacity: .7;
48 | margin-top: 6px;
49 | line-height: 26px;
50 | letter-spacing: .6px;
51 | }
52 |
53 |
54 | .mixedItem-story{
55 | width: 800px;
56 | margin: 0 auto;
57 | box-shadow: 0 1px 4px rgba(0,0,0,.25);
58 | display:flex;
59 | justify-content: space-between;
60 | position:relative;
61 | margin-bottom: 30px;
62 | margin-top: 30px;
63 | }
64 |
65 | .mixedItem-storyImage{
66 | width: 420px;
67 | overflow:hidden;
68 | text-align: center;
69 | margin-left: 20px;
70 | margin-right: 20px;
71 | padding-top: 20px;
72 | padding-bottom: 20px;
73 | /*border-radius: 4px;*/
74 | }
75 |
76 | .mixedItem-storyImage img{
77 | /*clip: rect(0px,420px,240px,0px);*/
78 | /*border-radius: 4px;*/
79 | height: 240px;
80 |
81 | }
82 |
83 | .mixedItem-storyTitle{
84 | padding-top: 26px;
85 | font-size: 28px;
86 | font-weight: 700;
87 | width: 320px;
88 | line-height:31px;
89 | }
90 |
91 |
92 | .mixedItem-authorPhotoContainer {
93 | margin-top: 12px;
94 | margin-left: 12px;
95 | margin-right: 18px;
96 | height: 32px;
97 | overflow: hidden;
98 | border-radius: 50%;
99 | opacity: .8;
100 | position: relative;
101 | padding: 0px;
102 | min-width: 65px;
103 | max-width: 65px;
104 | min-height: 65px;
105 | }
106 |
107 | .mixedItem-authorPhotoContainer img{
108 | display: inline;
109 | margin: 0 auto;
110 | min-width: 65px;
111 | height: 65px;
112 | }
113 |
114 | .mixedItem-storyAuthor{
115 | display: flex;
116 | position: absolute;
117 | bottom: 32px;
118 | }
119 |
120 | .mixedItem-authorName{
121 | padding-top: 26px;
122 | color: #999;
123 | }
124 |
125 | .mixedItem-storyDate{
126 | color: #999;
127 | padding-top: 4px;
128 | font-size: 12px;
129 | }
130 |
131 | .mixedItem-storyDescription{
132 | padding-top: 5px;
133 | color: #777;
134 | width: 330px;
135 | }
136 |
137 |
138 | .profileIconsHolder{
139 | display: flex;
140 | justify-content: space-between;
141 | width:400px;
142 | margin:auto;
143 | }
144 |
145 | .iconHolder{
146 | color:#bbb;
147 | text-transform: uppercase;
148 | font-size: 13px;
149 | letter-spacing: 1.3px;
150 | width: 120px;
151 | /*background-color: yellow;*/
152 | text-align: center;
153 | }
154 |
155 | .profile-icon{
156 | width:60px;
157 | margin: 0 auto;
158 | padding-bottom: 6px;
159 | }
160 |
161 | .profileFeedTrans-enter {
162 | opacity: 0.01;
163 | /*height: 400px;*/
164 | /*background-color: purple;*/
165 | }
166 |
167 | .profileFeedTrans-enter.profileFeedTrans-enter-active {
168 | opacity: 1;
169 | /*height: 700px;*/
170 | transition: all 200ms ease-in;
171 | /*background-color: pink;*/
172 | }
173 |
174 | .profileFeedTrans-leave {
175 | opacity: 1;
176 | /*height: 700px;*/
177 | /*background-color: yellow;*/
178 | }
179 |
180 | .profileFeedTrans-leave.profileFeedTrans-leave-active {
181 | opacity: 0;
182 | /*height: 400px;*/
183 | transition: all 200ms ease-in;
184 | /*background-color: blue;*/
185 | }
186 |
187 | .profileFollow{
188 | display: flex;
189 | justify-content: center;
190 | }
191 |
192 | .profileFollow .followButtonContainer{
193 | width:115px;
194 | top:10px
195 | }
196 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/publish_options.css:
--------------------------------------------------------------------------------
1 | .publishOptionsBox{
2 | background-color: white;
3 | border: 1px solid #eaeaea;
4 | box-shadow: 0 2px 2px -2px rgba(0,0,0,.15);
5 | text-align: center;
6 | position: absolute;
7 | left: 50%;
8 | /*margin-left: 320px;*/
9 | margin-left: 220px;
10 | top: 54px;
11 | width: 200px;
12 | padding-top: 15px;
13 | padding-bottom: 15px;
14 | z-index: 300;
15 | }
16 |
17 | .publishOptionsBox form{
18 | text-align: center;
19 | }
20 |
21 | .topicSelect{}
22 |
23 | .topicButtons{
24 | text-align: center;
25 | padding-left: 0px;
26 | }
27 |
28 | /*ul {
29 | list-style: none;
30 | }*/
31 |
32 | .topicButtons li {
33 | display: block;
34 | margin-left:-13px;
35 | text-transform:uppercase;
36 | padding-top:3px;
37 | padding-bottom:3px;
38 | font-size: 16px;
39 | letter-spacing:1.4px;
40 | }
41 | .topicButtons input {
42 | visibility:hidden;
43 | }
44 | .topicButtons label {
45 | cursor: pointer;
46 | padding-left:6px;
47 | padding-right:6px;
48 | }
49 | .topicButtons input:checked + label {
50 | background: bisque;
51 | }
52 |
53 | .form-wrapper-2 {
54 | position: fixed;
55 | top: 0;
56 | left: 0;
57 | width: 100%;
58 | height: 100%;
59 | background-color: rgba(1, 1, 1, 0);
60 | z-index: 200;
61 | opacity: 1;
62 | /*transition: opacity 1s;*/
63 | }
64 |
65 | .form-wrapper-3 {
66 | position: fixed;
67 | top: 0;
68 | left: 0;
69 | width: 100%;
70 | height: 100%;
71 | background-color: rgba(1, 1, 1, 0);
72 | z-index: 201;
73 | display: none;
74 | opacity: 0;
75 | /*transition: opacity 1s;*/
76 | }
77 |
78 | .moveUp{
79 | margin-top:-10px;
80 | }
81 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/reset.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'PT Sans', 'Lucida Sans', 'Arial', sans-serif;
3 | word-spacing: -.07em;
4 | /*letter-spacing: .2em;*/
5 | margin: 0px;
6 | color: #555;
7 | z-index: 1;
8 | }
9 |
10 | ul {
11 | list-style: none;
12 | }
13 |
14 | div {
15 | width: 100%;
16 | /*padding: 8px;*/
17 | }
18 |
19 | a:link {
20 | color: #777;
21 | }
22 |
23 | a:visited {
24 | color: #777;
25 | }
26 |
27 | a:hover {
28 | color: #bbb;
29 | }
30 |
31 | a:active {
32 | color: #555;
33 | }
34 |
35 | a {
36 | text-decoration: none;
37 | }
38 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sessions.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the sessions 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/stories_feed.css:
--------------------------------------------------------------------------------
1 | .storiesFeed {
2 | margin: auto;
3 | }
4 |
5 | .homeFeed {
6 | /*background-color: pink;*/
7 | margin: 0 auto;
8 | width: 1000px;
9 | }
10 |
11 | .homeFeed img{
12 | width: 100%;
13 | position: absolute;
14 | display: inline-block;
15 | }
16 |
17 | .storyInfoSection{
18 | margin-top: 170px;
19 | z-index: 10;
20 | margin-right:10px;
21 | }
22 |
23 | .homeFeedItem{
24 | margin-top: 30px;
25 | }
26 |
27 | .groupOfFour {
28 | margin-bottom: 20px;
29 | }
30 |
31 | .groupOfFive {
32 | /*background-color: purple;*/
33 | }
34 |
35 | .groupOfFour div {
36 | /*background-color: green;*/
37 | width: 333px;
38 | display: inline-block;
39 | height: 350px;
40 | vertical-align: top;
41 | position: relative;
42 | /*padding-bottom: 20px;*/
43 | }
44 |
45 | .groupOfFive div {
46 | /*background-color: green;*/
47 | width: 333px;
48 | display: inline-block;
49 | height: 350px;
50 | vertical-align: top;
51 | position: relative;
52 | padding-bottom: 20px;
53 | padding-top: 30px;
54 | }
55 |
56 | .groupOfFour div:nth-child(1) {
57 | /*background-color: turquoise;*/
58 | width: 1000px;
59 | display: block;
60 | }
61 |
62 | .groupOfFour div img {
63 | clip: rect(0px,320px,170px,0px);
64 | }
65 |
66 | .groupOfFour div:nth-child(1) img {
67 | width: 655px;
68 | clip: rect(0px,655px,355px,0px);
69 | }
70 |
71 | .groupOfFour div:nth-child(1) .storyInfoSection {
72 | /*color: red;*/
73 | display: inline-block;
74 | margin-top: -20px;
75 | margin-left: 673px;
76 | width: 300px;
77 | padding-top: 10px
78 | }
79 |
80 | .groupOfFour div:nth-child(1) .homeFeedItemHeadline {
81 | font-weight: 700;
82 | color: #666;
83 | font-size: 34px;
84 | line-height: 1.1em;
85 | }
86 |
87 | .groupOfFive div:nth-child(-n+2) {
88 | /*background-color: orange;*/
89 | width: 500px;
90 | display: inline-block;
91 | margin-bottom: 40px;
92 | margin-top: -20px;
93 | padding-top: 30px;
94 | }
95 |
96 | .groupOfFive div:nth-child(-n+2) .storyInfoSection {
97 | margin-top: 270px;
98 | width: 450px;
99 | }
100 |
101 | .groupOfFive div:nth-child(-n+2) .homeFeedItemHeadline {
102 | font-weight: 700;
103 | color: #666;
104 | font-size: 30px;
105 | line-height: 1.1em;
106 | }
107 |
108 | .groupOfFive div img {
109 | clip: rect(0px,320px,170px,0px);
110 | }
111 |
112 | .groupOfFive div:nth-child(-n+2) img {
113 | clip: rect(0px,485px,270px,0px);
114 | }
115 |
116 | .homeFeedItemHeadline {
117 | z-index: 20;
118 | font-family: 'PT Sans', 'Lucida Sans', sans-serif;
119 | font-weight: 700;
120 | font-size: 22px;
121 | margin-top: -8px;
122 | }
123 |
124 | .homeFeed a:link {
125 | color: #444;
126 | }
127 |
128 | .homeFeed a:visited {
129 | color: #444;
130 | }
131 |
132 | .homeFeed a:hover {
133 | color: #777;
134 | }
135 |
136 |
137 | .homeFeed a:active {
138 | color: #444;
139 | }
140 |
141 | .homeFeedItemAuthor{
142 | color: #888;
143 | padding-top: 11px;
144 | }
145 |
146 | .homeFeedItemDate{
147 | color: #888;
148 | padding-top: 4px;
149 | font-size: 12px;
150 | }
151 |
152 |
153 | /*.homeFeedAvatar img{
154 | position: static;
155 | display: inline;
156 | width: 20px;
157 | }
158 |
159 | .homeFeedAvatar{
160 | width: 20px;
161 | display: inline;
162 | overflow: hidden;
163 | }*/
164 |
165 |
166 | /*.homeFeed div:nth-child(1) {
167 | background-color: yellow;
168 | margin: 0 auto;
169 | width: 1000px;
170 | }
171 |
172 | .homeFeed div:nth-child(2) {
173 | background-color: purple;
174 | margin: 0 auto;
175 | width: 1000px;
176 | }*/
177 |
178 | .homeFeedItemDescription{
179 | margin-right:10px;
180 | padding-top:5px;
181 | }
182 |
183 | .lightener{
184 | opacity:.5;
185 | }
186 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/story.css:
--------------------------------------------------------------------------------
1 | .mainContainer {
2 | width: 100%;
3 | background-color: white;
4 | }
5 |
6 | .storyContentContainer {
7 | background-color: white;
8 | width:700px;
9 | display: flex;
10 | justify-content: center;
11 | margin: auto;
12 | }
13 |
14 | .storyContent {
15 | width: 700px;
16 | color: rgba(0,0,0,.8);
17 | }
18 |
19 | .storyInfo {
20 | display: flex;
21 | justify-content: left;
22 | width: 80%;
23 | }
24 |
25 | .authorInfoContainer {
26 | padding-top: 16px;
27 | font-size: 16px;
28 | width: 180px;
29 | }
30 |
31 | .storyInfo .smallInfo {
32 | font-size: 13.5px;
33 | color: rgba(0,0,0,.5);
34 | }
35 |
36 | .storyInfo .smallInfo .dateInfo {
37 | font-size: 13.5px;
38 | color: rgba(0,0,0,.35);
39 | }
40 |
41 | .authorPhotoContainer {
42 | margin-top: 12px;
43 | margin-left: 12px;
44 | margin-right: 18px;
45 | height: 32px;
46 | overflow: hidden;
47 | border-radius: 50%;
48 | opacity: .8;
49 | position: relative;
50 | padding: 0px;
51 | min-width: 65px;
52 | max-width: 65px;
53 | min-height: 65px;
54 | }
55 |
56 | .authorPhotoContainer img {
57 | display: inline;
58 | margin: 0 auto;
59 | min-width: 65px;
60 | height: 65px;
61 | }
62 |
63 |
64 | .storyHead {
65 | background-color: white;
66 | }
67 |
68 | .storyTitle {
69 | }
70 |
71 | .storyTitle h1 {
72 | font-family: 'PT Sans Bold', 'PT Sans', 'Lucida Sans';
73 | font-size: 36px;
74 | font-weight: bold;
75 | color: rgba(0,0,0,.8);
76 | line-height: 1.15em;
77 | margin-bottom: 8px;
78 | }
79 |
80 | .storyDescription h3 {
81 | font-family: 'PT Sans', 'Lucida Sans';
82 | font-weight: normal;
83 | font-size: 24px;
84 | color: rgba(0,0,0,.5);
85 | margin-top: 0px;
86 | margin-bottom: 40px;
87 | }
88 |
89 | .storyMainImageContainer {
90 | width: 700px;
91 | max-height: 430px;
92 | overflow: hidden;
93 | margin-bottom: 30px;
94 | }
95 |
96 | .storyMainImageContainer img {
97 | width: 100%;
98 | }
99 |
100 | .storyBody {
101 | background-color: white;
102 | font-family: 'Amiri', serif;
103 | }
104 |
105 | .editThis {
106 | font-size: 16px;
107 | opacity: .6;
108 | margin-top: 25px;
109 | font-style: italic;
110 | }
111 |
112 | .storyBody p {
113 | font-family: 'Amiri', serif;
114 | font-size: 22px;
115 | line-height: 1.8em;
116 | letter-spacing: .02em;
117 | word-spacing: -.08em;
118 | }
119 |
120 | .storyBody p strong {
121 | text-shadow: .3px 0;
122 | }
123 |
124 | .storyBody h2 {
125 | font-size: 1.8em;
126 | font-weight: bold;
127 | }
128 |
129 | .storyBody h3 {
130 | font-size: 1.6em;
131 | font-weight: bold;
132 | }
133 |
134 | .storyBody blockquote {
135 | color: rgba(0,0,0,.5);
136 | font-size: 28px;
137 | line-height: 1.3em;
138 | }
139 |
140 | .storyBody p:first-child:first-letter {
141 | color: rgba(0,0,0,.8);
142 | font-family: 'PT Sans Bold', 'PT Sans', 'Lucida Sans', sans-serif;
143 | /*text-shadow: .7px 0;*/
144 | float: left;
145 | font-weight: bold;
146 | font-size: 60px;
147 | line-height: 60px;
148 | padding-top: 10px;
149 | padding-right: 7px;
150 | padding-left: 0px;
151 | }
152 |
153 | .storyBody hr {
154 | width: 90%;
155 | border-top: 1px solid rgba(0,0,0,.15);
156 | margin-top: 20px;
157 | margin-bottom: 24px;
158 | }
159 |
160 | .storyTopic {
161 | font-family: 'PT Sans';
162 | text-transform: uppercase;
163 | text-align: center;
164 | letter-spacing: .8px;
165 | opacity: .8;
166 | border-top: 1px solid #ccc;
167 | padding-top: 20px;
168 | }
169 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/story_input.css:
--------------------------------------------------------------------------------
1 | .mainEditor {
2 | background-color: white;
3 | width: 700px;
4 | }
5 |
6 | .inputContentContainer {
7 | background-color: white;
8 | width:700px;
9 | display: flex;
10 | justify-content: center;
11 | margin: auto;
12 | }
13 |
14 | .inputContent {
15 | width: 700px;
16 | color: rgba(0,0,0,.8);
17 | }
18 |
19 | .inputPadder {
20 | padding-left: 12px;
21 | }
22 |
23 | .inputTitle input {
24 | background-color: white;
25 | border: 0px;
26 | border-bottom: 0;
27 | width: 80%;
28 | height: 20px;
29 | outline: none;
30 | margin-bottom: 2px;
31 | box-sizing: border-box;
32 | font-family: 'PT Sans Bold', 'PT Sans', 'Lucida Sans';
33 | font-size: 36px;
34 | font-weight: bold;
35 | color: rgba(0,0,0,.4);
36 | height: 40px;
37 | /*word-break: break-word;*/
38 | }
39 |
40 | .inputTitle input::placeholder {
41 | color: rgba(0,0,0,.2);
42 | }
43 |
44 | .inputTitle input[type='text']:focus {
45 | border-bottom: 1px solid #aaa;
46 | }
47 |
48 | .inputDescription input {
49 | background-color: white;
50 | border: 0px;
51 | border-bottom: 0;
52 | width: 80%;
53 | height: 20px;
54 | outline: none;
55 | font-family: 'PT Sans', 'Lucida Sans';
56 | color: rgba(0,0,0,.4);
57 | font-weight: normal;
58 | font-size: 24px;
59 | margin-bottom: 2px;
60 | box-sizing: border-box;
61 | height: 40px;
62 | }
63 |
64 | .inputDescription input::placeholder {
65 | color: rgba(0,0,0,.2);
66 | }
67 |
68 | .inputDescription input[type='text']:focus {
69 | border-bottom: 1px solid #aaa;
70 | }
71 |
72 | .inputElements {
73 | display: flex;
74 | /*background-color: turquoise;*/
75 | justify-content: left;
76 | margin-bottom: -20px;
77 | }
78 |
79 | .inputPhoto {
80 | display: inline-block;
81 | position: relative;
82 | left: -27px;
83 | top: -35px;
84 | background-color: pink;
85 | width: 25px;
86 | height: 25px;
87 | margin-right: -20px;
88 | margin-bottom: -35px;
89 | float: left;
90 | /*z-index: 100;*/
91 | background-size: 24px;
92 | opacity: .3;
93 | transition: opacity 200ms;
94 | }
95 |
96 | .inputPhoto:hover {
97 | opacity: .4;
98 | }
99 |
100 | .inputPreviewImage {
101 | padding-left: 12px;
102 | display: inline;
103 | /*background-color: yellow;*/
104 | margin-top: -15px;
105 | margin-bottom: 25px;
106 | }
107 |
108 | .inputPreviewImage img {
109 | max-height: 200px;
110 | opacity: .8;
111 | }
112 |
113 | .hidden-input-upload {
114 | /*opacity: 0;*/
115 | display: none;
116 | position: relative;
117 | top: -20px;
118 | margin-bottom: -20px;
119 | /*cursor: pointer;*/
120 | }
121 |
122 | .input-image-upload {
123 | display:inline-block;
124 | opacity: 0;
125 | cursor: pointer;
126 | }
127 |
128 | .errors-list-story-input {
129 | list-style: none;
130 | font-size: 12px;
131 | letter-spacing: 1px;
132 | color: #a50000;
133 | padding: 0px;
134 | padding-top: 15px;
135 | margin-bottom: 0px;
136 | padding-left: 4px;
137 | margin-bottom: 0px;
138 | opacity: .6;
139 | top: -40px;
140 | position: relative;
141 | }
142 |
143 | /*.image-upload:active {
144 | background-color: sandybrown;
145 | }*/
146 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/story_sidebar.css:
--------------------------------------------------------------------------------
1 | .storySidebar{
2 | position: fixed;
3 | top:50;
4 | left:50%;
5 | margin-left:-460px;
6 | width:100px;
7 | }
8 |
9 | .likeHeartDiv{
10 | width:50px;
11 | }
12 |
13 | .likeHeart-canLike{
14 | fill:white;
15 | stroke:#e1bd5b;
16 | stroke-width:.5px;
17 | transition: fill 200ms;
18 | }
19 |
20 | .likeHeart-canLike:hover{
21 | fill:#e1bd5b;
22 | cursor:pointer;
23 | }
24 |
25 | .likeHeart-canUnlike{
26 | fill:#e1bd5b;
27 | stroke:#e1bd5b;
28 | stroke-width:.5px;
29 | transition: fill 200ms;
30 | }
31 |
32 | .likeHeart-canUnlike:hover{
33 | fill:white;
34 | cursor:pointer;
35 | }
36 |
37 | .likeHeart-static{
38 | fill:white;
39 | stroke:#e1bd5b;
40 | stroke-width:.5px;
41 | transition: fill 200ms;
42 | }
43 |
44 | .likeHeart-static-orange{
45 | fill:#e1bd5b;
46 | stroke:#e1bd5b;
47 | stroke-width:.5px;
48 | transition: fill 200ms;
49 | }
50 |
51 | .likeNumber{
52 | color: #999;
53 | text-align: center;
54 | width: 50px;
55 | }
56 |
57 | .sidebarTrans-enter {
58 | opacity: 0.01;
59 | height: 0px;
60 | }
61 |
62 | .sidebarTrans-enter.sidebarTrans-enter-active {
63 | opacity: 1;
64 | height: 160px;
65 | transition: all 100ms ease-in;
66 | }
67 |
68 | .sidebarTrans-leave {
69 | opacity: 1;
70 | height: 160px;
71 | }
72 |
73 | .sidebarTrans-leave.sidebarTrans-leave-active {
74 | opacity: 0;
75 | height: 0px;
76 | transition: all 100ms ease-in;
77 | }
78 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/users.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the users controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/api/followings_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::FollowingsController < ApplicationController
2 |
3 | def index
4 | if params[:followerId]
5 | followings = Following.where(follower_id: params[:followerId])
6 | else params[:followingId]
7 | followings = Following.where(following_id: params[:followingId])
8 | end
9 | @followings = followings
10 | render :index
11 | end
12 |
13 | def show
14 | render :show
15 | end
16 |
17 | def create
18 | @following = Following.new(following_params)
19 | if @following.save
20 | render :show
21 | else
22 | render json: @following.errors.full_messages, status: 422
23 | end
24 | end
25 |
26 | def destroy
27 | @following = Following.where(follower_id: params[:followerId]).where(following_id: params[:followingId]).first
28 | Following.destroy(@following.id)
29 | render :show
30 | end
31 |
32 | private
33 |
34 | def following_params
35 | params.require(:following).permit(:follower_id, :following_id)
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/app/controllers/api/likes_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::LikesController < ApplicationController
2 |
3 | def index
4 | if params[:likerId]
5 | likes = Like.where(liker_id: params[:likerId])
6 | elsif params[:storyId]
7 | likes = Like.where(story_id: params[:storyId])
8 | else
9 | likes = Like.where(response_id: params[:responseId])
10 | end
11 | @likes = likes
12 | render :index
13 | end
14 |
15 | def show
16 | render :show
17 | end
18 |
19 | def create
20 | @like = Like.new(like_params)
21 |
22 | if @like.save
23 | render :show
24 | else
25 | render json: @like.errors.full_messages, status: 422
26 | end
27 | end
28 |
29 | def destroy
30 | if(params[:responseId])
31 | @like = Like.where(liker_id: params[:likerId]).where(response_id: params[:responseId]).first
32 | else
33 | @like = Like.where(liker_id: params[:likerId]).where(story_id: params[:storyId]).first
34 | end
35 | Like.destroy(@like.id)
36 | render :show
37 | end
38 |
39 | private
40 |
41 | def like_params
42 | params.require(:like).permit(:liker_id, :story_id, :response_id)
43 | end
44 |
45 | end
46 |
--------------------------------------------------------------------------------
/app/controllers/api/responses_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::ResponsesController < ApplicationController
2 |
3 | def index
4 | if params[:writerId]
5 | responses = Response.where(writer_id: params[:writerId])
6 | else
7 | responses = Response.where(story_id: params[:storyId])
8 | end
9 | @responses = responses
10 | render :index
11 | end
12 |
13 | def create
14 | @response = Response.new(response_params)
15 | if current_user.id == @response.writer_id
16 | if @response.save
17 | render :show
18 | else
19 | render json: @response.errors.full_messages, status: 422
20 | end
21 | else
22 | render json: 'Invalid response'
23 | end
24 | end
25 |
26 | def show
27 | @response = Response.find(params[:id])
28 | end
29 |
30 | def update
31 | @response = Response.find(params[:this_id])
32 | if current_user.id == @response.writer_id
33 | if @response.update(response_params)
34 | render :show
35 | else
36 | render json: @response.errors.full_messages, status: 422
37 | end
38 | else
39 | render json: 'Invalid edit'
40 | end
41 | end
42 |
43 | def destroy
44 |
45 | end
46 |
47 | private
48 |
49 | def response_params
50 | params.require(:response).permit(:writer_id, :story_id, :in_response_id, :body, :date)
51 | end
52 |
53 | end
54 |
--------------------------------------------------------------------------------
/app/controllers/api/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::SessionsController < ApplicationController
2 |
3 | def create
4 | @user = User.find_by_credentials(
5 | params[:user][:username],params[:user][:password]
6 | )
7 | if @user
8 | login(@user)
9 | render 'api/users/session_user.json.jbuilder'
10 | else
11 | render json: ["Invalid username or password."], status: 401
12 | end
13 | end
14 |
15 | def destroy
16 | if logged_in?
17 | @user = current_user
18 | logout
19 | # rendering basic user data on logout
20 | render 'api/users/session_user.json.jbuilder'
21 | else
22 | render json: ["No one to log out."], status: 404
23 | end
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/app/controllers/api/stories_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::StoriesController < ApplicationController
2 |
3 | def index
4 | # returns title, description, etc. but NOT the body
5 | stories = Story.all.includes(:likes, :likers, :author, :topic)
6 |
7 | if params[:authorId]
8 | stories = stories.where(author_id: params[:authorId])
9 | elsif params[:topicId]
10 | stories = stories.where(topic_id: params[:topicId])
11 | end
12 | @stories = stories
13 | render :index
14 | end
15 |
16 | def create
17 | @story = Story.new(story_params)
18 | if current_user.id == @story.author_id
19 | if @story.save
20 | render :show
21 | else
22 | render json: @story.errors.full_messages, status: 422
23 | end
24 | else
25 | # only if someone's maliciously using the URL
26 | render json: 'Invalid post'
27 | end
28 | end
29 |
30 | def show
31 | @story = Story.includes(:author).find(params[:id])
32 | end
33 |
34 | def update
35 | @story = Story.find(params[:story][:id])
36 | if current_user.id == @story.author_id
37 | if @story.update(story_params)
38 | render :show
39 | else
40 | render json: @story.errors.full_messages, status: 422
41 | end
42 | else
43 | # only if someone's maliciously using the URL
44 | render json: 'Invalid edit'
45 | end
46 | end
47 |
48 | def destroy
49 |
50 | end
51 |
52 | private
53 |
54 | def story_params
55 | params.require(:story).permit(:author_id, :title, :description, :body, :date, :topic_id, :main_image)
56 | end
57 |
58 | end
59 |
--------------------------------------------------------------------------------
/app/controllers/api/topics_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::TopicsController < ApplicationController
2 |
3 | def index
4 | @topics = Topic.all
5 | render :index
6 | end
7 |
8 | end
9 |
--------------------------------------------------------------------------------
/app/controllers/api/users_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::UsersController < ApplicationController
2 |
3 | def create
4 | @user = User.new(user_params)
5 | if @user.save
6 | login(@user)
7 | # note: going to show page, here
8 | render 'api/users/show'
9 | else
10 | render json: @user.errors.full_messages, status: 422
11 | end
12 | end
13 |
14 | def show
15 | @user = User.includes(stories: :author, responses: [:writer, :likes, :likers]).find(params[:id])
16 | render :show
17 | end
18 |
19 | def update
20 |
21 | end
22 |
23 | def destroy
24 |
25 | end
26 |
27 | private
28 |
29 | def user_params
30 | params.require(:user).permit(:username, :password, :email, :name, :bio, :photo)
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 |
4 | helper_method :current_user, :logged_in?
5 |
6 | def current_user
7 | @current_user ||= User.find_by(session_token: session[:session_token])
8 | end
9 |
10 | def logged_in?
11 | ! current_user.nil?
12 | end
13 |
14 | def login(user)
15 | session[:session_token] = user.reset_session_token!
16 | end
17 |
18 | def logout
19 | if current_user
20 | current_user.reset_session_token!
21 | end
22 | session[:session_token] = nil
23 | end
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/static_pages_controller.rb:
--------------------------------------------------------------------------------
1 | class StaticPagesController < ApplicationController
2 |
3 | def root
4 | end
5 |
6 | end
7 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/sessions_helper.rb:
--------------------------------------------------------------------------------
1 | module SessionsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/users_helper.rb:
--------------------------------------------------------------------------------
1 | module UsersHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/following.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: followings
4 | #
5 | # id :integer not null, primary key
6 | # follower_id :integer not null
7 | # following_id :integer not null
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | #
11 |
12 | class Following < ApplicationRecord
13 | validates :follower_id, :following_id, presence: true
14 |
15 | belongs_to :follower,
16 | foreign_key: :follower_id,
17 | class_name: 'User'
18 |
19 | belongs_to :following,
20 | foreign_key: :following_id,
21 | class_name: 'User'
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/app/models/like.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: likes
4 | #
5 | # id :integer not null, primary key
6 | # liker_id :integer not null
7 | # story_id :integer
8 | # response_id :integer
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 |
13 | class Like < ApplicationRecord
14 |
15 | validates :liker_id, presence: true
16 |
17 | belongs_to :liker,
18 | foreign_key: :liker_id,
19 | class_name: 'User'
20 |
21 | belongs_to :story,
22 | foreign_key: :story_id,
23 | class_name: 'Story',
24 | optional: true
25 |
26 | belongs_to :response,
27 | foreign_key: :response_id,
28 | class_name: 'Response',
29 | optional: true
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/app/models/response.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: responses
4 | #
5 | # id :integer not null, primary key
6 | # writer_id :integer not null
7 | # story_id :integer
8 | # body :text not null
9 | # date :string
10 | # in_response_id :integer
11 | # created_at :datetime not null
12 | # updated_at :datetime not null
13 | #
14 |
15 | class Response < ApplicationRecord
16 |
17 | validates :writer_id, :body, presence: true
18 |
19 | belongs_to :writer,
20 | foreign_key: :writer_id,
21 | class_name: 'User'
22 |
23 | belongs_to :story,
24 | foreign_key: :story_id,
25 | class_name: 'Story',
26 | optional: true
27 |
28 | has_many :child_responses,
29 | foreign_key: :in_response_id,
30 | class_name: 'Response'
31 |
32 | belongs_to :parent_response,
33 | foreign_key: :in_response_id,
34 | class_name: 'Response',
35 | optional: true
36 |
37 | has_many :likes,
38 | foreign_key: :response_id,
39 | class_name: 'Like'
40 |
41 | has_many :likers,
42 | through: :likes,
43 | source: :liker
44 |
45 | end
46 |
--------------------------------------------------------------------------------
/app/models/story.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: stories
4 | #
5 | # id :integer not null, primary key
6 | # author_id :integer not null
7 | # title :string not null
8 | # description :text
9 | # body :text not null
10 | # date :string
11 | # topic_id :integer
12 | # created_at :datetime not null
13 | # updated_at :datetime not null
14 | # main_image_file_name :string
15 | # main_image_content_type :string
16 | # main_image_file_size :integer
17 | # main_image_updated_at :datetime
18 | #
19 |
20 | class Story < ApplicationRecord
21 |
22 | validates :author_id, :title, :body, presence: true
23 |
24 | has_attached_file :main_image, default_url: "story_default.png"
25 | validates_attachment_content_type :main_image, content_type: /\Aimage\/.*\Z/
26 |
27 | belongs_to :author,
28 | foreign_key: :author_id,
29 | class_name: 'User'
30 |
31 | has_many :responses,
32 | foreign_key: :story_id,
33 | class_name: 'Response'
34 |
35 | has_many :likes,
36 | foreign_key: :story_id,
37 | class_name: 'Like'
38 |
39 | has_many :likers,
40 | through: :likes,
41 | source: :liker
42 |
43 | belongs_to :topic,
44 | foreign_key: :topic_id,
45 | class_name: 'Topic',
46 | optional: true
47 |
48 | end
49 |
--------------------------------------------------------------------------------
/app/models/topic.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: topics
4 | #
5 | # id :integer not null, primary key
6 | # name :string not null
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | #
10 |
11 | class Topic < ApplicationRecord
12 |
13 | validates :name, presence: true
14 |
15 | has_many :stories,
16 | foreign_key: :topic_id,
17 | class_name: 'Story'
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/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 | # email :string not null
8 | # name :string not null
9 | # bio :text
10 | # photo_url :string
11 | # password_digest :string not null
12 | # session_token :string not null
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | # photo_file_name :string
16 | # photo_content_type :string
17 | # photo_file_size :integer
18 | # photo_updated_at :datetime
19 | #
20 |
21 | class User < ApplicationRecord
22 | # styles: { medium: "300x300>", thumb: "100x100>" },
23 | has_attached_file :photo, default_url: "missing.png"
24 | validates_attachment_content_type :photo, content_type: /\Aimage\/.*\Z/
25 |
26 | validates :username, :email, :session_token, uniqueness: true
27 | validates :username, :email, :name, :session_token,
28 | :password_digest, presence: true
29 | validates :password, length:{minimum: 6, allow_nil: true}
30 |
31 | after_initialize :ensure_session_token
32 |
33 | attr_reader :password
34 |
35 |
36 | has_many :stories,
37 | foreign_key: :author_id,
38 | class_name: 'Story'
39 |
40 | has_many :responses,
41 | foreign_key: :writer_id,
42 | class_name: 'Response'
43 |
44 | # has_many :likes_on_stories,
45 | # foreign_key: :story_id,
46 | # class_name: 'Like'
47 | #
48 | # has_many :likes_on_responses,
49 | # foreign_key: :response_id,
50 | # class_name: 'Like'
51 |
52 | has_many :things_liked,
53 | foreign_key: :liker_id,
54 | class_name: 'Like'
55 |
56 | has_many :liked_stories,
57 | through: :things_liked,
58 | source: :story
59 |
60 | has_many :liked_responses,
61 | through: :things_liked,
62 | source: :response
63 |
64 | # will this work?
65 | # has_many :liked_stories,
66 | # through: :likes_on_stories,
67 |
68 | # yipes
69 |
70 | has_many :follower_ids,
71 | foreign_key: :follower_id,
72 | class_name: 'Following'
73 |
74 | has_many :followers,
75 | through: :following_ids,
76 | source: :follower
77 |
78 | has_many :following_ids,
79 | foreign_key: :following_id,
80 | class_name: 'Following'
81 |
82 | has_many :following,
83 | through: :follower_ids,
84 | source: :following
85 |
86 | # will this work?
87 | has_many :stories_by_followed_users,
88 | through: :following,
89 | source: :stories
90 |
91 | has_many :responses_by_followed_users,
92 | through: :following,
93 | source: :responses
94 | # yipes
95 |
96 |
97 | def self.find_by_credentials(username, password)
98 | user = User.find_by(username: username)
99 | return user if user && user.is_password?(password)
100 | nil
101 | end
102 |
103 | def is_password?(password)
104 | BCrypt::Password.new(self.password_digest).is_password?(password)
105 | end
106 |
107 | def password=(password)
108 | @password = password
109 | self.password_digest = BCrypt::Password.create(password)
110 | end
111 |
112 | def self.generate_session_token
113 | SecureRandom.urlsafe_base64(16)
114 | end
115 |
116 | def reset_session_token!
117 | self.session_token = User.generate_session_token
118 | self.save!
119 | self.session_token
120 | end
121 |
122 | private
123 |
124 | def ensure_session_token
125 | self.session_token ||= User.generate_session_token
126 | end
127 |
128 | end
129 |
130 |
131 | # username | string | not null, indexed, unique
132 | # email | string | not null, indexed, unique
133 | # name | string | not null
134 | # bio | text |
135 | # photo_url | string |
136 | # password_digest | string | not null
137 | # session_token | string | not null, indexed, unique
138 |
--------------------------------------------------------------------------------
/app/views/api/followings/_following.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! following, :id, :follower_id, :following_id
2 |
--------------------------------------------------------------------------------
/app/views/api/followings/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @followings.each do |following|
2 | json.set! following.id do
3 | json.partial! 'following', following: following
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/followings/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'following', following: @following
2 |
--------------------------------------------------------------------------------
/app/views/api/likes/_like.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! like, :id, :liker_id, :story_id, :response_id
2 |
--------------------------------------------------------------------------------
/app/views/api/likes/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @likes.each do |like|
2 | json.set! like.id do
3 | json.partial! 'like', like: like
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/likes/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'like', like: @like
2 |
--------------------------------------------------------------------------------
/app/views/api/responses/_response.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! response, :id, :writer_id, :story_id, :in_response_id, :body, :date, :likers, :likes, :created_at, :updated_at
2 | json.writer_name response.writer.name
3 | json.writer_photo_url asset_path(response.writer.photo.url(:original))
4 |
--------------------------------------------------------------------------------
/app/views/api/responses/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @responses.each do |response|
2 | json.set! response.id do
3 | json.partial! 'response', response: response
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/responses/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.partial! 'response', response: @response
2 |
--------------------------------------------------------------------------------
/app/views/api/stories/_story.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! story, :id, :author_id, :title, :description, :date, :likers, :likes, :created_at, :updated_at
2 | json.author_name story.author.name
3 | json.author_photo_url asset_path(story.author.photo.url(:original))
4 | json.main_image_url asset_path(story.main_image.url(:original))
5 |
--------------------------------------------------------------------------------
/app/views/api/stories/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @stories.each do |story|
2 | json.set! story.id do
3 | json.partial! 'story', story: story
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/stories/show.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! @story, :id, :author_id, :title, :description, :body, :date, :likes, :created_at, :updated_at, :topic
2 | json.author_name @story.author.name
3 | json.author_bio @story.author.bio
4 | json.author_follower_ids @story.author.follower_ids
5 | json.author_photo_url asset_path(@story.author.photo.url(:original))
6 | json.main_image_url asset_path(@story.main_image.url(:original))
7 |
8 | json.likers do
9 | json.array! @story.likers do |liker|
10 | json.id liker.id
11 | json.username liker.username
12 | json.email liker.email
13 | json.bio liker.bio
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/views/api/topics/_topic.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! topic, :id, :name
2 |
--------------------------------------------------------------------------------
/app/views/api/topics/index.json.jbuilder:
--------------------------------------------------------------------------------
1 | @topics.each do |topic|
2 | json.set! topic.id do
3 | json.partial! 'topic', topic: topic
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/users/_user.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! user, :id, :username, :name, :follower_ids, :following_ids
2 | json.image_url asset_path(user.photo.url(:original))
3 |
--------------------------------------------------------------------------------
/app/views/api/users/session_user.json.jbuilder:
--------------------------------------------------------------------------------
1 | json.extract! @user, :id, :username, :name
2 | json.image_url asset_path(@user.photo.url(:original))
3 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Message
5 | <%= csrf_meta_tags %>
6 |
7 | <%= stylesheet_link_tag 'application', media: 'all' %>
8 | <%= javascript_include_tag 'application' %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
27 |
28 |
29 | Message
30 |
31 |
32 |
33 | I am the root.
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../config/application', __dir__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a starting point to setup your application.
15 | # Add necessary setup steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | # puts "\n== Copying sample files =="
22 | # unless File.exist?('config/database.yml')
23 | # cp 'config/database.yml.sample', 'config/database.yml'
24 | # end
25 |
26 | puts "\n== Preparing database =="
27 | system! 'bin/rails db:setup'
28 |
29 | puts "\n== Removing old logs and tempfiles =="
30 | system! 'bin/rails log:clear tmp:clear'
31 |
32 | puts "\n== Restarting application server =="
33 | system! 'bin/rails restart'
34 | end
35 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | if spring
13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
14 | gem 'spring', spring.version
15 | require 'spring/binstub'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Message
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 | config.paperclip_defaults = {
16 | :storage => :s3,
17 | s3_host_name: "s3-#{ENV['s3_region']}.amazonaws.com",
18 | :s3_credentials => {
19 | :bucket => ENV["s3_bucket"],
20 | :access_key_id => ENV["s3_access_key_id"],
21 | :secret_access_key => ENV["s3_secret_access_key"],
22 | :s3_region => ENV["s3_region"]
23 | }
24 | }
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 9.1 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On OS X with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On OS X with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem 'pg'
16 | #
17 | default: &default
18 | adapter: postgresql
19 | encoding: unicode
20 | # For details on connection pooling, see rails configuration guide
21 | # http://guides.rubyonrails.org/configuring.html#database-pooling
22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
23 |
24 | development:
25 | <<: *default
26 | database: message_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: message
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: message_test
61 |
62 | # As with config/secrets.yml, you never want to store sensitive information,
63 | # like your database password, in your source code. If your source code is
64 | # ever seen by anyone, they now have access to your database.
65 | #
66 | # Instead, provide the password as a unix environment variable when you boot
67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
68 | # for a full rundown on how to provide these environment variables in a
69 | # production deployment.
70 | #
71 | # On Heroku and other platform providers, you may have a full connection URL
72 | # available as an environment variable. For example:
73 | #
74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
75 | #
76 | # You can use this database configuration with:
77 | #
78 | # production:
79 | # url: <%= ENV['DATABASE_URL'] %>
80 | #
81 | production:
82 | <<: *default
83 | database: message_production
84 | username: message
85 | password: <%= ENV['MESSAGE_DATABASE_PASSWORD'] %>
86 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => 'public, max-age=172800'
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Don't care if the mailer can't send.
30 | config.action_mailer.raise_delivery_errors = false
31 |
32 | config.action_mailer.perform_caching = false
33 |
34 | # Print deprecation notices to the Rails logger.
35 | config.active_support.deprecation = :log
36 |
37 | # Raise an error on page load if there are pending migrations.
38 | config.active_record.migration_error = :page_load
39 |
40 | # Debug mode disables concatenation and preprocessing of assets.
41 | # This option may cause significant delays in view rendering with a large
42 | # number of complex assets.
43 | config.assets.debug = true
44 |
45 | # Suppress logger output for asset requests.
46 | config.assets.quiet = true
47 |
48 | # Raises error for missing translations
49 | # config.action_view.raise_on_missing_translations = true
50 |
51 | # Use an evented file watcher to asynchronously detect changes in source code,
52 | # routes, locales, etc. This feature depends on the listen gem.
53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
54 | end
55 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Disable serving static files from the `/public` folder by default since
18 | # Apache or NGINX already handles this.
19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
20 |
21 | # Compress JavaScripts and CSS.
22 | config.assets.js_compressor = :uglifier
23 | # config.assets.css_compressor = :sass
24 |
25 | # Do not fallback to assets pipeline if a precompiled asset is missed.
26 | config.assets.compile = false
27 |
28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
29 |
30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
31 | # config.action_controller.asset_host = 'http://assets.example.com'
32 |
33 | # Specifies the header that your server uses for sending files.
34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
36 |
37 | # Mount Action Cable outside main process or domain
38 | # config.action_cable.mount_path = nil
39 | # config.action_cable.url = 'wss://example.com/cable'
40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
41 |
42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
43 | # config.force_ssl = true
44 |
45 | # Use the lowest log level to ensure availability of diagnostic information
46 | # when problems arise.
47 | config.log_level = :debug
48 |
49 | # Prepend all log lines with the following tags.
50 | config.log_tags = [ :request_id ]
51 |
52 | # Use a different cache store in production.
53 | # config.cache_store = :mem_cache_store
54 |
55 | # Use a real queuing backend for Active Job (and separate queues per environment)
56 | # config.active_job.queue_adapter = :resque
57 | # config.active_job.queue_name_prefix = "message_#{Rails.env}"
58 | config.action_mailer.perform_caching = false
59 |
60 | # Ignore bad email addresses and do not raise email delivery errors.
61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
62 | # config.action_mailer.raise_delivery_errors = false
63 |
64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
65 | # the I18n.default_locale when a translation cannot be found).
66 | config.i18n.fallbacks = true
67 |
68 | # Send deprecation notices to registered listeners.
69 | config.active_support.deprecation = :notify
70 |
71 | # Use default logging formatter so that PID and timestamp are not suppressed.
72 | config.log_formatter = ::Logger::Formatter.new
73 |
74 | # Use a different logger for distributed setups.
75 | # require 'syslog/logger'
76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
77 |
78 | if ENV["RAILS_LOG_TO_STDOUT"].present?
79 | logger = ActiveSupport::Logger.new(STDOUT)
80 | logger.formatter = config.log_formatter
81 | config.logger = ActiveSupport::TaggedLogging.new(logger)
82 | end
83 |
84 | # Do not dump schema after migrations.
85 | config.active_record.dump_schema_after_migration = false
86 | end
87 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => 'public, max-age=3600'
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 5.0 upgrade.
4 | #
5 | # Read the Guide for Upgrading Ruby on Rails for more info on each option.
6 |
7 | # Enable per-form CSRF tokens. Previous versions had false.
8 | Rails.application.config.action_controller.per_form_csrf_tokens = true
9 |
10 | # Enable origin-checking CSRF mitigation. Previous versions had false.
11 | Rails.application.config.action_controller.forgery_protection_origin_check = true
12 |
13 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
14 | # Previous versions had false.
15 | ActiveSupport.to_time_preserves_timezone = true
16 |
17 | # Require `belongs_to` associations by default. Previous versions had false.
18 | Rails.application.config.active_record.belongs_to_required_by_default = true
19 |
20 | # Do not halt callback chains when a callback returns false. Previous versions had true.
21 | ActiveSupport.halt_callback_chains_on_return_false = false
22 |
23 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false.
24 | Rails.application.config.ssl_options = { hsts: { subdomains: true } }
25 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_message_session'
4 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum, this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # The code in the `on_worker_boot` will be called if you are using
36 | # clustered mode by specifying a number of `workers`. After each worker
37 | # process is booted this block will be run, if you are using `preload_app!`
38 | # option you will want to use this block to reconnect to any threads
39 | # or connections that may have been created at application boot, Ruby
40 | # cannot share connections between processes.
41 | #
42 | # on_worker_boot do
43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
44 | # end
45 |
46 | # Allow puma to be restarted by `rails restart` command.
47 | plugin :tmp_restart
48 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
3 |
4 | root to: 'static_pages#root'
5 |
6 | namespace :api, defaults: {format: :json} do
7 | resource :session, only:[:create, :destroy]
8 | resources :users, only:[:create, :update, :show]
9 | resources :stories, only:[:index, :create, :update, :show, :destroy]
10 | resources :responses, only:[:index, :create, :update, :show, :destroy]
11 | resources :likes, only:[:index, :create, :show, :destroy]
12 | resources :followings, only:[:index, :create, :show, :destroy]
13 | resources :topics, only:[:index]
14 | end
15 |
16 | end
17 |
18 |
19 | # routes:
20 | # Rails.application.routes.draw do
21 | # namespace :api, defaults: {format: :json} do
22 | # resources :benches, only: [:index, :show, :create]
23 | # resources :reviews, only: [:create]
24 | # resource :user, only: [:create]
25 | # resource :session, only: [:create, :destroy, :show]
26 | # resource :favorites, only: [:create, :destroy]
27 | # end
28 | #
29 | # root "static_pages#root"
30 | # end
31 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: fe299539e7ae78bc08bd3e9e39f277437fb8d782db9bad365d1616a5ef213e609a0bfbb98cc99bec3059625b6e3433649bc5ad68b75542d696cc9fea021ac7af
15 |
16 | test:
17 | secret_key_base: 61b0c293c3be6fb079199f36361ea0227a4079e1662c0741e11a6f40408c3880c9ceebd1f6379dd58656fa688261fe9e6d6d823965e6e04e892ec0bdffaa9260
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w(
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ).each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/db/migrate/20170418152702_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :users do |t|
4 | t.string :username, null: false
5 | t.string :email, null: false
6 | t.string :name, null: false
7 | t.text :bio
8 | t.string :photo_url
9 | t.string :password_digest, null: false
10 | t.string :session_token, null: false
11 |
12 | t.timestamps
13 | end
14 |
15 | add_index :users, :username, unique: true
16 | add_index :users, :email, unique: true
17 | add_index :users, :session_token, unique: true
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/migrate/20170420000222_add_attachment_photo_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddAttachmentPhotoToUsers < ActiveRecord::Migration
2 | def self.up
3 | change_table :users do |t|
4 | t.attachment :photo
5 | end
6 | end
7 |
8 | def self.down
9 | remove_attachment :users, :photo
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20170420175634_create_stories.rb:
--------------------------------------------------------------------------------
1 | class CreateStories < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :stories do |t|
4 | t.integer :author_id, null: false
5 | t.string :title, null: false
6 | t.text :description
7 | t.text :body, null: false
8 | t.string :date
9 | t.integer :topic_id
10 |
11 | t.timestamps
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/db/migrate/20170420180234_add_attachment_main_image_to_stories.rb:
--------------------------------------------------------------------------------
1 | class AddAttachmentMainImageToStories < ActiveRecord::Migration
2 | def self.up
3 | change_table :stories do |t|
4 | t.attachment :main_image
5 | end
6 | end
7 |
8 | def self.down
9 | remove_attachment :stories, :main_image
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20170422234929_create_responses.rb:
--------------------------------------------------------------------------------
1 | class CreateResponses < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :responses do |t|
4 | t.integer :writer_id, null: false
5 | t.integer :story_id
6 | t.text :body, null: false
7 | t.string :date
8 | t.integer :in_response_id
9 |
10 | t.timestamps
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20170425133845_create_likes.rb:
--------------------------------------------------------------------------------
1 | class CreateLikes < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :likes do |t|
4 | t.integer :liker_id, null: false
5 | t.integer :story_id, null: false
6 | t.integer :response_id
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20170425133900_create_followings.rb:
--------------------------------------------------------------------------------
1 | class CreateFollowings < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :followings do |t|
4 | t.integer :follower_id, null: false
5 | t.integer :following_id, null: false
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20170427015509_add_null_to_likes.rb:
--------------------------------------------------------------------------------
1 | class AddNullToLikes < ActiveRecord::Migration[5.0]
2 | def change
3 | change_column_null :likes, :story_id, true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20170428111440_create_topics.rb:
--------------------------------------------------------------------------------
1 | class CreateTopics < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :topics do |t|
4 | t.string :name, null: false
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20170428111440) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "followings", force: :cascade do |t|
19 | t.integer "follower_id", null: false
20 | t.integer "following_id", null: false
21 | t.datetime "created_at", null: false
22 | t.datetime "updated_at", null: false
23 | end
24 |
25 | create_table "likes", force: :cascade do |t|
26 | t.integer "liker_id", null: false
27 | t.integer "story_id"
28 | t.integer "response_id"
29 | t.datetime "created_at", null: false
30 | t.datetime "updated_at", null: false
31 | end
32 |
33 | create_table "responses", force: :cascade do |t|
34 | t.integer "writer_id", null: false
35 | t.integer "story_id"
36 | t.text "body", null: false
37 | t.string "date"
38 | t.integer "in_response_id"
39 | t.datetime "created_at", null: false
40 | t.datetime "updated_at", null: false
41 | end
42 |
43 | create_table "stories", force: :cascade do |t|
44 | t.integer "author_id", null: false
45 | t.string "title", null: false
46 | t.text "description"
47 | t.text "body", null: false
48 | t.string "date"
49 | t.integer "topic_id"
50 | t.datetime "created_at", null: false
51 | t.datetime "updated_at", null: false
52 | t.string "main_image_file_name"
53 | t.string "main_image_content_type"
54 | t.integer "main_image_file_size"
55 | t.datetime "main_image_updated_at"
56 | end
57 |
58 | create_table "topics", force: :cascade do |t|
59 | t.string "name", null: false
60 | t.datetime "created_at", null: false
61 | t.datetime "updated_at", null: false
62 | end
63 |
64 | create_table "users", force: :cascade do |t|
65 | t.string "username", null: false
66 | t.string "email", null: false
67 | t.string "name", null: false
68 | t.text "bio"
69 | t.string "photo_url"
70 | t.string "password_digest", null: false
71 | t.string "session_token", null: false
72 | t.datetime "created_at", null: false
73 | t.datetime "updated_at", null: false
74 | t.string "photo_file_name"
75 | t.string "photo_content_type"
76 | t.integer "photo_file_size"
77 | t.datetime "photo_updated_at"
78 | t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
79 | t.index ["session_token"], name: "index_users_on_session_token", unique: true, using: :btree
80 | t.index ["username"], name: "index_users_on_username", unique: true, using: :btree
81 | end
82 |
83 | end
84 |
--------------------------------------------------------------------------------
/docs/api-endpoints.md:
--------------------------------------------------------------------------------
1 | # API Endpoints
2 |
3 | ## HTML API
4 |
5 | ### Root
6 |
7 | - `GET /` - loads React app
8 |
9 | ## JSON API
10 |
11 | ### Users
12 | - `POST /api/users`
13 | - `PATCH /api/users`
14 | - `GET /api/users/:userId` - view user profile page
15 |
16 | ### Session
17 | - (render user partial for current user)
18 | - `POST /api/session`
19 | - `DELETE /api/session`
20 |
21 | ### Stories
22 | - `GET /api/stories`
23 | - `POST /api/stories`
24 | - `PATCH /api/stories/:storyId`
25 | - `DELETE /api/stories/:storyId`
26 |
27 | ### Responses
28 | - `GET /api/responses` - should be able to query by story or by user
29 | - `POST /api/:storyId/responses`
30 | - `DELETE /api/responses/:responseId`
31 |
32 | ### Tags
33 | - `GET /api/tags`
34 | - `POST /api/tags`
35 | - `DELETE /api/tags/:tagId` - individual users probably should not be able to delete tags, however
36 |
37 | ### Taggings
38 | - `GET /api/taggings` - should be able to query by story or by tag
39 | - `POST /api/taggings/` - pass in :storyId and :tagId; add tag to story
40 | - `DELETE /api/taggings/` - pass in :storyId and :tagId; remove tag from story
41 |
42 | ### RelatedTags
43 | - `GET /api/related_tags/:tagId`
44 | - `POST /api/related_tags/:tagId` - pass in other tag id's to assign related tags
45 | - `DELETE /api/related_tags/:tagId` - pass in other tag id's to remove related tags
46 |
47 | ### FollowedTags
48 | - `GET /api/followed_tags/:tagId`
49 | - `POST /api/followed_tags/:userId` - pass in :tagId to follow tag
50 | - `DELETE /api/followed_tags/:userId` - pass in :tagId to stop following tag
51 |
52 | ### Topics
53 | - `GET /api/topics/:topicId`
54 |
55 | ### FollowedTopics
56 | - `GET /api/followed_topics/:topicId`
57 | - `POST /api/followed_topics/:userId` - pass in :topicId to follow topic
58 | - `DELETE /api/followed_topics/:userId` - pass in :topicId to stop following topic
59 |
60 | ### Followings
61 | - `GET /api/followings/:followerId`
62 | - `POST /api/followings/:followerId` - also pass in :followingId; user follows another user
63 | - `DELETE /api/followings/:followerID` - also pass in :followingId; user stops following another user
64 |
65 | ### Highlights
66 | - `GET /api/highlights/:storyId` - get all highlights for a story
67 | - `POST /api/highlights/:storyId` - also pass in highlight data; add highlight to a story
68 | - `DELETE /api/highlights/:highlightId` - remove highlight
69 |
70 | ### Likes
71 | - `GET /api/likes` - query by user (things liked by user), story, or response
72 | - `POST /api/likes/:userId` - user can like a story or a response
73 | - `DELETE /api/likes/:userId` - user can unlike a story or a response
74 |
--------------------------------------------------------------------------------
/docs/caps/m-cap-home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/caps/m-cap-home.jpg
--------------------------------------------------------------------------------
/docs/caps/m-cap-profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/caps/m-cap-profile.jpg
--------------------------------------------------------------------------------
/docs/caps/m-cap-story-input.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/caps/m-cap-story-input.jpg
--------------------------------------------------------------------------------
/docs/caps/m-cap-story.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/caps/m-cap-story.jpg
--------------------------------------------------------------------------------
/docs/component-hierarchy.md:
--------------------------------------------------------------------------------
1 | ## Component Hierarchy
2 |
3 | **AuthFormContainer**
4 | - AuthForm
5 | - ErrorsComponent
6 |
7 | **HomeContainer**
8 | - HomeNav
9 | + UserSidebar
10 | - TagsYouFollow
11 | + StoriesFeed
12 | - StoriesFeedItem
13 |
14 | **StoryContainer**
15 | - InteriorNav
16 | - StorySidebar
17 | - StoryFooter
18 | - StoryContent
19 | - StoryTags
20 | + RelatedStories
21 | - RelatedStory
22 | + ResponseSectionContainer
23 | - ResponseInput
24 | + Responses
25 | - Response
26 | - ErrorsComponent
27 |
28 | **FilteredFeedContainer**
29 | - InteriorNav
30 | + UserSidebar
31 | - RelatedTags
32 | - TagsYouFollow
33 | + StoriesFeed
34 | - FeedData
35 | - StoriesFeedItem
36 |
37 | **StoryInputContainer**
38 | - StoryInputNav
39 | + PublishOptions
40 | - AddTags
41 | - StoryInput
42 | - StoryInputFooter
43 |
44 | **UserProfileContainer**
45 | - InteriorNav
46 | + UserHeader
47 | - EditProfile
48 | - UsersFollowing
49 | - UsersFollowed
50 | + UserProfileFeed
51 | - UserProfileItem
52 | + UserStoriesFeed
53 | - UserStoriesFeedItem
54 | + UserRecommendsFeed
55 | - UserRecommendsFeedItem
56 | + UserResponsesFeed
57 | - UserResponsesFeedItem
58 |
59 | **SearchPageContainer**
60 | - InteriorNav
61 | - SearchField
62 | - SearchResultsSidebar
63 |
64 | ## Routes
65 |
66 | |Path | Component |
67 | |-------|-------------|
68 | | '/sign-up' | 'AuthFormContainer' |
69 | | '/sign-in' | 'AuthFormContainer' |
70 | | '/home' | 'HomeContainer' |
71 | | '/new-story' | 'StoryInputContainer' |
72 | | '/search' | 'SearchPageContainer' |
73 | | '/:userId/:storyId' | 'StoryContainer' |
74 | | '/tag/:tagId' | 'FilteredFeedContainer' |
75 | | '/tag/:topicId' | 'FilteredFeedContainer' |
76 | | '/:userId' | 'UserProfileContainer' |
77 |
--------------------------------------------------------------------------------
/docs/readme.md:
--------------------------------------------------------------------------------
1 | # Message
2 |
3 | [Heroku](https://message-medium.herokuapp.com/#/)
4 |
5 | [Trello](https://trello.com/b/XmbpaiSW/medium-clone)
6 |
7 | ## Minimum Viable Product
8 | Message is a single-page web-application inspired by Medium, built using Ruby on Rails, React/Redux, and QuillJS. By the end of Week 9, this app will, at a minimum, satisfy the following criteria with smooth, bug-free navigation, adequate seed data and sufficient CSS styling:
9 | - [ ] New account creation, login, and guest/demo login.
10 | - [ ] Users can write stories.
11 | - [ ] Users can add comments ('responses') to stories.
12 | - [ ] Users can follow other users and view a feed of relevant stories.
13 | - [ ] Users can 'like' stories and comments.
14 | - [ ] A production README.
15 | - [ ] Hosting on Heroku.
16 | - [ ] Bonus: Stories are organized by topics.
17 | - [ ] Bonus: Users can bookmark stories.
18 | - [ ] Bonus: Users can highlight sections of stories.
19 |
20 |
21 | ## Design Docs
22 |
23 | * [View Wireframes][wireframes]
24 | * [React Components][components]
25 | * [API endpoints][api-endpoints]
26 | * [DB schema][schema]
27 | * [Sample State][sample-state]
28 |
29 | [wireframes]: wireframes/
30 | [components]: component-hierarchy.md
31 | [sample-state]: sample-state.md
32 | [api-endpoints]: api-endpoints.md
33 | [schema]: schema.md
34 |
35 |
36 | ## Implementation Timeline
37 |
38 | ### Phase 1: Backend setup and authentication (2 days)
39 |
40 | **Objective:** Functioning Rails project with front-end auth.
41 |
42 | ### Phase 2: Story model, API (2 days)
43 |
44 | **Objective:** Users can post stories; stories can be modified via API.
45 |
46 | ### Phase 3: Responses (1 day)
47 |
48 | **Objective:** Users can post responses; responses can be modified via API.
49 |
50 | ### Phase 4: Tags (maybe Topics) (1 day)
51 |
52 | **Objective:** Stories can be tagged, and tags can be set as relevant to other tags. Stories may also belong to topics (bonus).
53 |
54 | ### Phase 5: Likes (1 day)
55 |
56 | **Objective:** Users can 'like' (and unlike) stories and responses.
57 |
58 | ### Phase 6: Following/Feeds/Profiles (2 days)
59 |
60 | **Objective:** Users can follow other users; users have their own feeds of stories; each user's profile has a feed of their own content.
61 |
62 | ### Phase 7: Search (1 day)
63 |
64 | **Objective:** Users can search for stories, users and tags.
65 |
66 | ### Phase 8: Allow complex styling in stories (1 day)
67 |
68 | **Objective:** Users can style the text in their stories and add images.
69 |
70 | ### Bonus Features (tbd)
71 | - [ ] Stories are categorized by topics.
72 | - [ ] Add highlights (users can highlight other users' stories)
73 | - [ ] Add bookmarks (users can bookmark other users' stories)
74 |
--------------------------------------------------------------------------------
/docs/sample-state.md:
--------------------------------------------------------------------------------
1 | ```js
2 | // Sample State
3 | {
4 | currentUser: {
5 | id: 1,
6 | username: "test-user",
7 | name: "Don Joe",
8 | bio: "I am a user",
9 | photo_url: "www.url.com"
10 | },
11 | forms: {
12 | signUp: {errors: []},
13 | logIn: {errors: []},
14 | },
15 | stories: {
16 | 1: {
17 | story_id: 1,
18 | title: "Test Story",
19 | body: "Story content here.",
20 | date: "1/1/01",
21 | author_id: 1,
22 | author_name: "Don Joe",
23 | topic_id: 1,
24 | topic_name: "Cats",
25 | tags: {
26 | 1: {
27 | id: 1
28 | name: "Interesting tag"
29 | }
30 | }
31 | }
32 | },
33 | responses: {
34 | response_id: 2,
35 | body: "Good story!",
36 | date: "1/2/01",
37 | writer_id: 3,
38 | writer_name: "Lon Boe"
39 | }
40 | }
41 | ```
42 |
--------------------------------------------------------------------------------
/docs/schema.md:
--------------------------------------------------------------------------------
1 | # Schema Information
2 |
3 | ## users
4 | column name | data type | details
5 | ----------------|-----------|-----------------------
6 | id | integer | not null, primary key
7 | username | string | not null, indexed, unique
8 | email | string | not null, indexed, unique
9 | name | string | not null
10 | bio | text |
11 | photo_url | string |
12 | password_digest | string | not null
13 | session_token | string | not null, indexed, unique
14 |
15 | ## stories
16 | column name | data type | details
17 | ----------------|-----------|-----------------------
18 | id | integer | not null, primary key
19 | author_id | integer | not null, foreign key (references users), indexed
20 | topic_id | integer | (bonus feature) not null, foreign key (references topics), indexed
21 | title | string | not null
22 | description | text |
23 | body | text | not null
24 | date | string | (allowing null for now - to make use of timestamps)
25 | image | string | (generated via paperclip; can be default)
26 |
27 | ## responses
28 | column name | data type | details
29 | ----------------|-----------|-----------------------
30 | id | integer | not null, primary key
31 | story_id | integer | foreign key (references stories), indexed
32 | writer_id | integer | not null, foreign key (references users), indexed
33 | body | text | not null
34 | date | string | not null
35 | in_response_id | integer | (if possible, foreign key for responses to responses)
36 |
37 | ## topics
38 | column name | data type | details
39 | ----------------|-----------|-----------------------
40 | id | integer | not null, primary key
41 | name | string | not null, indexed, unique
42 |
43 | ## followed_topics
44 | column name | data type | details
45 | ----------------|-----------|-----------------------
46 | id | integer | not null, primary key
47 | user_id | integer | not null, foreign key (references users), indexed
48 | topic_id | integer | not null, foreign key (references topics), indexed
49 |
50 | ## tags
51 | column name | data type | details
52 | ----------------|-----------|-----------------------
53 | id | integer | not null, primary key
54 | name | string | not null, indexed, unique
55 |
56 | ## taggings
57 | column name | data type | details
58 | ----------------|-----------|-----------------------
59 | tag_id | integer | not null, foreign key (references tags), indexed
60 | story_id | integer | not null, foreign key (references stories), indexed
61 |
62 | ## related_tags
63 | column name | data type | details
64 | ----------------|-----------|-----------------------
65 | tag_id | integer | not null, foreign key (references tags), indexed
66 | related_tag_id | integer | not null, foreign key (references tags), indexed
67 |
68 | ## followed_tags
69 | column name | data type | details
70 | ----------------|-----------|-----------------------
71 | tag_id | integer | not null, foreign key (references tags), indexed
72 | user_id | integer | not null, foreign key (references users), indexed
73 |
74 | ## followings
75 | column name | data type | details
76 | ----------------|-----------|-----------------------
77 | follower_id | integer | not null, foreign key (references users), indexed
78 | following_id | integer | not null, foreign key (references users), indexed
79 |
80 | ## highlights
81 | column name | data type | details
82 | ----------------|-----------|-----------------------
83 | story_id | integer | not null, foreign key (references stories), indexed
84 | user_id | integer | not null, foreign key (references users), indexed
85 | data | string | not null
86 |
87 | ## likes
88 | column name | data type | details
89 | ----------------|-----------|-----------------------
90 | liker_id | integer | not null, foreign key (references users), indexed
91 | story_id | integer | not null, foreign key (references stories), indexed
92 | response_id | integer | foreign key (references responses), indexed
93 |
--------------------------------------------------------------------------------
/docs/wireframes/auth-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/auth-form.png
--------------------------------------------------------------------------------
/docs/wireframes/filtered-feed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/filtered-feed.png
--------------------------------------------------------------------------------
/docs/wireframes/home-logged-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/home-logged-in.png
--------------------------------------------------------------------------------
/docs/wireframes/search-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/search-page.png
--------------------------------------------------------------------------------
/docs/wireframes/story-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/story-input.png
--------------------------------------------------------------------------------
/docs/wireframes/story.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/story.png
--------------------------------------------------------------------------------
/docs/wireframes/user-profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/docs/wireframes/user-profile.png
--------------------------------------------------------------------------------
/frontend/actions/following_actions.js:
--------------------------------------------------------------------------------
1 | import * as FollowingAPIUtil from '../util/following_api_util';
2 |
3 | export const RECEIVE_ALL_FOLLOWINGS = "RECEIVE_ALL_FOLLOWINGS";
4 | export const RECEIVE_FOLLOWING = "RECEIVE_FOLLOWING";
5 |
6 | export const receiveAllFollowings = followings => ({
7 | type: RECEIVE_ALL_FOLLOWINGS,
8 | followings
9 | });
10 |
11 | export const receiveFollowing = following => ({
12 | type: RECEIVE_FOLLOWING,
13 | following
14 | });
15 |
16 | export const fetchAllFollowings = (data) => dispatch => {
17 | return(FollowingAPIUtil.followingIndex(data)
18 | .then(followings => dispatch(receiveAllFollowings(followings)))
19 | );
20 | };
21 |
22 | export const fetchFollowing = (followingId) => dispatch => (
23 | FollowingAPIUtil.followingShow(followingId)
24 | .then(following => dispatch(receiveFollowing(following)))
25 | );
26 |
27 | export const createFollowing = (following) => dispatch => {
28 | return (
29 | FollowingAPIUtil.followingCreate(following)
30 | .then(following => dispatch(receiveFollowing(following)))
31 | );
32 | };
33 |
34 | export const destroyFollowing = (following) => dispatch => {
35 | return (
36 | FollowingAPIUtil.followingDestroy(following)
37 | .then(following => dispatch(receiveFollowing(following)))
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/frontend/actions/like_actions.js:
--------------------------------------------------------------------------------
1 | import * as LikeAPIUtil from '../util/like_api_util';
2 |
3 | export const RECEIVE_ALL_LIKES = "RECEIVE_ALL_LIKES";
4 | export const RECEIVE_LIKE = "RECEIVE_LIKE";
5 |
6 | export const receiveAllLikes = likes => ({
7 | type: RECEIVE_ALL_LIKES,
8 | likes
9 | });
10 |
11 | export const receiveLike = like => ({
12 | type: RECEIVE_LIKE,
13 | like
14 | });
15 |
16 | export const fetchAllLikes = (data) => dispatch => {
17 | return(LikeAPIUtil.likeIndex(data)
18 | .then(likes => dispatch(receiveAllLikes(likes)))
19 | );
20 | };
21 |
22 | export const fetchLike = (likeId) => dispatch => (
23 | LikeAPIUtil.likeShow(likeId)
24 | .then(like => dispatch(receiveLike(like)))
25 | );
26 |
27 | export const createLike = (like) => dispatch => {
28 | return (
29 | LikeAPIUtil.likeCreate(like)
30 | .then(like => dispatch(receiveLike(like)))
31 | );
32 | };
33 |
34 | export const destroyLike = (like) => dispatch => {
35 | return (
36 | LikeAPIUtil.likeDestroy(like)
37 | .then(like => dispatch(receiveLike(like)))
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/frontend/actions/response_actions.js:
--------------------------------------------------------------------------------
1 | import * as ResponseAPIUtil from '../util/response_api_util';
2 |
3 | export const RECEIVE_ALL_RESPONSES = "RECEIVE_ALL_RESPONSES";
4 | export const RECEIVE_RESPONSE = "RECEIVE_RESPONSE";
5 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
6 |
7 | export const receiveAllResponses = responses => ({
8 | type: RECEIVE_ALL_RESPONSES,
9 | responses
10 | });
11 |
12 | export const receiveResponse = response => ({
13 | type: RECEIVE_RESPONSE,
14 | response
15 | });
16 |
17 | export const receiveErrors = errors => ({
18 | type: RECEIVE_ERRORS,
19 | errors
20 | });
21 |
22 | export const fetchAllResponses = (data) => dispatch => {
23 | return(ResponseAPIUtil.responseIndex(data)
24 | .then(responses => dispatch(receiveAllResponses(responses)),
25 | err => dispatch(receiveErrors(err.responseJSON)))
26 | );
27 | };
28 |
29 | export const fetchResponse = (responseId) => dispatch => (
30 | ResponseAPIUtil.responseShow(responseId)
31 | .then(response => dispatch(receiveResponse(response)),
32 | err => dispatch(receiveErrors(err.responseJSON)))
33 | );
34 |
35 | export const createResponse = (response) => dispatch => {
36 | return (
37 | ResponseAPIUtil.responseCreate(response)
38 | .then(response => dispatch(receiveResponse(response)),
39 | err => dispatch(receiveErrors(err.responseJSON)))
40 | );
41 | };
42 |
43 | export const updateResponse = (response) => dispatch => {
44 | return (
45 | ResponseAPIUtil.responseUpdate(response)
46 | .then(response => dispatch(receiveResponse(response)),
47 | err => dispatch(receiveErrors(err.responseJSON)))
48 | );
49 | };
50 |
--------------------------------------------------------------------------------
/frontend/actions/session_actions.js:
--------------------------------------------------------------------------------
1 |
2 | import * as SessionAPIUtil from '../util/session_api_util';
3 |
4 | export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER";
5 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
6 |
7 | export const receiveCurrentUser = currentUser => ({
8 | type: RECEIVE_CURRENT_USER,
9 | currentUser
10 | });
11 |
12 | export const receiveErrors = errors => ({
13 | type: RECEIVE_ERRORS,
14 | errors
15 | });
16 |
17 | export const signup = user => dispatch => {
18 | return (
19 | SessionAPIUtil.signup(user)
20 | .then(user => dispatch(receiveCurrentUser(user)),
21 | err => dispatch(receiveErrors(err.responseJSON)))
22 | );
23 | };
24 |
25 | export const login = user => dispatch => (
26 | SessionAPIUtil.login(user)
27 | .then(user => dispatch(receiveCurrentUser(user)),
28 | err => dispatch(receiveErrors(err.responseJSON)))
29 | );
30 |
31 | export const logout = user => dispatch => (
32 | SessionAPIUtil.logout(user).then(user => dispatch(receiveCurrentUser(null)),
33 | err => dispatch(receiveErrors(err.responseJSON)))
34 | );
35 |
--------------------------------------------------------------------------------
/frontend/actions/story_actions.js:
--------------------------------------------------------------------------------
1 | import * as StoryAPIUtil from '../util/story_api_util';
2 |
3 | export const RECEIVE_ALL_STORIES = "RECEIVE_ALL_STORIES";
4 | export const RECEIVE_STORY = "RECEIVE_STORY";
5 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
6 |
7 | export const START_LOADING_ALL_STORIES = 'START_LOADING_ALL_STORIES';
8 | export const START_LOADING_SINGLE_STORY = 'START_LOADING_SINGLE_STORY';
9 |
10 | export const startLoadingAllStories = () => ({
11 | type: START_LOADING_ALL_STORIES
12 | });
13 |
14 | export const startLoadingSingleStory = () => ({
15 | type: START_LOADING_SINGLE_STORY
16 | });
17 |
18 | export const receiveAllStories = stories => ({
19 | type: RECEIVE_ALL_STORIES,
20 | stories
21 | });
22 |
23 | export const receiveStory = story => ({
24 | type: RECEIVE_STORY,
25 | story
26 | });
27 |
28 | export const receiveErrors = errors => ({
29 | type: RECEIVE_ERRORS,
30 | errors
31 | });
32 |
33 | export const fetchAllStories = (data) => (dispatch) => {
34 | dispatch(startLoadingAllStories());
35 | return(
36 | StoryAPIUtil.storyIndex(data)
37 | .then(stories => dispatch(receiveAllStories(stories)),
38 | err => dispatch(receiveErrors(err.responseJSON)))
39 | );
40 | }
41 |
42 | export const fetchStory = (storyId) => (dispatch) => {
43 | dispatch(startLoadingSingleStory());
44 | return(
45 | StoryAPIUtil.storyShow(storyId)
46 | .then(story => dispatch(receiveStory(story)),
47 | err => dispatch(receiveErrors(err.responseJSON)))
48 | );
49 | }
50 |
51 | export const createStory = (story) => dispatch => {
52 | return (
53 | StoryAPIUtil.storyCreate(story)
54 | .then(newStory => dispatch(receiveStory(newStory)),
55 | err => dispatch(receiveErrors(err.responseJSON)))
56 | );
57 | };
58 |
59 | export const updateStory = (story) => dispatch => {
60 | return (
61 | StoryAPIUtil.storyUpdate(story)
62 | .then(story => dispatch(receiveStory(story)),
63 | err => dispatch(receiveErrors(err.responseJSON)))
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/frontend/actions/topic_actions.js:
--------------------------------------------------------------------------------
1 | import * as TopicAPIUtil from '../util/topic_api_util';
2 |
3 | export const RECEIVE_ALL_TOPICS = "RECEIVE_ALL_TOPICS";
4 |
5 | export const receiveAllTopics = topics => ({
6 | type: RECEIVE_ALL_TOPICS,
7 | topics
8 | });
9 |
10 | export const fetchAllTopics = (data) => dispatch => {
11 | return(TopicAPIUtil.topicIndex(data)
12 | .then(topics => dispatch(receiveAllTopics(topics)))
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/frontend/actions/user_actions.js:
--------------------------------------------------------------------------------
1 | import * as UserAPIUtil from '../util/user_api_util';
2 |
3 | export const RECEIVE_ALL_USERS = "RECEIVE_ALL_USERS";
4 | export const RECEIVE_USER = "RECEIVE_USER";
5 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
6 |
7 | export const START_LOADING_USER = 'START_LOADING_USER';
8 |
9 | export const startLoadingUser = () => ({
10 | type: START_LOADING_USER
11 | });
12 |
13 | export const receiveUser = user => ({
14 | type: RECEIVE_USER,
15 | user
16 | });
17 |
18 | export const receiveErrors = errors => ({
19 | type: RECEIVE_ERRORS,
20 | errors
21 | });
22 |
23 | export const fetchUser = (userId) => (dispatch) => {
24 | dispatch(startLoadingUser());
25 | return(
26 | UserAPIUtil.userShow(userId)
27 | .then(user => dispatch(receiveUser(user)),
28 | err => dispatch(receiveErrors(err.responseJSON)))
29 | );
30 | }
31 |
32 | export const updateUser = (user) => dispatch => {
33 | return (
34 | UserAPIUtil.userUpdate(user)
35 | .then(user => dispatch(receiveUser(user)),
36 | err => dispatch(receiveErrors(err.responseJSON)))
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/components/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import HomeContainer from './home/home_container';
4 |
5 |
6 | const App = ({ children }) => (
7 |
8 | < HomeContainer />
9 | { children }
10 |
11 | );
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/frontend/components/auth_form/auth_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, withRouter, hashHistory } from 'react-router';
3 |
4 | class AuthForm extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { username: "", password: ""};
8 | this.handleLogin = this.handleLogin.bind(this);
9 | this.guestLogin = this.guestLogin.bind(this);
10 | this.formWrapperRedirect = this.formWrapperRedirect.bind(this);
11 |
12 | this.backPath = this.props.location.pathname.slice(0, -7);
13 | }
14 |
15 | componentDidMount() {
16 | this.redirectIfLoggedIn();
17 | this.props.receiveErrors([]);
18 | this.fade();
19 | }
20 |
21 | componentDidUpdate() {
22 | this.redirectIfLoggedIn();
23 | }
24 | redirectIfLoggedIn() {
25 | if (this.props.loggedIn) {
26 | hashHistory.push(this.backPath);
27 | }
28 | }
29 |
30 | fade() {
31 | $('#form-wrapper').animate({
32 | opacity: 1,}, 200);
33 | }
34 |
35 | formWrapperRedirect(e) {
36 | if($(e.target).attr('class') === 'form-wrapper'){
37 | this.props.router.push(this.backPath);
38 | }
39 | }
40 |
41 | updateField(field) {
42 | return e => this.setState({
43 | [field]: e.currentTarget.value
44 | });
45 | }
46 |
47 | handleLogin(e) {
48 | e.preventDefault();
49 | const user = this.state;
50 | this.props.login({user});
51 | }
52 |
53 | guestLogin(e) {
54 | e.preventDefault();
55 | let guest = {user: {username: 'guest', password: 'password'}};
56 | this.props.login(guest).then(() => hashHistory.push('/'));
57 | }
58 |
59 | renderErrors() {
60 | return(
61 |
62 | {this.props.errors.map((error, i) => (
63 | -
64 | {error}
65 |
66 | ))}
67 |
68 | );
69 | }
70 |
71 | render(){
72 |
73 | return (
74 |
112 | );
113 | }
114 |
115 | }
116 |
117 | export default withRouter(AuthForm);
118 |
--------------------------------------------------------------------------------
/frontend/components/auth_form/auth_form_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { login, logout, signup, receiveErrors } from '../../actions/session_actions';
3 | import AuthForm from './auth_form';
4 |
5 |
6 | const mapStateToProps = (state) => ({
7 | loggedIn: Boolean(state.session.currentUser),
8 | errors: state.session.errors
9 | });
10 |
11 | const mapDispatchToProps = (dispatch) => {
12 | return {
13 | login: user => dispatch(login(user)),
14 | receiveErrors: errors => dispatch(receiveErrors(errors))
15 | };
16 | };
17 |
18 | export default connect(
19 | mapStateToProps,
20 | mapDispatchToProps
21 | )(AuthForm);
22 |
--------------------------------------------------------------------------------
/frontend/components/follow_user/follow_user.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class FollowUser extends React.Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | this.state = {isFollowedByCurrentUser: false,
8 | authorFollowerIds: this.props.authorFollowerIds};
9 |
10 | this.handleFollow = this.handleFollow.bind(this);
11 | this.handleUnfollow = this.handleUnfollow.bind(this);
12 | }
13 |
14 | componentDidMount(){
15 | if(this.props.loggedIn && this.props.authorFollowerIds){
16 | if(this.props.authorFollowerIds.includes(this.props.currentUser.id)){
17 | this.setState({isFollowedByCurrentUser: true});
18 | }
19 | }
20 | this.setState({authorFollowerIds: this.props.authorFollowerIds});
21 | }
22 |
23 | componentWillReceiveProps(nextProps) {
24 | if(this.props.loggedIn && this.props.authorFollowerIds){
25 | if(nextProps.authorFollowerIds.includes(this.props.currentUser.id)){
26 | this.setState({isFollowedByCurrentUser: true});
27 | }
28 | }
29 | this.setState({authorFollowerIds: this.props.authorFollowerIds});
30 | }
31 |
32 |
33 | handleUnfollow(e){
34 | e.preventDefault();
35 | let authorFollowerIdsArr = this.state.authorFollowerIds;
36 | let index = authorFollowerIdsArr.indexOf(this.props.currentUser.id);
37 | authorFollowerIdsArr.splice(index, 1);
38 | this.setState({isFollowedByCurrentUser: false,
39 | authorFollowerIds: authorFollowerIdsArr});
40 |
41 | let unfollowData = {
42 | followerId: this.props.currentUser.id,
43 | followingId: this.props.authorId
44 | };
45 | this.props.destroyFollowing(unfollowData);
46 | }
47 |
48 | handleFollow(e){
49 | e.preventDefault();
50 | let authorFollowerIdsArr = this.state.authorFollowerIds;
51 | authorFollowerIdsArr.push(this.props.currentUser.id);
52 | this.setState({isFollowedByCurrentUser: true,
53 | authorFollowerIds: authorFollowerIdsArr});
54 |
55 | let followData = {following:{
56 | follower_id: this.props.currentUser.id,
57 | following_id: this.props.authorId
58 | }
59 | };
60 | this.props.createFollowing(followData);
61 | }
62 |
63 | render(){
64 | let followThis;
65 | if(this.props.loggedIn && this.state.authorFollowerIds){
66 | if(this.props.currentUser.id === this.props.authorId){
67 | followThis = ();
68 | } else {
69 | if(this.state.authorFollowerIds.includes(this.props.currentUser.id)){
70 | followThis = ();
71 | } else {
72 | followThis = ();
73 | }
74 | }
75 | }
76 |
77 | return(
78 |
79 | {followThis}
80 |
81 | )
82 | }
83 | }
84 |
85 | export default FollowUser;
86 |
--------------------------------------------------------------------------------
/frontend/components/home/home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, withRouter, hashHistory } from 'react-router';
3 |
4 | import HomeNavContainer from '../home_nav/home_nav_container';
5 | import StoriesFeed from '../stories_feed/stories_feed';
6 | import HomeFeed from '../home_feed/home_feed';
7 | import LoadingIcon from '../loading_icon/loading_icon';
8 |
9 | class Home extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {scrollTop: 0, scrollDir: 'down'};
13 |
14 | this.handleScroll = this.handleScroll.bind(this);
15 | this.topicName;
16 | }
17 |
18 | componentDidMount() {
19 | window.addEventListener('scroll', this.handleScroll);
20 | if(this.props.route){
21 | if(this.props.route.path === "/topics/:id"){
22 | this.props.fetchAllTopics();
23 | this.props.fetchAllStories({topicId: this.props.params.id});
24 | }
25 | } else {
26 | this.props.fetchAllTopics();
27 | this.props.fetchAllStories();
28 | }
29 | }
30 |
31 | componentWillReceiveProps(nextProps){
32 | if(nextProps.route){
33 | if((nextProps.route.path === "/topics/:id") && (nextProps.params.id != this.props.params.id)){
34 | this.props.fetchAllTopics();
35 | this.props.fetchAllStories({topicId: nextProps.params.id});
36 | }
37 | } else if (!nextProps.route && this.props.route) {
38 | this.props.fetchAllTopics();
39 | this.props.fetchAllStories();
40 | }
41 | }
42 |
43 | componentWillUnmount() {
44 | window.removeEventListener('scroll', this.handleScroll);
45 | }
46 |
47 | handleScroll(event) {
48 | if(($(document).scrollTop()) > this.state.scrollTop){
49 | this.setState({scrollDir: 'down'});
50 | } else {
51 | this.setState({scrollDir: 'up'});
52 | }
53 | this.setState({scrollTop: $(document).scrollTop()});
54 | }
55 |
56 | render() {
57 |
58 | if(this.props.loading){
59 | return();
60 | }
61 |
62 | if(this.props.route){
63 | let paramId = parseInt(this.props.params.id);
64 | let name;
65 | this.props.topics.forEach((topic) => {
66 | if(topic.id === paramId){
67 | name = topic.name
68 | }
69 | });
70 | this.topicName = name;
71 | }
72 |
73 | return(
74 |
75 | {this.props.children}
76 | < HomeNavContainer scrollDir={this.state.scrollDir} scrollTop={this.state.scrollTop} topics={this.props.topics}/>
77 |
78 |
79 |
80 |
81 |
82 |
83 | < HomeFeed stories={this.props.stories} topicName={this.topicName} />
84 |
85 |
86 |
87 |

88 |
89 |
90 | );
91 | }
92 |
93 | }
94 |
95 | export default withRouter(Home);
96 |
--------------------------------------------------------------------------------
/frontend/components/home/home_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchAllStories } from './../../actions/story_actions';
3 | import { fetchAllTopics } from './../../actions/topic_actions';
4 | import Home from './home';
5 |
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return({
9 | loggedIn: Boolean(state.session.currentUser),
10 | currentUser: state.session.currentUser,
11 | stories: Object.values(state.stories),
12 | topics: Object.values(state.topics),
13 | loading: state.loading.indexLoading
14 | });
15 | };
16 |
17 | const mapDispatchToProps = (dispatch) => {
18 | return {
19 | fetchAllStories: (id) => dispatch(fetchAllStories(id)),
20 | fetchAllTopics: () => dispatch(fetchAllTopics())
21 | };
22 | };
23 |
24 | export default connect(
25 | mapStateToProps,
26 | mapDispatchToProps
27 | )(Home);
28 |
--------------------------------------------------------------------------------
/frontend/components/home_feed/home_feed.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import HomeFeedItem from './home_feed_item';
5 |
6 | class HomeFeed extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render(){
12 | let stories;
13 | if(this.props.stories){
14 |
15 | let orderedStories = this.props.stories.sort((a, b) => { return b.created_at.localeCompare(a.created_at); });
16 | // select the first 23 stories for the home page
17 | let firstFour = orderedStories.slice(0,4);
18 | let secondFour = orderedStories.slice(4,8);
19 | let thirdFive = orderedStories.slice(8,13);
20 | let fourthFive = orderedStories.slice(13,18);
21 | let fifthFive = orderedStories.slice(18,23);
22 |
23 | firstFour = firstFour.map((story) => {
24 | return(
25 | < HomeFeedItem story={story} key={story.id} />
26 | );
27 | });
28 |
29 | secondFour = secondFour.map((story) => {
30 | return(
31 | < HomeFeedItem story={story} key={story.id} />
32 | );
33 | });
34 |
35 | thirdFive = thirdFive.map((story) => {
36 | return(
37 | < HomeFeedItem story={story} key={story.id} />
38 | );
39 | });
40 |
41 | fourthFive = fourthFive.map((story) => {
42 | return(
43 | < HomeFeedItem story={story} key={story.id} />
44 | );
45 | });
46 |
47 | fifthFive = fifthFive.map((story) => {
48 | return(
49 | < HomeFeedItem story={story} key={story.id} />
50 | );
51 | });
52 |
53 |
54 | stories = (
55 |
{firstFour}
56 |
{secondFour}
57 |
{thirdFive}
58 |
{fourthFive}
59 |
{fifthFive}
60 |
);
61 | }
62 |
63 | return(
64 |
65 |
66 | {this.props.topicName}
67 |
68 | { stories }
69 |
70 | );
71 | }
72 | }
73 |
74 | export default HomeFeed;
75 |
--------------------------------------------------------------------------------
/frontend/components/home_feed/home_feed_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import * as MUtil from '../../util/m_util';
5 |
6 | class HomeFeedItem extends React.Component {
7 |
8 | constructor(props){
9 | super(props);
10 | }
11 |
12 | render(){
13 | return(
14 |
15 |

16 |
17 |
18 |
19 |
20 |
21 | { this.props.story.title }
22 |
23 |
24 |
25 | { this.props.story.description }
26 |
27 |
28 |
29 |
30 | { this.props.story.author_name }
31 |
32 |
33 |
34 |
35 | { MUtil.formatDate(this.props.story.date.split(',')) }
36 |
37 |
38 |
39 | );}
40 |
41 | }
42 |
43 | export default HomeFeedItem;
44 |
--------------------------------------------------------------------------------
/frontend/components/home_nav/home_nav_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { logout, login } from '../../actions/session_actions';
3 | import HomeNav from './home_nav';
4 |
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | return({
8 | loggedIn: Boolean(state.session.currentUser),
9 | currentUser: state.session.currentUser,
10 | scrollDir: ownProps.scrollDir,
11 | scrollTop: ownProps.scrollTop,
12 | topics: ownProps.topics
13 | });
14 | };
15 |
16 | const mapDispatchToProps = (dispatch) => {
17 | return {
18 | logout: user => dispatch(logout(user)),
19 | login: user => dispatch(login(user))
20 | };
21 | };
22 |
23 | export default connect(
24 | mapStateToProps,
25 | mapDispatchToProps
26 | )(HomeNav);
27 |
--------------------------------------------------------------------------------
/frontend/components/input_nav/input_nav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Link, withRouter, hashHistory } from 'react-router';
4 |
5 | import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
6 |
7 | import PublishOptions from '../publish_options/publish_options';
8 |
9 | class InputNav extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {publishOptions: ''}
13 |
14 | this.loadPublishOptions = this.loadPublishOptions.bind(this);
15 | }
16 |
17 | componentDidMount(){
18 | this.props.fetchAllTopics();
19 | }
20 |
21 | componentWillReceiveProps(newProps){
22 | }
23 |
24 | loadPublishOptions(e){
25 | e.preventDefault();
26 |
27 | this.setState({publishOptions: (
28 |
34 | )});
35 | }
36 |
37 | render() {
38 | let imageUrl;
39 | let userUrl;
40 |
41 | if(this.props.currentUser){
42 | imageUrl = this.props.currentUser.image_url;
43 | userUrl = `/users/${this.props.currentUser.id}`;
44 | }
45 |
46 | let avatarBox = (
47 |
48 |

49 |
50 |
);
51 |
52 | return(
53 |
54 |
55 |
56 |

57 |
58 |
59 |
68 | { avatarBox }
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | }
76 |
77 | export default withRouter(InputNav);
78 |
--------------------------------------------------------------------------------
/frontend/components/input_nav/input_nav_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { createStory, updateStory } from '../../actions/story_actions';
3 | import { fetchAllTopics } from './../../actions/topic_actions';
4 | import InputNav from './input_nav';
5 |
6 |
7 | const mapStateToProps = (state, ownProps) => {
8 | return({
9 | currentUser: state.session.currentUser,
10 | storyData: ownProps.storyData,
11 | topics: Object.values(state.topics),
12 | });
13 | };
14 |
15 | const mapDispatchToProps = (dispatch) => {
16 | return {
17 | createStory: story => dispatch(createStory(story)),
18 | updateStory: story => dispatch(updateStory(story)),
19 | fetchAllTopics: () => dispatch(fetchAllTopics())
20 | };
21 | };
22 |
23 | export default connect(
24 | mapStateToProps,
25 | mapDispatchToProps
26 | )(InputNav);
27 |
--------------------------------------------------------------------------------
/frontend/components/interior_nav/interior_nav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Link, withRouter, hashHistory } from 'react-router';
4 |
5 | class InteriorNav extends React.Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.handleLogout = this.handleLogout.bind(this);
10 | this.guestLogin = this.guestLogin.bind(this);
11 |
12 | this.mainClass = 'nav-outer';
13 | }
14 |
15 | componentDidMount(){
16 | }
17 |
18 | componentWillReceiveProps(newProps){
19 | const elem = ReactDOM.findDOMNode(this);
20 | if((newProps.scrollDir === 'up') && (newProps.scrollTop > 80)){
21 | elem.style.opacity = 0;
22 | elem.style.top = '-80px';
23 | this.mainClass = 'nav-outer-scroll';
24 | window.requestAnimationFrame(function() {
25 | elem.style.transition = "top 200ms, opacity 150ms";
26 | elem.style.opacity = 1;
27 | elem.style.top = '0px';
28 | });
29 | } else if((newProps.scrollDir === 'down') && (newProps.scrollTop > 80)) {
30 | let fadePromise = new Promise(function(resolve, reject){
31 | elem.style.opacity = 1;
32 | elem.style.top = '0px';
33 | window.requestAnimationFrame(function() {
34 | elem.style.transition = "top 200ms, opacity 150ms";
35 | elem.style.opacity = 0;
36 | elem.style.top = '-80px';
37 | });
38 | });
39 | fadePromise.then(()=>{this.mainClass = 'nav-outer';});
40 |
41 | }
42 | else if((newProps.scrollDir === 'down') && (newProps.scrollTop < 80)) {
43 | this.mainClass = 'nav-outer'; }
44 | else { this.mainClass = 'nav-outer-scroll'; }
45 | }
46 |
47 | handleLogout(e) {
48 | e.preventDefault();
49 | this.props.logout().then(() => hashHistory.push('/'));
50 | }
51 |
52 | guestLogin(e) {
53 | e.preventDefault();
54 | let guest = {user: {username: 'guest', password: 'password'}};
55 | this.props.login(guest).then(() => hashHistory.push('/'));
56 | }
57 |
58 | render() {
59 | const logoutButton =(
60 | );
63 |
64 | const sessionButtons = (
65 |
73 | );
74 |
75 | let helloMessage = ' ';
76 | let buttons;
77 | let avatarBox;
78 | let imageUrl;
79 |
80 | if(this.props.loggedIn){
81 | helloMessage = `Hello, ${this.props.currentUser.name}!`;
82 | buttons = logoutButton;
83 | imageUrl = this.props.currentUser.image_url;
84 | avatarBox = (
85 |
86 |

87 |
88 |
);
89 | } else {
90 | buttons = sessionButtons;
91 | }
92 |
93 | let writeLink;
94 | if(this.props.loggedIn){
95 | writeLink = '/write';
96 | } else {
97 | writeLink = `${this.props.location.pathname}/signin`;
98 | }
99 |
100 | return(
101 |
102 |
103 |
104 |

105 |
106 |
107 |
108 | { helloMessage }
109 |
110 |
111 | Write a Story
112 |
113 | { buttons }
114 |
115 |
116 |
117 | { avatarBox }
118 |
119 |
120 |
121 | );
122 | }
123 |
124 | }
125 |
126 | export default withRouter(InteriorNav);
127 |
--------------------------------------------------------------------------------
/frontend/components/interior_nav/interior_nav_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { logout, login } from '../../actions/session_actions';
3 | import InteriorNav from './interior_nav';
4 |
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | return({
8 | loggedIn: Boolean(state.session.currentUser),
9 | currentUser: state.session.currentUser,
10 | scrollDir: ownProps.scrollDir,
11 | scrollTop: ownProps.scrollTop
12 | });
13 | };
14 |
15 | const mapDispatchToProps = (dispatch) => {
16 | return {
17 | logout: user => dispatch(logout(user)),
18 | login: user => dispatch(login(user))
19 | };
20 | };
21 |
22 | export default connect(
23 | mapStateToProps,
24 | mapDispatchToProps
25 | )(InteriorNav);
26 |
--------------------------------------------------------------------------------
/frontend/components/loading_icon/loading_icon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LoadingIcon = () => (
4 |
5 |
6 | Loading...
7 |
8 |
9 | );
10 |
11 | export default LoadingIcon;
12 |
--------------------------------------------------------------------------------
/frontend/components/mixed_feed/mixed_feed.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import MixedFeedItem from './mixed_feed_item';
5 |
6 | class MixedFeed extends React.Component {
7 | // pass in an array of mixed items from the profile page
8 | constructor(props) {
9 | super(props);
10 |
11 | }
12 |
13 | render(){
14 | let mixedItems;
15 | let n = 0;
16 | if(this.props.mixedItems){
17 | mixedItems = this.props.mixedItems.map((mixedItem) => {
18 | n = n + 1;
19 | return(
20 | < MixedFeedItem
21 | mixedItem={mixedItem}
22 | currentUser={this.props.currentUser}
23 | loggedIn={this.props.loggedIn}
24 | destroyLike={this.props.destroyLike}
25 | createLike={this.props.createLike}
26 | responses={this.props.responses}
27 | key={n} />
28 | );
29 | });
30 | }
31 |
32 | return(
33 |
34 | { mixedItems }
35 |
36 | );
37 | }
38 | }
39 |
40 | export default MixedFeed;
41 |
--------------------------------------------------------------------------------
/frontend/components/mixed_feed/mixed_feed_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import * as MUtil from '../../util/m_util';
5 |
6 | import Response from '../response_section/response';
7 |
8 | class MixedFeedItem extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | }
12 |
13 | render(){
14 | let thisItem = ();
15 | let mainImagePath = this.props.mixedItem.story_main_image_url;
16 | let userImagePath = this.props.mixedItem.response_writer_photo_url;
17 | let storyDate;
18 |
19 | if(this.props.mixedItem.this_is === 'story'){
20 | storyDate = MUtil.formatDate(this.props.mixedItem.story_date.split(','));
21 |
22 | thisItem = (
23 |
24 |
25 |
26 |
27 |

28 |
29 |
30 |
31 |
32 |
33 |
34 | { this.props.mixedItem.story_title }
35 |
36 |
37 |
38 | { this.props.mixedItem.story_description }
39 |
40 |
41 |
42 |

43 |
44 |
45 |
46 |
47 | { this.props.mixedItem.story_author_name }
48 |
49 |
50 |
51 | { storyDate }
52 |
53 |
54 |
55 |
56 |
57 | );
58 | } else {
59 |
60 | let improvedResponse = this.props.mixedItem.response;
61 | improvedResponse.likes = this.props.mixedItem.response_likes;
62 | improvedResponse.writer_name = this.props.mixedItem.response_writer_name;
63 | improvedResponse.writer_photo_url = userImagePath;
64 | thisItem = ();
73 | }
74 |
75 | return({thisItem}
);
76 | }
77 |
78 | }
79 |
80 | export default MixedFeedItem;
81 |
--------------------------------------------------------------------------------
/frontend/components/publish_options/publish_options.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import * as MUtil from '../../util/m_util';
5 |
6 | class PublishOptions extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {topicId: 1, wrapperClass: 'form-wrapper-2'}
10 |
11 | this.handleStoryInput = this.handleStoryInput.bind(this);
12 | this.handleOptionChange = this.handleOptionChange.bind(this);
13 | this.changeWrapper = this.changeWrapper.bind(this);
14 | }
15 |
16 | componentDidMount(){
17 | if(this.props.storyData.topic){
18 | this.setState({topicId: this.props.storyData.topic.id, wrapperClass: 'form-wrapper-2'});
19 | }
20 | }
21 |
22 | componentWillReceiveProps(nextProps){
23 | // this.setState({wrapperClass: 'form-wrapper-2'});
24 | }
25 |
26 | handleStoryInput(e) {
27 | e.preventDefault();
28 |
29 | // will also pass in topic data from the new form below
30 | let formData = new FormData();
31 | let file = this.props.storyData.file;
32 |
33 | formData.append("story[author_id]", this.props.storyData.author_id);
34 | formData.append("story[title]", this.props.storyData.title);
35 | formData.append("story[description]", this.props.storyData.description);
36 | formData.append("story[body]", this.props.storyData.body);
37 | formData.append("story[date]", MUtil.encodeDate());
38 | formData.append("story[topic_id]", this.state.topicId);
39 | if(file){
40 | formData.append("story[main_image]", file);
41 | }
42 |
43 | if(this.props.storyData.id){
44 | formData.append("story[id]", this.props.storyData.id);
45 | this.props.updateStory(formData).then(() => hashHistory.push(`/stories/${this.props.storyData.id}`));;
46 | } else {
47 | // remember, the 'then' is receive the entire action
48 | this.props.createStory(formData).then(({story}) => hashHistory.push(`/stories/${story.id}`));
49 | }
50 | }
51 |
52 | handleOptionChange(e){
53 | this.setState({
54 | topicId: parseInt(e.target.value)
55 | });
56 | }
57 |
58 | changeWrapper(e){
59 | if($(e.target).attr('id') === 'publish-wrapper'){
60 | this.setState({
61 | wrapperClass: 'form-wrapper-3'
62 | });
63 | }
64 | }
65 |
66 | render(){
67 | let topics = this.props.topics;
68 | topics = topics.map((topic) => {
69 | return(
70 |
72 |
73 |
74 | );
75 | });
76 |
77 | return(
78 |
79 |
80 | Select a topic:
81 |
91 |
92 | );
93 | }
94 |
95 | }
96 |
97 | export default PublishOptions;
98 |
99 | //
100 | //
101 |
--------------------------------------------------------------------------------
/frontend/components/response_input/response_input.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Link, withRouter, hashHistory } from 'react-router';
4 |
5 | import * as MUtil from '../../util/m_util';
6 |
7 | class ResponseInput extends React.Component {
8 |
9 | constructor(props){
10 | super(props);
11 | this.state = {body: "", change: ""};
12 |
13 | this.handleResponseInput = this.handleResponseInput.bind(this);
14 | }
15 |
16 | componentDidMount() {
17 | this.props.receiveErrors([]);
18 | if(this.props.thisResponse){
19 | this.setState({body: this.props.thisResponse.body});
20 | }
21 | }
22 |
23 | componentWillReceiveProps(newProps){
24 | }
25 |
26 | updateField(field) {
27 | return e => this.setState({
28 | [field]: e.currentTarget.value
29 | });
30 | }
31 |
32 | handleResponseInput(e) {
33 | e.preventDefault();
34 | let body = this.state.body;
35 | let date = MUtil.encodeDate();
36 | let writer_id = this.props.currentUser.id;
37 | let story_id = this.props.storyId;
38 | let in_response_id = this.props.inResponseId;
39 |
40 | let responseData = {response:{
41 | body: body,
42 | date: date,
43 | writer_id: writer_id,
44 | story_id: story_id,
45 | in_response_id: in_response_id
46 | }
47 | };
48 |
49 | if(this.props.thisResponse){
50 | responseData.this_id = this.props.thisResponse.id;
51 | this.props.updateResponse(responseData);
52 | } else {
53 | this.props.createResponse(responseData);
54 | }
55 |
56 | }
57 |
58 | renderErrors() {
59 | return(
60 |
61 | {this.props.errors.map((error, i) => (
62 | -
63 | {error}
64 |
65 | ))}
66 |
67 | );
68 | }
69 |
70 |
71 | render(){
72 | let buttonText = 'Respond';
73 |
74 | if(this.props.thisResponse){
75 | buttonText = 'Edit your response'
76 | }
77 |
78 | let spacerDiv = {height: '15px', display: 'block'};
79 |
80 | return(
81 |
96 | );
97 | }
98 |
99 | }
100 |
101 | export default withRouter(ResponseInput);
102 |
--------------------------------------------------------------------------------
/frontend/components/response_input/response_input_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { createResponse, fetchResponse, updateResponse, receiveErrors } from '../../actions/response_actions';
3 | import ResponseInput from './response_input';
4 |
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let thisResponse;
8 | if(ownProps.thisResponse){
9 | // indicates if you're editing a preexisting comment
10 | thisResponse = ownProps.thisResponse;
11 | }
12 | return ({
13 | loggedIn: Boolean(state.session.currentUser),
14 | currentUser: state.session.currentUser,
15 | storyId: ownProps.storyId,
16 | inResponseId: ownProps.inResponseId,
17 | makeVisible: ownProps.makeVisible,
18 | thisResponse: thisResponse,
19 | errors: state.session.errors
20 | });
21 | };
22 |
23 | const mapDispatchToProps = (dispatch) => {
24 | return {
25 | createResponse: response => dispatch(createResponse(response)),
26 | fetchResponse: response => dispatch(fetchResponse(response)),
27 | updateResponse: response => dispatch(updateResponse(response)),
28 | receiveErrors: errors => dispatch(receiveErrors(errors))
29 | };
30 | };
31 |
32 | export default connect(
33 | mapStateToProps,
34 | mapDispatchToProps
35 | )(ResponseInput);
36 |
--------------------------------------------------------------------------------
/frontend/components/response_section/response_section.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, withRouter, hashHistory } from 'react-router';
3 |
4 | import Response from './response';
5 |
6 | class ResponseSection extends React.Component {
7 |
8 | constructor(props){
9 | super(props);
10 |
11 | this.flatten = this.flatten.bind(this);
12 | }
13 |
14 | // flatten arrays
15 | flatten(ary) {
16 | var ret = [];
17 | for(var i = 0; i < ary.length; i++) {
18 | if(Array.isArray(ary[i])) {
19 | ret = ret.concat(this.flatten(ary[i]));
20 | } else {
21 | ret.push(ary[i]);
22 | }
23 | }
24 | return ret;
25 | }
26 |
27 | render(){
28 | let responses;
29 | if(this.props.responses){
30 |
31 | let allResponses = this.props.responses;
32 | let obj = {};
33 | let tempArr = [];
34 |
35 | allResponses.forEach((response) => {
36 | if(response.in_response_id){
37 | tempArr.push(response);
38 | } else {
39 | obj[response.id] = [];
40 | obj[response.id].push(response);
41 | }
42 | });
43 |
44 | tempArr.forEach((response) => {
45 | obj[response.in_response_id].push(response);
46 | });
47 |
48 | let ordered = Object.values(obj);
49 |
50 | ordered = this.flatten(ordered);
51 |
52 | responses = ordered.map((response) => {
53 | let isChild = false;
54 | if(response.in_response_id){
55 | isChild = true;
56 | }
57 | return(
58 | < Response response={response}
59 | storyId={this.props.storyId}
60 | isChild = {isChild}
61 | loggedIn = {this.props.loggedIn}
62 | currentUser = {this.props.currentUser}
63 | createLike = {this.props.createLike}
64 | destroyLike = {this.props.destroyLike}
65 | key={response.id} />
66 | );
67 | });
68 |
69 | }
70 |
71 | return(
72 |
75 | );
76 | }
77 |
78 | }
79 |
80 | export default withRouter(ResponseSection);
81 |
--------------------------------------------------------------------------------
/frontend/components/response_section/response_section_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchAllLikes, fetchLike, createLike, destroyLike } from '../../actions/like_actions';
3 | import ResponseSection from './response_section';
4 |
5 | const mapStateToProps = (state, ownProps) => {
6 | return ({
7 | loggedIn: Boolean(state.session.currentUser),
8 | currentUser: state.session.currentUser,
9 | responses: ownProps.responses,
10 | storyId: ownProps.storyId,
11 | writerId: ownProps.writerId
12 | });
13 | };
14 |
15 | const mapDispatchToProps = (dispatch) => {
16 | return {
17 | fetchAllLikes: id => dispatch(fetchAllLikes(id)),
18 | fetchLike: like => dispatch(fetchLike(like)),
19 | createLike: like => dispatch(createLike(like)),
20 | updateLike: like => dispatch(updateLike(like)),
21 | destroyLike: like => dispatch(destroyLike(like))
22 | };
23 | };
24 |
25 | export default connect(
26 | mapStateToProps,
27 | mapDispatchToProps
28 | )(ResponseSection);
29 |
--------------------------------------------------------------------------------
/frontend/components/root.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { Router, Route, hashHistory, IndexRoute } from 'react-router';
4 |
5 | import App from './app';
6 |
7 | import AuthFormContainer from './auth_form/auth_form_container';
8 | import SignupFormContainer from './signup_form/signup_form_container';
9 | import StoryInputContainer from './story_input/story_input_container';
10 | import StoryContainer from './story/story_container';
11 | import UserProfileContainer from './user_profile/user_profile_container';
12 | import HomeContainer from './home/home_container';
13 |
14 | const Root = ({ store }) => (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 |
40 | export default Root;
41 |
--------------------------------------------------------------------------------
/frontend/components/signup_form/signup_form_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { login, logout, signup, receiveErrors } from '../../actions/session_actions';
3 | import SignupForm from './signup_form';
4 |
5 |
6 | const mapStateToProps = (state) => ({
7 | loggedIn: Boolean(state.session.currentUser),
8 | errors: state.session.errors
9 | });
10 |
11 | const mapDispatchToProps = (dispatch) => {
12 | return {
13 | signup: user => dispatch(signup(user)),
14 | receiveErrors: errors => dispatch(receiveErrors(errors))
15 | };
16 | };
17 |
18 | export default connect(
19 | mapStateToProps,
20 | mapDispatchToProps
21 | )(SignupForm);
22 |
--------------------------------------------------------------------------------
/frontend/components/stories_feed/stories_feed.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import StoriesFeedItem from './stories_feed_item';
5 |
6 | class StoriesFeed extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render(){
12 | let stories;
13 | if(this.props.stories){
14 | stories = this.props.stories.map((story) => {
15 | return(
16 | < StoriesFeedItem story={story} key={story.id} />
17 | );
18 | });
19 | }
20 |
21 | return(
22 |
25 | );
26 | }
27 | }
28 |
29 | export default StoriesFeed;
30 |
--------------------------------------------------------------------------------
/frontend/components/stories_feed/stories_feed_item.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | const StoriesFeedItem = ({ story }) => (
5 |
6 |
7 |
8 |
9 | { story.title }
10 |
11 |
12 | { story.author_name }
13 |
14 | { story.date }
15 |
16 | { story.description }
17 |
18 |
19 | );
20 |
21 | export default StoriesFeedItem;
22 |
--------------------------------------------------------------------------------
/frontend/components/story/story_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchAllStories, fetchStory } from '../../actions/story_actions';
3 | import { fetchAllResponses, fetchResponse } from '../../actions/response_actions';
4 | import { fetchAllLikes, fetchLike, createLike, destroyLike } from '../../actions/like_actions';
5 | import { fetchAllFollowings, fetchFollowing, createFollowing, destroyFollowing } from '../../actions/following_actions';
6 | import Story from './story';
7 |
8 |
9 | const mapStateToProps = (state, ownProps) => {
10 | return({
11 | loggedIn: Boolean(state.session.currentUser),
12 | currentUser: state.session.currentUser,
13 | story: state.stories[ownProps.params.id],
14 | responses: Object.values(state.responses),
15 | likes: state.likes,
16 | loading: state.loading.detailLoading
17 | });
18 | };
19 |
20 | const mapDispatchToProps = (dispatch) => {
21 | return {
22 | fetchAllStories: () => dispatch(fetchAllStories()),
23 | fetchStory: storyId => dispatch(fetchStory(storyId)),
24 | fetchAllResponses: storyId => dispatch(fetchAllResponses(storyId)),
25 | fetchResponse: responseId => dispatch(fetchResponse(responseId)),
26 | fetchAllLikes: id => dispatch(fetchAllLikes(id)),
27 | fetchLike: likeId => dispatch(fetchAllLikes(likeId)),
28 | createLike: like => dispatch(createLike(like)),
29 | destroyLike: like => dispatch(destroyLike(like)),
30 | fetchAllFollowings: id => dispatch(fetchAllFollowings(id)),
31 | fetchFollowing: followingId => dispatch(fetchAllFollowings(followingId)),
32 | createFollowing: following => dispatch(createFollowing(following)),
33 | destroyFollowing: following => dispatch(destroyFollowing(following))
34 | };
35 | };
36 |
37 | export default connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | )(Story);
41 |
--------------------------------------------------------------------------------
/frontend/components/story_input/story_input_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { createStory, fetchStory, updateStory, receiveErrors } from '../../actions/story_actions';
3 | import StoryInput from './story_input';
4 |
5 |
6 | const mapStateToProps = (state, ownProps) => {
7 | let story;
8 | if(ownProps.params.id){
9 | story = state.stories[ownProps.params.id];
10 | }
11 | return ({
12 | loggedIn: Boolean(state.session.currentUser),
13 | currentUser: state.session.currentUser,
14 | errors: state.session.errors,
15 | story: story
16 | });
17 | };
18 |
19 | const mapDispatchToProps = (dispatch) => {
20 | return {
21 | createStory: story => dispatch(createStory(story)),
22 | fetchStory: story => dispatch(fetchStory(story)),
23 | updateStory: story => dispatch(updateStory(story)),
24 | receiveErrors: errors => dispatch(receiveErrors(errors))
25 | };
26 | };
27 |
28 | export default connect(
29 | mapStateToProps,
30 | mapDispatchToProps
31 | )(StoryInput);
32 |
--------------------------------------------------------------------------------
/frontend/components/story_sidebar/story_sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ScrollableAnchor from 'react-scrollable-anchor';
3 |
4 | class StorySidebar extends React.Component {
5 |
6 | constructor(props) {
7 | super(props);
8 | this.state = {likerIds: []};
9 |
10 | this.handleLike = this.handleLike.bind(this);
11 | this.handleUnlike = this.handleUnlike.bind(this);
12 | }
13 |
14 | componentDidMount(){
15 | this.setState({likerIds: this.props.likerIds})
16 | }
17 |
18 | componentWillReceiveProps(nextProps) {
19 | if(nextProps.likerIds.length != this.props.likerIds.length){
20 | this.setState({likerIds: nextProps.likerIds})
21 | }
22 | }
23 |
24 | handleUnlike(e){
25 | e.preventDefault();
26 | let likerIdsArr = this.state.likerIds;
27 | let index = likerIdsArr.indexOf(this.props.currentUser.id);
28 | likerIdsArr.splice(index, 1);
29 | this.setState({likerIds: likerIdsArr});
30 |
31 | let unlikeData = {
32 | likerId: this.props.currentUser.id,
33 | storyId: this.props.storyId
34 | };
35 | this.props.destroyLike(unlikeData);
36 | }
37 |
38 | handleLike(e){
39 | e.preventDefault();
40 | let likerIdsArr = this.state.likerIds;
41 | likerIdsArr.push(this.props.currentUser.id);
42 | this.setState({likerIds: likerIdsArr});
43 |
44 | let likeData = {like:{
45 | liker_id: this.props.currentUser.id,
46 | story_id: this.props.storyId
47 | }
48 | };
49 | this.props.createLike(likeData);
50 | }
51 |
52 |
53 | render(){
54 | let likeThis;
55 |
56 | if(this.props.loggedIn){
57 | if(this.state.likerIds.includes(this.props.currentUser.id)){
58 | likeThis = ();
63 | } else {
64 | likeThis = ();
71 | }
72 | } else {
73 | likeThis = ();
79 | }
80 |
81 |
82 | return(
83 |
84 | {likeThis}
85 |
86 | {this.state.likerIds.length}
87 |
88 |
89 | )
90 | }
91 |
92 | }
93 |
94 | export default StorySidebar;
95 |
--------------------------------------------------------------------------------
/frontend/components/user_profile/following.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import MixedFeed from '../mixed_feed/mixed_feed';
5 |
6 | class Following extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render(){
12 | return(
13 |
14 |
22 |
23 | );
24 | }
25 | }
26 |
27 | export default Following;
28 |
--------------------------------------------------------------------------------
/frontend/components/user_profile/latest.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import MixedFeed from '../mixed_feed/mixed_feed';
5 |
6 | class Latest extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render(){
12 | return(
13 |
14 |
22 |
23 | );
24 | }
25 | }
26 |
27 | export default Latest;
28 |
--------------------------------------------------------------------------------
/frontend/components/user_profile/recommended.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 |
4 | import MixedFeed from '../mixed_feed/mixed_feed';
5 |
6 | class Recommended extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | }
10 |
11 | render(){
12 | return(
13 |
14 |
22 |
23 | );
24 | }
25 | }
26 |
27 | export default Recommended;
28 |
--------------------------------------------------------------------------------
/frontend/components/user_profile/user_profile_container.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { fetchAllStories } from './../../actions/story_actions';
3 | import { fetchAllResponses } from './../../actions/response_actions';
4 | import { fetchUser } from './../../actions/user_actions';
5 | import { createLike, destroyLike } from '../../actions/like_actions';
6 | import { createFollowing, destroyFollowing } from '../../actions/following_actions';
7 | import UserProfile from './user_profile';
8 |
9 | const mapStateToProps = (state, ownProps) => {
10 | return({
11 | loggedIn: Boolean(state.session.currentUser),
12 | currentUser: state.session.currentUser,
13 | user: state.users[ownProps.params.id],
14 | responses: Object.values(state.responses),
15 | loading: state.loading.indexLoading
16 | });
17 | };
18 |
19 | const mapDispatchToProps = (dispatch) => {
20 | return {
21 | fetchUser: (id) => dispatch(fetchUser(id)),
22 | createLike: like => dispatch(createLike(like)),
23 | destroyLike: like => dispatch(destroyLike(like)),
24 | createFollowing: like => dispatch(createFollowing(like)),
25 | destroyFollowing: like => dispatch(destroyFollowing(like))
26 | };
27 | };
28 |
29 | export default connect(
30 | mapStateToProps,
31 | mapDispatchToProps
32 | )(UserProfile);
33 |
--------------------------------------------------------------------------------
/frontend/message_entry.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import configureStore from './store/store';
5 | import Root from './components/root';
6 |
7 | document.addEventListener('DOMContentLoaded', () => {
8 |
9 | let store;
10 | if (window.currentUser) {
11 | const preloadedState = { session: { currentUser: window.currentUser, errors: [] } };
12 | store = configureStore(preloadedState);
13 | } else {
14 | store = configureStore();
15 | }
16 |
17 | return(
18 | ReactDOM.render(,
19 | document.getElementById('root'))
20 | );
21 | }
22 | );
23 |
--------------------------------------------------------------------------------
/frontend/reducers/followings_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_FOLLOWINGS,
3 | RECEIVE_FOLLOWING } from '../actions/following_actions';
4 |
5 | import merge from 'lodash/merge';
6 |
7 | const FollowingsReducer = (state = {}, action) => {
8 | Object.freeze(state);
9 | let newState;
10 | switch(action.type) {
11 | case RECEIVE_ALL_FOLLOWINGS:
12 | return action.followings;
13 | case RECEIVE_FOLLOWING:
14 | return merge({}, state, {[action.following.id]: action.following});
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default FollowingsReducer;
21 |
--------------------------------------------------------------------------------
/frontend/reducers/likes_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_LIKES,
3 | RECEIVE_LIKE } from '../actions/like_actions';
4 |
5 | import merge from 'lodash/merge';
6 |
7 | const LikesReducer = (state = {}, action) => {
8 | Object.freeze(state);
9 | let newState;
10 | switch(action.type) {
11 | case RECEIVE_ALL_LIKES:
12 | return action.likes;
13 | case RECEIVE_LIKE:
14 | return merge({}, state, {[action.like.id]: action.like});
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default LikesReducer;
21 |
--------------------------------------------------------------------------------
/frontend/reducers/loading_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_STORIES,
3 | RECEIVE_STORY,
4 | START_LOADING_ALL_STORIES,
5 | START_LOADING_SINGLE_STORY } from '../actions/story_actions';
6 |
7 | import {
8 | START_LOADING_USER,
9 | RECEIVE_USER } from '../actions/user_actions';
10 |
11 | const initialState = {
12 | indexLoading: false,
13 | detailLoading: false
14 | };
15 |
16 | export default (state = initialState, action) => {
17 | Object.freeze(state);
18 | switch(action.type){
19 | case RECEIVE_ALL_STORIES:
20 | case RECEIVE_STORY:
21 | case RECEIVE_USER:
22 | return initialState;
23 | case START_LOADING_ALL_STORIES:
24 | case START_LOADING_USER:
25 | return Object.assign({}, state, { indexLoading: true });
26 | case START_LOADING_SINGLE_STORY:
27 | return Object.assign({}, state, { detailLoading: true });
28 | default:
29 | return state;
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/frontend/reducers/responses_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_RESPONSES,
3 | RECEIVE_RESPONSE } from '../actions/response_actions';
4 |
5 | import merge from 'lodash/merge';
6 |
7 | const ResponsesReducer = (state = {}, action) => {
8 | Object.freeze(state);
9 | let newState;
10 | switch(action.type) {
11 | case RECEIVE_ALL_RESPONSES:
12 | return action.responses;
13 | case RECEIVE_RESPONSE:
14 | return merge({}, state, {[action.response.id]: action.response});
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default ResponsesReducer;
21 |
--------------------------------------------------------------------------------
/frontend/reducers/root_reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import SessionReducer from './sessions_reducer';
3 | import StoriesReducer from './stories_reducer';
4 | import ResponsesReducer from './responses_reducer';
5 | import LikesReducer from './likes_reducer';
6 | import FollowingsReducer from './followings_reducer';
7 | import UsersReducer from './users_reducer';
8 | import TopicsReducer from './topics_reducer';
9 | import LoadingReducer from './loading_reducer';
10 |
11 | export default combineReducers({
12 | session: SessionReducer,
13 | stories: StoriesReducer,
14 | responses: ResponsesReducer,
15 | likes: LikesReducer,
16 | followings: FollowingsReducer,
17 | users: UsersReducer,
18 | topics: TopicsReducer,
19 | loading: LoadingReducer
20 | });
21 |
--------------------------------------------------------------------------------
/frontend/reducers/sessions_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_CURRENT_USER,
3 | RECEIVE_ERRORS } from '../actions/session_actions';
4 |
5 | import merge from 'lodash/merge';
6 |
7 | const _nullUser = Object.freeze({
8 | currentUser: null,
9 | errors: []
10 | });
11 |
12 | const SessionReducer = (state = _nullUser, action) => {
13 | Object.freeze(state);
14 | switch(action.type) {
15 | case RECEIVE_CURRENT_USER:
16 | const currentUser = action.currentUser;
17 | return merge({}, _nullUser, {currentUser});
18 | case RECEIVE_ERRORS:
19 | const errors = action.errors;
20 | return merge({}, {currentUser: state.currentUser}, {errors});
21 | default:
22 | return state;
23 | }
24 | };
25 |
26 | export default SessionReducer;
27 |
--------------------------------------------------------------------------------
/frontend/reducers/stories_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_STORIES,
3 | RECEIVE_STORY } from '../actions/story_actions';
4 |
5 | import merge from 'lodash/merge';
6 |
7 | const StoriesReducer = (state = {}, action) => {
8 | Object.freeze(state);
9 | let newState;
10 | switch(action.type) {
11 | case RECEIVE_ALL_STORIES:
12 | return action.stories;
13 | case RECEIVE_STORY:
14 | return merge({}, state, {[action.story.id]: action.story});
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default StoriesReducer;
21 |
--------------------------------------------------------------------------------
/frontend/reducers/topics_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_TOPICS} from '../actions/topic_actions';
3 |
4 | import merge from 'lodash/merge';
5 |
6 | const TopicsReducer = (state = {}, action) => {
7 | Object.freeze(state);
8 | let newState;
9 | switch(action.type) {
10 | case RECEIVE_ALL_TOPICS:
11 | return action.topics;
12 | default:
13 | return state;
14 | }
15 | };
16 |
17 | export default TopicsReducer;
18 |
--------------------------------------------------------------------------------
/frontend/reducers/users_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ALL_USERS,
3 | RECEIVE_USER } from '../actions/user_actions';
4 |
5 | import merge from 'lodash/merge';
6 |
7 | const UsersReducer = (state = {}, action) => {
8 | Object.freeze(state);
9 | let newState;
10 | switch(action.type) {
11 | // case RECEIVE_ALL_USERS:
12 | // return action.users;
13 | case RECEIVE_USER:
14 | return merge({}, state, {[action.user.id]: action.user});
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default UsersReducer;
21 |
--------------------------------------------------------------------------------
/frontend/store/store.js:
--------------------------------------------------------------------------------
1 |
2 | // import { createStore, applyMiddleware } from 'redux';
3 | // import thunk from 'redux-thunk';
4 | // import { createLogger } from 'redux-logger';
5 |
6 | // import RootReducer from '../reducers/root_reducer';
7 |
8 | // const logger = createLogger();
9 | //
10 | // const configureStore = (preloadedState = {}) => {
11 | // return createStore(RootReducer, preloadedState, applyMiddleware(thunk, logger));
12 | // };
13 | //
14 | //export default configureStore;
15 |
16 |
17 | import { createStore, applyMiddleware } from 'redux';
18 | import thunk from 'redux-thunk';
19 | import { createLogger } from 'redux-logger';
20 | import RootReducer from '../reducers/root_reducer';
21 |
22 | const middlewares = [thunk];
23 |
24 | if (process.env.NODE_ENV !== 'production') {
25 | const logger = createLogger();
26 | middlewares.push(logger);
27 | }
28 |
29 | const configureStore = (preloadedState = {}) => (
30 | createStore(RootReducer, preloadedState, applyMiddleware(...middlewares))
31 | );
32 |
33 | export default configureStore;
34 |
--------------------------------------------------------------------------------
/frontend/util/following_api_util.js:
--------------------------------------------------------------------------------
1 | export const followingIndex = (data) => {
2 | return $.ajax({
3 | method: 'GET',
4 | url: '/api/followings',
5 | dataType: 'json',
6 | data
7 | });
8 | };
9 |
10 | export const followingShow = (followingId) => {
11 | return $.ajax({
12 | method: 'GET',
13 | url: `/api/followings/${followingId}`,
14 | dataType: 'json',
15 | });
16 | };
17 |
18 | export const followingCreate = (following) => {
19 | return $.ajax({
20 | method: 'POST',
21 | url: `/api/followings`,
22 | data: following
23 | });
24 | };
25 |
26 | export const followingDestroy = (following) => {
27 | return $.ajax({
28 | method: 'DELETE',
29 | url: `/api/followings/0`,
30 | data: following
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/util/like_api_util.js:
--------------------------------------------------------------------------------
1 | export const likeIndex = (data) => {
2 | return $.ajax({
3 | method: 'GET',
4 | url: '/api/likes',
5 | dataType: 'json',
6 | data
7 | });
8 | };
9 |
10 | export const likeShow = (likeId) => {
11 | return $.ajax({
12 | method: 'GET',
13 | url: `/api/likes/${likeId}`,
14 | dataType: 'json',
15 | });
16 | };
17 |
18 | export const likeCreate = (like) => {
19 | return $.ajax({
20 | method: 'POST',
21 | url: `/api/likes`,
22 | data: like
23 | });
24 | };
25 |
26 | export const likeDestroy = (like) => {
27 | return $.ajax({
28 | method: 'DELETE',
29 | url: `/api/likes/0`,
30 | data: like
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/util/m_util.js:
--------------------------------------------------------------------------------
1 | export const formatDate = (dateArr) => {
2 | let ampm;
3 | let hour;
4 | if(dateArr[3] > 12){
5 | ampm = 'pm';
6 | hour = (dateArr[3] - 12);
7 | } else {
8 | ampm = 'am';
9 | hour = dateArr[3];
10 | }
11 | const months = ['none','Jan.','Feb.','Mar.','Apr.','May','Jun.','Jul.','Aug.','Sep.','Oct.','Nov.','Dec.'];
12 | let str = `${months[dateArr[0]]} ${dateArr[1]}, ${dateArr[2]}. ${hour}:${dateArr[4]} ${ampm}`;
13 | return str;
14 | }
15 |
16 | export const encodeDate = () => {
17 | let date = new Date();
18 | let month = (date.getMonth() + 1);
19 | let day = date.getDate();
20 | let year = date.getFullYear();
21 | let hour = date.getHours();
22 | let minutes = date.getMinutes();
23 | let seconds = date.getSeconds();
24 | return `${month},${day},${year},${hour},${minutes},${seconds}`;
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/util/response_api_util.js:
--------------------------------------------------------------------------------
1 | export const responseIndex = (data) => {
2 | return $.ajax({
3 | method: 'GET',
4 | url: '/api/responses',
5 | dataType: 'json',
6 | data
7 | });
8 | };
9 |
10 | export const responseShow = (responseId) => {
11 | return $.ajax({
12 | method: 'GET',
13 | url: `/api/responses/${responseId}`,
14 | dataType: 'json',
15 | });
16 | };
17 |
18 | export const responseCreate = (response) => {
19 | return $.ajax({
20 | method: 'POST',
21 | url: `/api/responses`,
22 | data: response
23 | });
24 | };
25 |
26 | export const responseUpdate = (response) => {
27 | return $.ajax({
28 | method: 'PATCH',
29 | url: `/api/responses/${response.id}`,
30 | data: response
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/frontend/util/session_api_util.js:
--------------------------------------------------------------------------------
1 | export const signup = (user) => {
2 | return $.ajax({
3 | method: 'POST',
4 | url: '/api/users',
5 | processData: false,
6 | contentType: false,
7 | dataType: 'json',
8 | data: user
9 | });
10 | };
11 |
12 | export const login = (user) => {
13 | return $.ajax({
14 | method: 'POST',
15 | url: '/api/session',
16 | data: user
17 | });
18 | };
19 |
20 | export const logout = (user) => {
21 | return $.ajax({
22 | method: 'DELETE',
23 | url: '/api/session'
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/frontend/util/story_api_util.js:
--------------------------------------------------------------------------------
1 | export const storyIndex = (data) => {
2 | return $.ajax({
3 | method: 'GET',
4 | url: '/api/stories',
5 | dataType: 'json',
6 | data
7 | });
8 | };
9 |
10 | export const storyShow = (storyId) => {
11 | return $.ajax({
12 | method: 'GET',
13 | url: `/api/stories/${storyId}`,
14 | dataType: 'json',
15 | });
16 | };
17 |
18 | export const storyCreate = (story) => {
19 | return $.ajax({
20 | method: 'POST',
21 | url: `/api/stories`,
22 | processData: false,
23 | contentType: false,
24 | dataType: 'json',
25 | data: story
26 | });
27 | };
28 |
29 | export const storyUpdate = (story) => {
30 | return $.ajax({
31 | method: 'PATCH',
32 | url: `/api/stories/${story.id}`,
33 | processData: false,
34 | contentType: false,
35 | dataType: 'json',
36 | data: story
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/frontend/util/topic_api_util.js:
--------------------------------------------------------------------------------
1 | export const topicIndex = (data) => {
2 | return $.ajax({
3 | method: 'GET',
4 | url: '/api/topics',
5 | dataType: 'json',
6 | data
7 | });
8 | };
9 |
--------------------------------------------------------------------------------
/frontend/util/user_api_util.js:
--------------------------------------------------------------------------------
1 | export const userShow = (userId) => {
2 | return $.ajax({
3 | method: 'GET',
4 | url: `/api/users/${userId}`,
5 | dataType: 'json',
6 | });
7 | };
8 |
9 | export const userUpdate = (user) => {
10 | return $.ajax({
11 | method: 'PATCH',
12 | url: `/api/users/${user.id}`,
13 | processData: false,
14 | contentType: false,
15 | dataType: 'json',
16 | data: user
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/lib/tasks/.keep
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/log/.keep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "message",
3 | "version": "1.0.0",
4 | "description": "Mix of different projects in here.",
5 | "main": "webpack.config.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "dependencies": {
10 | "babel-core": "^6.24.1",
11 | "babel-loader": "^6.4.1",
12 | "babel-preset-es2015": "^6.24.1",
13 | "babel-preset-react": "^6.24.1",
14 | "create-react-class": "^15.5.2",
15 | "html-to-react": "^1.2.7",
16 | "lodash": "^4.17.4",
17 | "quill": "^1.2.4",
18 | "react": "^15.5.4",
19 | "react-dom": "^15.5.4",
20 | "react-quill": "^1.0.0-rc.2",
21 | "react-redux": "^5.0.4",
22 | "react-router": "^3.0.5",
23 | "react-router-redux": "^4.0.8",
24 | "react-scrollable-anchor": "^0.4.2",
25 | "react-transition-group": "^1.1.2",
26 | "redux": "^3.6.0",
27 | "redux-thunk": "^2.2.0",
28 | "webpack": "^2.4.1"
29 | },
30 | "devDependencies": {
31 | "redux-logger": "^3.0.1"
32 | },
33 | "engines": {
34 | "node": "6.7.0",
35 | "npm": "3.10.7"
36 | },
37 | "scripts": {
38 | "test": "echo \"Error: no test specified\" && exit 1",
39 | "postinstall": "webpack"
40 | },
41 | "keywords": [],
42 | "author": "",
43 | "license": "ISC"
44 | }
45 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/controllers/.keep
--------------------------------------------------------------------------------
/test/controllers/sessions_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SessionsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/controllers/users_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UsersControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/fixtures/.keep
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/fixtures/files/.keep
--------------------------------------------------------------------------------
/test/fixtures/followings.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: followings
4 | #
5 | # id :integer not null, primary key
6 | # follower_id :integer not null
7 | # following_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 | # liker_id :integer not null
7 | # story_id :integer
8 | # response_id :integer
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | #
12 |
13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
14 |
15 | # This model initially had no columns defined. If you add columns to the
16 | # model remove the '{}' from the fixture names and add the columns immediately
17 | # below each fixture, per the syntax in the comments below
18 | #
19 | one: {}
20 | # column: value
21 | #
22 | two: {}
23 | # column: value
24 |
--------------------------------------------------------------------------------
/test/fixtures/responses.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: responses
4 | #
5 | # id :integer not null, primary key
6 | # writer_id :integer not null
7 | # story_id :integer
8 | # body :text not null
9 | # date :string
10 | # in_response_id :integer
11 | # created_at :datetime not null
12 | # updated_at :datetime not null
13 | #
14 |
15 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
16 |
17 | # This model initially had no columns defined. If you add columns to the
18 | # model remove the '{}' from the fixture names and add the columns immediately
19 | # below each fixture, per the syntax in the comments below
20 | #
21 | one: {}
22 | # column: value
23 | #
24 | two: {}
25 | # column: value
26 |
--------------------------------------------------------------------------------
/test/fixtures/stories.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: stories
4 | #
5 | # id :integer not null, primary key
6 | # author_id :integer not null
7 | # title :string not null
8 | # description :text
9 | # body :text not null
10 | # date :string
11 | # topic_id :integer
12 | # created_at :datetime not null
13 | # updated_at :datetime not null
14 | # main_image_file_name :string
15 | # main_image_content_type :string
16 | # main_image_file_size :integer
17 | # main_image_updated_at :datetime
18 | #
19 |
20 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
21 |
22 | # This model initially had no columns defined. If you add columns to the
23 | # model remove the '{}' from the fixture names and add the columns immediately
24 | # below each fixture, per the syntax in the comments below
25 | #
26 | one: {}
27 | # column: value
28 | #
29 | two: {}
30 | # column: value
31 |
--------------------------------------------------------------------------------
/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # username :string not null
7 | # email :string not null
8 | # name :string not null
9 | # bio :text
10 | # photo_url :string
11 | # password_digest :string not null
12 | # session_token :string not null
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | # photo_file_name :string
16 | # photo_content_type :string
17 | # photo_file_size :integer
18 | # photo_updated_at :datetime
19 | #
20 |
21 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
22 |
23 | # This model initially had no columns defined. If you add columns to the
24 | # model remove the '{}' from the fixture names and add the columns immediately
25 | # below each fixture, per the syntax in the comments below
26 | #
27 | one: {}
28 | # column: value
29 | #
30 | two: {}
31 | # column: value
32 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/helpers/.keep
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/test/models/.keep
--------------------------------------------------------------------------------
/test/models/following_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: followings
4 | #
5 | # id :integer not null, primary key
6 | # follower_id :integer not null
7 | # following_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 FollowingTest < 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 | # liker_id :integer not null
7 | # story_id :integer
8 | # response_id :integer
9 | # created_at :datetime not null
10 | # updated_at :datetime 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/response_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: responses
4 | #
5 | # id :integer not null, primary key
6 | # writer_id :integer not null
7 | # story_id :integer
8 | # body :text not null
9 | # date :string
10 | # in_response_id :integer
11 | # created_at :datetime not null
12 | # updated_at :datetime not null
13 | #
14 |
15 | require 'test_helper'
16 |
17 | class ResponseTest < ActiveSupport::TestCase
18 | # test "the truth" do
19 | # assert true
20 | # end
21 | end
22 |
--------------------------------------------------------------------------------
/test/models/story_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: stories
4 | #
5 | # id :integer not null, primary key
6 | # author_id :integer not null
7 | # title :string not null
8 | # description :text
9 | # body :text not null
10 | # date :string
11 | # topic_id :integer
12 | # created_at :datetime not null
13 | # updated_at :datetime not null
14 | # main_image_file_name :string
15 | # main_image_content_type :string
16 | # main_image_file_size :integer
17 | # main_image_updated_at :datetime
18 | #
19 |
20 | require 'test_helper'
21 |
22 | class StoryTest < ActiveSupport::TestCase
23 | # test "the truth" do
24 | # assert true
25 | # end
26 | end
27 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: users
4 | #
5 | # id :integer not null, primary key
6 | # username :string not null
7 | # email :string not null
8 | # name :string not null
9 | # bio :text
10 | # photo_url :string
11 | # password_digest :string not null
12 | # session_token :string not null
13 | # created_at :datetime not null
14 | # updated_at :datetime not null
15 | # photo_file_name :string
16 | # photo_content_type :string
17 | # photo_file_size :integer
18 | # photo_updated_at :datetime
19 | #
20 |
21 | require 'test_helper'
22 |
23 | class UserTest < ActiveSupport::TestCase
24 | # test "the truth" do
25 | # assert true
26 | # end
27 | end
28 |
--------------------------------------------------------------------------------
/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/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/vendor/assets/javascripts/.keep
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/minusobjects/Message-Medium/21fd178f9a7baff0b48ff04f2f1624488db2c3e8/vendor/assets/stylesheets/.keep
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 |
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 | module.exports = {
26 | context: __dirname,
27 | entry: "./frontend/message_entry.jsx",
28 | output: {
29 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'),
30 | filename: "bundle.js"
31 | },
32 | plugins: plugins,
33 | module: {
34 | loaders: [
35 | {
36 | test: [/\.jsx?$/, /\.js?$/],
37 | exclude: /node_modules/,
38 | loader: 'babel-loader',
39 | query: {
40 | presets: ['es2015', 'react']
41 | }
42 | }
43 | ]
44 | },
45 | devtool: 'source-maps',
46 | resolve: {
47 | extensions: [".js", ".jsx", "*"]
48 | }
49 | };
50 |
--------------------------------------------------------------------------------