├── .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 | ![image of home_page](docs/caps/m-cap-home.jpg) 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 | ![image of story_input](docs/caps/m-cap-story-input.jpg) 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 | ![image of story](docs/caps/m-cap-story.jpg) 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 | ![image of story_page](docs/wireframes/story.png) 26 | 27 | ### Follows 28 | 29 | ![image of home_page](docs/caps/m-cap-profile.jpg) 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 | ![image of user_profile](docs/wireframes/user-profile.png) 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 | 68 | ); 69 | } 70 | 71 | render(){ 72 | 73 | return ( 74 |
75 |
76 |
77 | {this.renderErrors()} 78 |
79 | 80 |
81 |
82 | 89 |

90 | 97 |
98 | 99 |
100 |

101 |
102 | {"Don't have an account? Sign up here."} 103 |
104 |
105 | {"Click here to sign in as a guest."} 106 |
107 |
108 |
109 |
110 |
111 |
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 = (
UNFOLLOW
); 71 | } else { 72 | followThis = (
FOLLOW
); 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 |
60 | 61 | Publish 62 | 63 | 65 | {this.state.publishOptions} 66 | 67 |
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 |
66 | Sign in 67 |  /  68 | Sign up 69 |  / 70 | Sign in as guest 71 | 72 |
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 |
    82 |
    83 |
    84 |
      85 | {topics} 86 |
    87 |
    88 | 89 |
    90 |
    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 | 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 |
    82 |
    83 | {this.renderErrors()} 84 | 90 |
    91 | 92 |
    93 |
    94 |
    95 |
    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 = (
    59 | 60 | 61 | 62 |
    ); 63 | } else { 64 | likeThis = (
    65 | 66 | 67 | 68 |
    69 |
    70 |
    ); 71 | } 72 | } else { 73 | likeThis = (
    74 | 75 | 76 | 77 |
    78 |
    ); 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 | --------------------------------------------------------------------------------