├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── apple-touch-icon.png ├── apple-touch-icon-precomposed.png ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── vote_test.rb │ ├── comment_test.rb │ ├── user_test.rb │ ├── annotation_test.rb │ └── song_test.rb ├── controllers │ ├── .keep │ ├── api │ │ ├── songs_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── comments_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ └── annotations_controller_test.rb │ └── static_pages_controller_test.rb ├── fixtures │ ├── .keep │ ├── files │ │ └── .keep │ ├── votes.yml │ ├── comments.yml │ ├── users.yml │ ├── annotations.yml │ └── songs.yml ├── integration │ └── .keep └── test_helper.rb ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── beam.jpg │ │ ├── damn.jpg │ │ ├── way.png │ │ ├── blond.jpg │ │ ├── easily.jpg │ │ ├── aeroplane.jpg │ │ ├── dramamine.jpg │ │ ├── dressed.jpg │ │ ├── favicon.ico │ │ ├── million.jpg │ │ ├── presidents.jpg │ │ └── pacific_myth.jpg │ ├── javascripts │ │ ├── channels │ │ │ └── .keep │ │ ├── api │ │ │ ├── songs.coffee │ │ │ ├── users.coffee │ │ │ ├── annotations.coffee │ │ │ ├── comments.coffee │ │ │ └── sessions.coffee │ │ ├── static_pages.coffee │ │ ├── cable.js │ │ └── application.js │ ├── config │ │ └── manifest.js │ └── stylesheets │ │ ├── api │ │ ├── users.scss │ │ ├── sessions.scss │ │ ├── songs │ │ │ ├── songs.scss │ │ │ ├── track_form.scss │ │ │ └── song_show.scss │ │ ├── comments.scss │ │ └── annotations.scss │ │ ├── static_pages.scss │ │ ├── voting.scss │ │ ├── application.css │ │ ├── search.scss │ │ └── header.scss ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ ├── comment.rb │ ├── vote.rb │ ├── annotation.rb │ ├── song.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── users_controller.rb │ │ ├── sessions_controller.rb │ │ ├── comments_controller.rb │ │ ├── songs_controller.rb │ │ └── annotations_controller.rb │ └── application_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── favicon.ico │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── api │ │ ├── users │ │ │ ├── _user.json.jbuilder │ │ │ └── show.json.jbuilder │ │ ├── songs │ │ │ ├── show.json.jbuilder │ │ │ ├── _song.json.jbuilder │ │ │ └── index.json.jbuilder │ │ ├── annotations │ │ │ ├── index.json.jbuilder │ │ │ └── show.json.jbuilder │ │ └── comments │ │ │ ├── show.json.jbuilder │ │ │ └── index.json.jbuilder │ └── static_pages │ │ └── root.html.erb ├── helpers │ ├── api │ │ ├── songs_helper.rb │ │ ├── users_helper.rb │ │ ├── comments_helper.rb │ │ ├── sessions_helper.rb │ │ └── annotations_helper.rb │ ├── application_helper.rb │ └── static_pages_helper.rb ├── jobs │ └── application_job.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb └── mailers │ └── application_mailer.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── docs ├── wireframes │ ├── Auth_Form.png │ ├── song_show.png │ ├── annotation_form.png │ ├── home_wireframe.png │ └── search_results_wireframe.png ├── sample_state.md ├── api_endpoints.md ├── components.md ├── schema.md └── README.md ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── initializers │ ├── session_store.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── cookies_serializer.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── new_framework_defaults.rb ├── application.rb ├── locales │ └── en.yml ├── routes.rb ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── puma.rb └── database.yml ├── config.ru ├── db ├── migrate │ ├── 20170427152211_drop_comments.rb │ ├── 20170419200505_add_score_to_users.rb │ ├── 20170422182211_add_default_score.rb │ ├── 20170427153934_add_type_to_comments.rb │ ├── 20170423011643_change_index_of_annotation.rb │ ├── 20170421154246_add_attachment_image_to_songs.rb │ ├── 20170427153130_create_comment.rb │ ├── 20170418161227_create_users.rb │ ├── 20170427141913_create_comments.rb │ ├── 20170426192954_create_votes.rb │ ├── 20170419201111_create_songs.rb │ └── 20170422181627_create_annotations.rb └── schema.rb ├── frontend ├── util │ ├── search_api_util.js │ ├── session_api_util.js │ ├── song_api_util.js │ ├── annotation_api_util.js │ ├── annotations_util.js │ └── comment_api_util.js ├── components │ ├── app.jsx │ ├── header │ │ ├── header_logged_in.jsx │ │ ├── header_navigation.jsx │ │ ├── header_container.jsx │ │ ├── header.jsx │ │ └── header_logged_out.jsx │ ├── tracks_index │ │ ├── track_index_item.jsx │ │ ├── tracks_index_container.jsx │ │ └── track_index.jsx │ ├── annotations │ │ ├── voting │ │ │ ├── votes_container.jsx │ │ │ └── votes.jsx │ │ ├── annotation_container.jsx │ │ ├── annotation_field.jsx │ │ └── annotation.jsx │ ├── comments │ │ ├── comment_index_item.jsx │ │ ├── comment_container.jsx │ │ └── comment_index.jsx │ ├── search │ │ ├── search_index_item.jsx │ │ ├── search_container.jsx │ │ └── search_index.jsx │ ├── track_form │ │ ├── track_form_container.jsx │ │ └── track_form.jsx │ ├── root.jsx │ └── track_show │ │ ├── track_show_container.jsx │ │ └── track_show.jsx ├── reducers │ ├── selectors.js │ ├── loading_reducer.js │ ├── search_reducer.js │ ├── login_modal_reducer.js │ ├── root_reducer.js │ ├── session_reducer.js │ ├── comment_reducer.js │ ├── songs_reducer.js │ └── annotations_reducer.js ├── store │ └── store.js ├── actions │ ├── login_modal_actions.js │ ├── search_actions.js │ ├── session_actions.js │ ├── song_actions.js │ ├── comment_actions.js │ └── annotation_actions.js └── annotator.jsx ├── Rakefile ├── .gitignore ├── webpack.config.js ├── package.json ├── Gemfile ├── README.md └── Gemfile.lock /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/helpers/api/songs_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SongsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::CommentsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! user, :id, :username 2 | -------------------------------------------------------------------------------- /app/helpers/api/annotations_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::AnnotationsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", user: @user 2 | -------------------------------------------------------------------------------- /app/views/api/songs/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.partial! 'api/songs/song', song: @song 3 | -------------------------------------------------------------------------------- /app/assets/images/beam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/beam.jpg -------------------------------------------------------------------------------- /app/assets/images/damn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/damn.jpg -------------------------------------------------------------------------------- /app/assets/images/way.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/way.png -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | puts "I'm testing here!" 3 | end 4 | -------------------------------------------------------------------------------- /app/assets/images/blond.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/blond.jpg -------------------------------------------------------------------------------- /app/assets/images/easily.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/easily.jpg -------------------------------------------------------------------------------- /app/views/api/annotations/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.array! @annotations, :id, :start_index, :end_index 2 | -------------------------------------------------------------------------------- /app/assets/images/aeroplane.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/aeroplane.jpg -------------------------------------------------------------------------------- /app/assets/images/dramamine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/dramamine.jpg -------------------------------------------------------------------------------- /app/assets/images/dressed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/dressed.jpg -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/million.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/million.jpg -------------------------------------------------------------------------------- /app/views/layouts/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/views/layouts/favicon.ico -------------------------------------------------------------------------------- /docs/wireframes/Auth_Form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/docs/wireframes/Auth_Form.png -------------------------------------------------------------------------------- /docs/wireframes/song_show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/docs/wireframes/song_show.png -------------------------------------------------------------------------------- /app/assets/images/presidents.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/presidents.jpg -------------------------------------------------------------------------------- /app/assets/images/pacific_myth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/app/assets/images/pacific_myth.jpg -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /docs/wireframes/annotation_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/docs/wireframes/annotation_form.png -------------------------------------------------------------------------------- /docs/wireframes/home_wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/docs/wireframes/home_wireframe.png -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /docs/wireframes/search_results_wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drj17/Annotator/HEAD/docs/wireframes/search_results_wireframe.png -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /db/migrate/20170427152211_drop_comments.rb: -------------------------------------------------------------------------------- 1 | class DropComments < ActiveRecord::Migration[5.0] 2 | def change 3 | drop_table :comments 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/api/comments/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.extract! @comment, :id, :author, :body 2 | json.author_id @comment.author_id 3 | json.username @comment.author.username 4 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/api/songs/_song.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.extract! song, :id, :title, :lyrics, :author_id, :artist 3 | json.comments song.comment_ids 4 | json.image_url asset_path(song.image.url) 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_Annotator_session' 4 | -------------------------------------------------------------------------------- /db/migrate/20170419200505_add_score_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddScoreToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :iq, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170422182211_add_default_score.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultScore < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :annotations, :score, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170427153934_add_type_to_comments.rb: -------------------------------------------------------------------------------- 1 | class AddTypeToComments < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :comments, :commentable_type, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /frontend/util/search_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchSearchResults = (query) => { 2 | return $.ajax({ 3 | method: 'get', 4 | url: 'api/songs', 5 | data: {query: query} 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/controllers/api/songs_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SongsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::UsersControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/comments_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::CommentsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/sessions_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SessionsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/static_pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class StaticPagesControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/annotations_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::AnnotationsControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/users.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/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/assets/stylesheets/api/sessions.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/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/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the static_pages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/javascripts/api/songs.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/api/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/javascripts/api/annotations.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/api/comments.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/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/static_pages.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/api/annotations/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | json.extract! @annotation, :id, :author, :author_id, :song_id, :description, :score 3 | json.author @annotation.author.username 4 | json.comments @annotation.comment_ids 5 | json.did_vote @did_vote 6 | json.direction @direction 7 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /frontend/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderContainer from './header/header_container'; 3 | 4 | const App = ({ children }) => ( 5 |
6 | 7 | {children} 8 |
9 | ); 10 | 11 | export default App; 12 | -------------------------------------------------------------------------------- /db/migrate/20170423011643_change_index_of_annotation.rb: -------------------------------------------------------------------------------- 1 | class ChangeIndexOfAnnotation < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_index :annotations, [:start_index, :end_index] 4 | add_index :annotations, [:start_index, :end_index, :song_id], unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/api/comments/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | 2 | @comments.each do |comment| 3 | json.set! comment.id do 4 | json.extract! comment, :id, :body 5 | json.author_id comment.author.id 6 | json.username comment.author.username 7 | # json.created_at comment.created_at 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170421154246_add_attachment_image_to_songs.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToSongs < ActiveRecord::Migration 2 | def self.up 3 | change_table :songs do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :songs, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20170427153130_create_comment.rb: -------------------------------------------------------------------------------- 1 | class CreateComment < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :comments do |t| 4 | t.integer :author_id, null: false 5 | t.integer :commentable_id, null: false 6 | t.text :body, null: false 7 | 8 | t.timestamps 9 | end 10 | add_index :comments, :body 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /frontend/reducers/selectors.js: -------------------------------------------------------------------------------- 1 | export const songComments = (state) => { 2 | return state.songs.currentTrack.comments.map((id) => { 3 | return state.comments.comments[id]; 4 | }); 5 | }; 6 | export const annotationComments = (state) => { 7 | return state.annotations.currentAnnotation.comments.map((id) => { 8 | return state.comments.comments[id]; 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers/root_reducer'; 4 | 5 | const configureStore = (preloadedState = {}) => { 6 | return createStore( 7 | rootReducer, 8 | preloadedState, 9 | applyMiddleware(thunk) 10 | ); 11 | }; 12 | 13 | export default configureStore; 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/voting.scss: -------------------------------------------------------------------------------- 1 | .voting { 2 | display: flex; 3 | color: #9A9A9A; 4 | width: 40%; 5 | margin: 10px 0px; 6 | } 7 | 8 | .downvote { 9 | margin-right: 10px; 10 | } 11 | 12 | .upvote { 13 | margin-left: 10px; 14 | } 15 | .upvote:hover { 16 | color: #22C13E; 17 | transition: .1s 18 | } 19 | 20 | .downvote:hover { 21 | color: red; 22 | transition: .1s 23 | } 24 | -------------------------------------------------------------------------------- /db/migrate/20170418161227_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :username, null: false 5 | t.string :password_digest, null: false 6 | t.string :session_token, null: false 7 | 8 | t.timestamps 9 | end 10 | 11 | add_index :users, :username, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170427141913_create_comments.rb: -------------------------------------------------------------------------------- 1 | class CreateComments < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :comments do |t| 4 | t.integer :author_id, null: false 5 | t.integer :song_id 6 | t.integer :annotation_id 7 | t.text :body, null: false 8 | 9 | t.timestamps 10 | end 11 | add_index :comments, :author_id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20170426192954_create_votes.rb: -------------------------------------------------------------------------------- 1 | class CreateVotes < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :votes do |t| 4 | t.integer :user_id, null: false 5 | t.integer :annotation_id, null: false 6 | t.integer :value, default: 1 7 | 8 | t.timestamps 9 | end 10 | add_index :votes, [:user_id, :annotation_id, :value], unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170419201111_create_songs.rb: -------------------------------------------------------------------------------- 1 | class CreateSongs < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :songs do |t| 4 | t.string :title, null: false 5 | t.text :lyrics, null: false 6 | t.integer :author_id, null: false 7 | t.string :artist, null: false 8 | 9 | t.timestamps 10 | end 11 | add_index :songs, [:title, :artist], unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /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/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | def create 3 | @user = User.new(user_params) 4 | 5 | if @user.save 6 | login(@user) 7 | render "/api/users/show" 8 | else 9 | render json: @user.errors.full_messages, status: 422 10 | end 11 | end 12 | 13 | private 14 | 15 | def user_params 16 | params.require(:user).permit(:username, :password) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/api/songs/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.songs @songs do |song| 2 | json.extract! song, :id, :title, :artist 3 | json.image_url asset_path(song.image.url) 4 | end 5 | 6 | json.song_titles @song_titles do |song| 7 | json.extract! song, :id, :title, :artist 8 | json.image_url asset_path(song.image.url) 9 | end 10 | 11 | json.song_artists @song_artists do |song| 12 | json.extract! song, :id, :title, :artist 13 | json.image_url asset_path(song.image.url) 14 | end 15 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | export const signup = (user) => { 2 | let aj = $.ajax({ 3 | method: 'post', 4 | url: 'api/users/', 5 | data: user 6 | }); 7 | return aj; 8 | }; 9 | 10 | export const login = (user) => { 11 | return $.ajax({ 12 | method: 'post', 13 | url: 'api/session/', 14 | data: user 15 | }); 16 | }; 17 | 18 | export const logout = () => { 19 | return $.ajax({ 20 | method: 'delete', 21 | url: 'api/session/', 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/components/header/header_logged_in.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hashHistory } from 'react-router'; 3 | const greeting = (props) => ( 4 |
5 | 6 | 10 |
11 | ); 12 | 13 | export default greeting; 14 | -------------------------------------------------------------------------------- /frontend/actions/login_modal_actions.js: -------------------------------------------------------------------------------- 1 | export const OPEN_LOGIN_MODAL = "OPEN_LOGIN_MODAL"; 2 | export const OPEN_SIGNUP_MODAL = "OPEN_SIGNUP_MODAL"; 3 | export const CLOSE_MODAL = "CLOSE_MODAL"; 4 | 5 | export const openLoginModal = () => { 6 | return { 7 | type: OPEN_LOGIN_MODAL, 8 | }; 9 | }; 10 | 11 | export const openSignupModal = () => { 12 | return { 13 | type: OPEN_SIGNUP_MODAL 14 | }; 15 | }; 16 | export const closeModal = () => { 17 | return { 18 | type: CLOSE_MODAL, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/models/vote_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: votes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # annotation_id :integer not null 8 | # value :integer default("1") 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | require 'test_helper' 14 | 15 | class VoteTest < ActiveSupport::TestCase 16 | # test "the truth" do 17 | # assert true 18 | # end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20170422181627_create_annotations.rb: -------------------------------------------------------------------------------- 1 | class CreateAnnotations < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :annotations do |t| 4 | t.integer :author_id, null: false 5 | t.integer :score, null: false 6 | t.text :description, null: false 7 | t.integer :song_id, null: false 8 | t.integer :start_index, null: false 9 | t.integer :end_index, null: false 10 | 11 | t.timestamps 12 | end 13 | 14 | add_index :annotations, [:start_index, :end_index], unique: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/models/comment_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # song_id :integer 8 | # annotation_id :integer 9 | # body :text not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | require 'test_helper' 15 | 16 | class CommentTest < ActiveSupport::TestCase 17 | # test "the truth" do 18 | # assert true 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/annotator.jsx: -------------------------------------------------------------------------------- 1 | import Root from './components/root'; 2 | import React from 'react'; 3 | import ReactDom from 'react-dom'; 4 | import configureStore from './store/store'; 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | let store; 8 | if (window.currentUser) { 9 | const preloadedState = { 10 | session: { currentUser: window.currentUser } }; 11 | store = configureStore(preloadedState); 12 | } else { 13 | store = configureStore(); 14 | } 15 | 16 | const root = document.getElementById('root'); 17 | ReactDom.render(, root); 18 | }); 19 | -------------------------------------------------------------------------------- /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 | # password_digest :string not null 8 | # session_token :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # iq :integer default("0") 12 | # 13 | 14 | require 'test_helper' 15 | 16 | class UserTest < ActiveSupport::TestCase 17 | # test "the truth" do 18 | # assert true 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /test/fixtures/votes.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: votes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # annotation_id :integer not null 8 | # value :integer default("1") 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 14 | 15 | one: 16 | user_id: 1 17 | annotation_id: 1 18 | value: 1 19 | 20 | two: 21 | user_id: 1 22 | annotation_id: 1 23 | value: 1 24 | -------------------------------------------------------------------------------- /frontend/reducers/loading_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | START_LOADING_SONG 3 | } from '../actions/song_actions'; 4 | 5 | import { 6 | FINISH_LOADING_ANNOTATIONS 7 | } from '../actions/annotation_actions'; 8 | 9 | const defaultState = { 10 | loading: false 11 | }; 12 | 13 | export default (state = defaultState, action ) => { 14 | Object.freeze(state); 15 | switch(action.type){ 16 | case START_LOADING_SONG: 17 | return Object.assign({}, state, { loading: true }); 18 | case FINISH_LOADING_ANNOTATIONS: 19 | return Object.assign({}, state, { loading: false }); 20 | default: 21 | return state; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /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 Annotator 10 | class Application < Rails::Application 11 | config.paperclip_defaults = { 12 | :storage => :s3, 13 | :s3_credentials => { 14 | :bucket => ENV["s3_bucket"], 15 | :access_key_id => ENV["s3_access_key_id"], 16 | :secret_access_key => ENV["s3_secret_access_key"], 17 | :s3_region => ENV["s3_region"] 18 | } 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/comments.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # song_id :integer 8 | # annotation_id :integer 9 | # body :text not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 15 | 16 | one: 17 | author_id: 1 18 | song_id: 1 19 | annotation_id: 1 20 | 21 | two: 22 | author_id: 1 23 | song_id: 1 24 | annotation_id: 1 25 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: comments 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # song_id :integer 8 | # annotation_id :integer 9 | # body :text not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | 14 | class Comment < ApplicationRecord 15 | validates :author, :body, presence: true 16 | 17 | belongs_to :commentable, polymorphic: true 18 | 19 | belongs_to :author, 20 | primary_key: :id, 21 | foreign_key: :author_id, 22 | class_name: "User" 23 | 24 | end 25 | -------------------------------------------------------------------------------- /frontend/components/tracks_index/track_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | const TrackIndexItem = ({ track, fetchSong, index, style }) => ( 5 |
6 | 7 |
8 |

{index + 1}

9 | 10 |
11 |

{track.title}

12 | {track.artist} 13 |
14 |
15 | 16 |
17 | ); 18 | 19 | export default TrackIndexItem; 20 | -------------------------------------------------------------------------------- /frontend/components/annotations/voting/votes_container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Votes from './votes'; 4 | import { updateAnnotation } from '../../../actions/annotation_actions'; 5 | 6 | const mapStateToProps = state => { 7 | return { 8 | currentAnnotation: state.annotations.currentAnnotation, 9 | currentUser: state.session.currentUser 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = dispatch => { 14 | return { 15 | updateAnnotation: (annotation, vote) => dispatch(updateAnnotation(annotation, vote)), 16 | }; 17 | }; 18 | 19 | export default connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Votes); 23 | -------------------------------------------------------------------------------- /frontend/reducers/search_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_SEARCH_RESULTS, CLEAR_RESULTS } from '../actions/search_actions'; 2 | 3 | let defaultState = { 4 | "songTitles": [], 5 | "songArtists": [] 6 | }; 7 | 8 | const SearchReducer = (state = defaultState, action) => { 9 | Object.freeze(state); 10 | switch(action.type){ 11 | case RECEIVE_SEARCH_RESULTS: 12 | let results = Object.assign({}, state); 13 | results["songTitles"] = action.songTitles; 14 | results["songArtists"] = action.songArtists; 15 | return results; 16 | case CLEAR_RESULTS: 17 | return defaultState; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default SearchReducer; 24 | -------------------------------------------------------------------------------- /test/models/annotation_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: annotations 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # score :integer default("0"), not null 8 | # description :text not null 9 | # song_id :integer not null 10 | # start_index :integer not null 11 | # end_index :integer not null 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # 15 | 16 | require 'test_helper' 17 | 18 | class AnnotationTest < ActiveSupport::TestCase 19 | # test "the truth" do 20 | # assert true 21 | # end 22 | end 23 | -------------------------------------------------------------------------------- /frontend/components/comments/comment_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CommentIndexItem = ({comment, currentUser, deleteComment}) => { 4 | let editButton = ""; 5 | let deleteButton = ""; 6 | 7 | if(currentUser && currentUser.id === comment.author_id){ 8 | deleteButton = ; 9 | } 10 | 11 | return ( 12 |
13 |

{comment.username}

14 |

{comment.body}

15 |
16 | {editButton} 17 | {deleteButton} 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default CommentIndexItem; 24 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | def create 3 | @user = User.find_by_credentials( 4 | params[:user][:username], 5 | params[:user][:password] 6 | ) 7 | 8 | if @user 9 | login(@user) 10 | render("api/users/show") 11 | else 12 | render( 13 | json: ["Invalid username/password combination"], status: 401 14 | ) 15 | end 16 | end 17 | 18 | def destroy 19 | @user = current_user 20 | if @user 21 | logout 22 | render json: {} 23 | else 24 | render( 25 | json: ["No one signed in"], 26 | status: 404 27 | ) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /frontend/reducers/login_modal_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | OPEN_LOGIN_MODAL, 3 | OPEN_SIGNUP_MODAL, 4 | CLOSE_MODAL, 5 | } from '../actions/login_modal_actions.js'; 6 | 7 | const defaultState = { 8 | open: false, 9 | type: "login" 10 | }; 11 | 12 | export default (state = defaultState, action) => { 13 | Object.freeze(state); 14 | switch(action.type){ 15 | case OPEN_LOGIN_MODAL: 16 | return Object.assign({}, state, { open: true, type: "login" }); 17 | case OPEN_SIGNUP_MODAL: 18 | return Object.assign({}, state, { open: true, type: "signup" }); 19 | case CLOSE_MODAL: 20 | return Object.assign({}, state, { open: false }); 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | node_modules/ 20 | bundle.js 21 | bundle.js.map 22 | .byebug_history 23 | .DS_Store 24 | npm-debug.log 25 | 26 | # Ignore application configuration 27 | /config/application.yml 28 | -------------------------------------------------------------------------------- /frontend/components/search/search_index_item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, hashHistory } from 'react-router'; 3 | 4 | const SearchIndexItem = ({ track, closeSearch, closeAnnotation }) => { 5 | return ( 6 |
{hashHistory.push(`/songs/${track.id}`); closeAnnotation();}} > 7 | 8 | 9 |
10 |

{track.title}

11 | {track.artist} 12 |
13 | 14 |
15 | ); 16 | }; 17 | 18 | export default SearchIndexItem; 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery with: :exception 3 | helper_method :current_user, :logged_in? 4 | 5 | def current_user 6 | @current_user ||= User.find_by(session_token: session[:session_token]) 7 | end 8 | 9 | def login(user) 10 | @current_user = user 11 | session[:session_token] = user.session_token 12 | end 13 | 14 | def logout 15 | current_user.reset_token! 16 | session[:session_token] = nil 17 | end 18 | 19 | def logged_in? 20 | !!current_user 21 | end 22 | 23 | def require_signed_in 24 | render json: { base: ['You must sign it to view this page'] }, status: 401 unless logged_in? 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/models/vote.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: votes 4 | # 5 | # id :integer not null, primary key 6 | # user_id :integer not null 7 | # annotation_id :integer not null 8 | # value :integer default("1") 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # 12 | 13 | class Vote < ApplicationRecord 14 | validates :user, :annotation, presence: true 15 | 16 | belongs_to :user 17 | belongs_to :annotation, 18 | primary_key: :id, 19 | foreign_key: :annotation_id, 20 | class_name: "Annotation" 21 | 22 | def comment_ids 23 | comments.map do |comment| 24 | comment.id 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /frontend/actions/search_actions.js: -------------------------------------------------------------------------------- 1 | import * as SearchApiUtil from '../util/search_api_util'; 2 | 3 | export const RECEIVE_SEARCH_RESULTS = "RECEIVE_SEARCH_RESULTS"; 4 | export const CLEAR_RESULTS = "CLEAR_RESULTS"; 5 | 6 | const receiveSearchResults = (results) => { 7 | return { 8 | type: RECEIVE_SEARCH_RESULTS, 9 | songs: results.songs, 10 | songTitles: results.song_titles, 11 | songArtists: results.song_artists 12 | }; 13 | }; 14 | 15 | export const clearResults = () => { 16 | return { 17 | type: CLEAR_RESULTS, 18 | }; 19 | }; 20 | 21 | export const fetchSearchResults = (query) => dispatch => { 22 | return SearchApiUtil.fetchSearchResults(query) 23 | .then(results => dispatch(receiveSearchResults(results))); 24 | }; 25 | -------------------------------------------------------------------------------- /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 | # password_digest :string not null 8 | # session_token :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # iq :integer default("0") 12 | # 13 | 14 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 15 | 16 | one: 17 | username: MyString 18 | password_digest: MyString 19 | session_token: MyString 20 | 21 | two: 22 | username: MyString 23 | password_digest: MyString 24 | session_token: MyString 25 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import SessionReducer from './session_reducer'; 3 | import SongsReducer from './songs_reducer'; 4 | import LoadingReducer from './loading_reducer'; 5 | import AnnotationsReducer from './annotations_reducer'; 6 | import SearchReducer from './search_reducer'; 7 | import CommentsReducer from './comment_reducer'; 8 | import LoginModalReducer from './login_modal_reducer'; 9 | 10 | const rootReducer = combineReducers({ 11 | loading: LoadingReducer, 12 | session: SessionReducer, 13 | songs: SongsReducer, 14 | annotations: AnnotationsReducer, 15 | search: SearchReducer, 16 | comments: CommentsReducer, 17 | loginModal: LoginModalReducer 18 | }); 19 | 20 | export default rootReducer; 21 | -------------------------------------------------------------------------------- /test/models/song_test.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: songs 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # lyrics :text not null 8 | # author_id :integer not null 9 | # artist :string not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # 17 | 18 | require 'test_helper' 19 | 20 | class SongTest < ActiveSupport::TestCase 21 | # test "the truth" do 22 | # assert true 23 | # end 24 | end 25 | -------------------------------------------------------------------------------- /frontend/components/search/search_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchSearchResults, clearResults } from '../../actions/search_actions'; 3 | import SearchIndex from './search_index'; 4 | import React from 'react'; 5 | import { closeAnnotation } from '../../actions/annotation_actions'; 6 | const mapStateToProps = (state) => { 7 | return { 8 | results: state.search 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = dispatch => { 13 | return { 14 | fetchSearchResults: (query) => dispatch(fetchSearchResults(query)), 15 | clearResults: () => dispatch(clearResults()), 16 | closeAnnotation: () => dispatch(closeAnnotation()) 17 | }; 18 | }; 19 | 20 | export default connect( 21 | mapStateToProps, 22 | mapDispatchToProps 23 | )(SearchIndex); 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | root to: "static_pages#root" 4 | 5 | namespace :api, defaults: {format: JSON} do 6 | resource :session, only: [:create, :destroy] 7 | resources :users, only: [:create] do 8 | resources :songs, only: [:index] 9 | end 10 | resources :songs, only: [:index, :create, :show, :destroy, :update] do 11 | resources :annotations, only: [:index] 12 | resources :comments, only: [:index] 13 | end 14 | resources :annotations, only: [:create, :show, :destroy, :update] do 15 | resources :comments, only: [:index] 16 | end 17 | resources :comments, only: [:create, :update, :destroy, :show] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /frontend/components/tracks_index/tracks_index_container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchSongs, 4 | fetchSong, 5 | clearErrors } from '../../actions/song_actions'; 6 | import TrackIndex from './track_index'; 7 | 8 | const mapStateToProps = state => { 9 | return { 10 | tracks: state.songs.tracks, 11 | errors: state.songs.errors, 12 | loading: state.loading 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | fetchSongs: () => dispatch(fetchSongs()), 19 | clearErrors: () => dispatch(clearErrors()), 20 | fetchSong: (id) => dispatch(fetchSong(id)) 21 | }; 22 | }; 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(TrackIndex); 28 | -------------------------------------------------------------------------------- /frontend/util/song_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchSongs = () => { 2 | return $.ajax({ 3 | method: 'get', 4 | url: 'api/songs' 5 | }); 6 | }; 7 | 8 | export const fetchSong = (id) => { 9 | return $.ajax({ 10 | method: 'get', 11 | url: `api/songs/${id}` 12 | }); 13 | }; 14 | 15 | export const createSong = (song) => { 16 | return $.ajax({ 17 | method: 'post', 18 | url: 'api/songs', 19 | contentType: false, 20 | processData: false, 21 | data: song 22 | }); 23 | }; 24 | 25 | export const deleteSong = (id) => { 26 | return $.ajax({ 27 | method: 'delete', 28 | url: `api/songs/${id}` 29 | }); 30 | }; 31 | export const updateSong = (song, id) => { 32 | return $.ajax({ 33 | method: 'patch', 34 | url: `api/songs/${id}`, 35 | processData: false, 36 | contentType: false, 37 | data: song 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /docs/sample_state.md: -------------------------------------------------------------------------------- 1 | ```js 2 | { 3 | currentUser: { 4 | id: 1, 5 | username: "kdot" 6 | }, 7 | errors: { 8 | signUp: ["Name Can't be blank"], 9 | logIn: [] 10 | createSong: ["Artist can't be blank"] 11 | }, 12 | songs: { 13 | 1: { 14 | title: "POWER", 15 | body: "No one man should have all that power", 16 | artist: "Kanye West" 17 | user_id: 1, 18 | tags: { 19 | 1: { 20 | id: 1 21 | name: "Hip Hop" 22 | } 23 | } 24 | } 25 | }, 26 | annotations: { 27 | 1: { 28 | body: "What power means to Kanye" 29 | author_id: 1, 30 | lyrics: "No one man should have all that power" //Not sure yet how I am going to select/store the lyrics. Possible a start/end index would work better 31 | } 32 | } 33 | tagFilters: [1] //filter songs by tag 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/fixtures/annotations.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: annotations 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # score :integer default("0"), not null 8 | # description :text not null 9 | # song_id :integer not null 10 | # start_index :integer not null 11 | # end_index :integer not null 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # 15 | 16 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 17 | 18 | one: 19 | author_id: 1 20 | score: 1 21 | description: MyText 22 | song_id: 1 23 | start_index: 1 24 | end_index: 1 25 | 26 | two: 27 | author_id: 1 28 | score: 1 29 | description: MyText 30 | song_id: 1 31 | start_index: 1 32 | end_index: 1 33 | -------------------------------------------------------------------------------- /test/fixtures/songs.yml: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: songs 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # lyrics :text not null 8 | # author_id :integer not null 9 | # artist :string not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # 17 | 18 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 19 | 20 | one: 21 | title: MyString 22 | lyrics: MyText 23 | author_id: 1 24 | artist: MyString 25 | 26 | two: 27 | title: MyString 28 | lyrics: MyText 29 | author_id: 1 30 | artist: MyString 31 | -------------------------------------------------------------------------------- /frontend/util/annotation_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchAnnotations = (songId) => { 2 | return $.ajax({ 3 | method: 'get', 4 | url: `api/songs/${songId}/annotations` 5 | }); 6 | }; 7 | 8 | export const fetchAnnotation = (id) => { 9 | return $.ajax({ 10 | method: 'get', 11 | url: `api/annotations/${id}` 12 | }); 13 | }; 14 | 15 | export const createAnnotation = (annotation) => { 16 | return $.ajax({ 17 | method: 'post', 18 | url: 'api/annotations', 19 | data: { annotation } 20 | }); 21 | }; 22 | 23 | export const deleteAnnotation = (id) => { 24 | return $.ajax({ 25 | method: "delete", 26 | url: `api/annotations/${id}` 27 | }); 28 | }; 29 | 30 | export const updateAnnotation = (annotation, vote) => { 31 | return $.ajax({ 32 | method: 'patch', 33 | url: `api/annotations/${annotation.id}`, 34 | data: { annotation: annotation, vote: vote } 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/components/tracks_index/track_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TrackIndexItem from './track_index_item'; 3 | import { Link } from 'react-router'; 4 | 5 | class TrackIndex extends React.Component { 6 | constructor(props){ 7 | super(props); 8 | } 9 | 10 | componentDidMount(){ 11 | this.props.fetchSongs(); 12 | } 13 | 14 | render() { 15 | const tracks = this.props.tracks.map((track, index) => { 16 | 17 | let style = ""; 18 | if(index < 3){ 19 | style = "top"; 20 | } 21 | 22 | return ; 28 | }); 29 | return ( 30 |
31 |
    32 | {tracks} 33 |
34 |
35 | ); 36 | } 37 | 38 | 39 | } 40 | 41 | export default TrackIndex; 42 | -------------------------------------------------------------------------------- /frontend/util/annotations_util.js: -------------------------------------------------------------------------------- 1 | export const findOffset = (element) => { 2 | let offset = 0; 3 | while(element.previousSibling) { 4 | offset += element.previousSibling.textContent.length; 5 | element = element.previousSibling; 6 | 7 | } 8 | return offset; 9 | }; 10 | 11 | export const orderAnnotations = (annotations) => { 12 | let ordered = annotations.sort((a, b) => { 13 | if(a.start_index < b.start_index){ 14 | return -1; 15 | } else { 16 | return 1; 17 | } 18 | }); 19 | 20 | return ordered; 21 | }; 22 | 23 | export const isValidAnnotation = (range, annotations) => { 24 | let valid = true; 25 | if(range[1] - range[0] <= 0){ 26 | valid = false; 27 | } 28 | annotations.forEach(annotation => { 29 | if(range[0] <= annotation.end_index && annotation.start_index <= range[1]){ 30 | valid = false; 31 | } 32 | }); 33 | return valid; 34 | }; 35 | 36 | export const uniqueId = () => { 37 | return Math.random(10000); 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ERRORS, 3 | RECEIVE_CURRENT_USER, 4 | CLEAR_ERRORS, 5 | RECEIVE_SESSION_ERRORS } from '../actions/session_actions'; 6 | import merge from 'lodash/merge'; 7 | 8 | let defaultState = { 9 | currentUser: null, 10 | errors: [] 11 | }; 12 | 13 | const SessionReducer = (state = defaultState, action) => { 14 | Object.freeze(state); 15 | switch(action.type){ 16 | case RECEIVE_CURRENT_USER: 17 | const currentUser = action.user; 18 | let merged = merge({}, state, { 19 | currentUser 20 | }); 21 | 22 | return merged; 23 | case RECEIVE_SESSION_ERRORS: 24 | let errors = action.errors; 25 | return merge({}, state, { 26 | errors 27 | }); 28 | case CLEAR_ERRORS: 29 | const newState = Object.assign({}, state); 30 | newState.errors = []; 31 | return newState; 32 | 33 | default: 34 | return state; 35 | } 36 | }; 37 | 38 | export default SessionReducer; 39 | -------------------------------------------------------------------------------- /frontend/util/comment_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchSongComments = (songId) => { 2 | return $.ajax({ 3 | method: 'get', 4 | url: `api/songs/${songId}/comments` 5 | }); 6 | }; 7 | 8 | export const fetchAnnotationComments = (annotationId) => { 9 | return $.ajax({ 10 | method: 'get', 11 | url: `api/annotations/${annotationId}/comments` 12 | }); 13 | }; 14 | 15 | export const fetchComment = (id) => { 16 | return $.ajax({ 17 | method: 'get', 18 | url: `api/comments/${id}` 19 | }); 20 | }; 21 | 22 | export const deleteComment = (id) => { 23 | return $.ajax({ 24 | method: 'delete', 25 | url: `api/comments/${id}` 26 | }); 27 | }; 28 | 29 | export const updateComment = (comment) => { 30 | return $.ajax({ 31 | method: 'patch', 32 | url: `api/comments/${comment.id}`, 33 | data: comment 34 | }); 35 | }; 36 | 37 | export const createComment = (comment) => { 38 | return $.ajax({ 39 | method: 'post', 40 | url: '/api/comments', 41 | data: comment 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /app/models/annotation.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: annotations 4 | # 5 | # id :integer not null, primary key 6 | # author_id :integer not null 7 | # score :integer default("0"), not null 8 | # description :text not null 9 | # song_id :integer not null 10 | # start_index :integer not null 11 | # end_index :integer not null 12 | # created_at :datetime not null 13 | # updated_at :datetime not null 14 | # 15 | 16 | class Annotation < ApplicationRecord 17 | validates :author, :song, :description, :start_index, :end_index, presence: true 18 | 19 | has_many :votes, 20 | primary_key: :id, 21 | foreign_key: :annotation_id, 22 | class_name: "Vote" 23 | 24 | has_many :comments, as: :commentable 25 | 26 | belongs_to :song 27 | belongs_to :author, 28 | primary_key: :id, 29 | foreign_key: :author_id, 30 | class_name: "User" 31 | 32 | def user_votes 33 | self.votes.sum(:value) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /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: 85c42e6d0a8f49f8afa003ddd3aecffef90548ddb4c1e7bc256dd2388dd885d18ea1a1cda76eb216a915674c492a5530f9473417f7ee689b4844a43fbbf3e109 15 | 16 | test: 17 | secret_key_base: cd9162b23d5a2cf4ea27ed8bc06d962da96c174de9f7f4ec6ec1c575f666a8874985bb32dcc1386925c7e0f5b6669e0fdbe512e3c1504496401d049bc1ea6c31 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/header/header_navigation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class HeaderNavigation extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | } 8 | 9 | render(){ 10 | let newSongLink = ""; 11 | let homeLink = Top Songs; 12 | if(this.props.user){ 13 | newSongLink = New Song; 14 | } 15 | return( 16 |
17 |
18 | 19 | 20 | {homeLink} 21 | {newSongLink} 22 |
23 |
24 | ); 25 | } 26 | } 27 | 28 | export default HeaderNavigation; 29 | -------------------------------------------------------------------------------- /frontend/components/track_form/track_form_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createSong, clearErrors, updateSong, fetchSong } from '../../actions/song_actions'; 3 | import TrackForm from './track_form'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | let formType = "edit"; 7 | if(ownProps.location.pathname.replace(/\//g, "") === "new_song"){ 8 | formType = "new"; 9 | } 10 | 11 | return { 12 | loading: state.loading.loading, 13 | currentUser: state.session.currentUser, 14 | errors: state.songs.errors, 15 | songId: ownProps.params.songId, 16 | formType: formType, 17 | currentTrack: state.songs.currentTrack 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = (dispatch, ownProps) => { 22 | const action = ownProps.params.songId ? updateSong : createSong; 23 | return { 24 | action: (song, id) => dispatch(action(song, id)), 25 | clearErrors: () => dispatch(clearErrors()), 26 | fetchSong: (id) => dispatch(fetchSong(id)) 27 | }; 28 | }; 29 | 30 | export default connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(TrackForm); 34 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Annotator 5 | 15 | <%= csrf_meta_tags %> 16 | 17 | <%= stylesheet_link_tag 'application', media: 'all' %> 18 | <%= javascript_include_tag 'application' %> 19 | 20 | 21 | <%= favicon_link_tag 'favicon.ico' %> 22 | 30 | 31 | 32 | 33 | <%= yield %> 34 | 35 | 36 | -------------------------------------------------------------------------------- /frontend/components/comments/comment_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createComment, 3 | deleteComment, 4 | updateComment, 5 | fetchComment, 6 | clearErrors } from '../../actions/comment_actions'; 7 | import CommentIndex from './comment_index'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | comments: ownProps.comments, 12 | commentableId: ownProps.commentableId, 13 | commentableType: ownProps.commentableType, 14 | currentUser: state.session.currentUser, 15 | overRide: ownProps.overRide, 16 | errors: state.comments.errors 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = (dispatch) => { 21 | return { 22 | createComment: (comment) => dispatch(createComment(comment)), 23 | deleteComment: (id) => dispatch(deleteComment(id)), 24 | updateComment: (comment) => dispatch(updateComment(comment)), 25 | fetchComment: (id) => dispatch(fetchComment(id)), 26 | clearErrors: () => dispatch(clearErrors()) 27 | }; 28 | }; 29 | 30 | export default connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(CommentIndex); 34 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 4 | import TracksIndexContainer from './tracks_index/tracks_index_container'; 5 | import TrackFormContainer from './track_form/track_form_container'; 6 | import TrackShowContainer from './track_show/track_show_container'; 7 | 8 | import App from './app'; 9 | 10 | 11 | const Root = ({ store }) => { 12 | 13 | const _ensureLoggedIn = (nextState, replace) => { 14 | const currentUser = store.getState().session.currentUser; 15 | if(!currentUser){ 16 | replace('/'); 17 | } 18 | }; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default Root; 34 | -------------------------------------------------------------------------------- /docs/api_endpoints.md: -------------------------------------------------------------------------------- 1 | # API Endpoints 2 | 3 | ## HTML API 4 | 5 | ### Root 6 | 7 | - `GET /` - loads React web app 8 | 9 | ## JSON API 10 | 11 | ### Users 12 | 13 | - `POST /api/users` 14 | - `DELETE /api/users/:id` 15 | 16 | ### Session 17 | 18 | - `POST /api/session` 19 | - `DELETE /api/session` 20 | 21 | ### SONGS 22 | 23 | - `GET /api/songs` 24 | - Songs index/search 25 | - accepts `tag_name` query param to list songs by tag 26 | - `POST /api/songs` 27 | - `GET /api/songs/:id` 28 | - `PATCH /api/songs/:id` 29 | - `DELETE /api/songs/:id` 30 | 31 | ### Annotation 32 | 33 | - `GET /api/annotations/:id`ß 34 | - `POST /api/songs/:song_id/annotations/` 35 | - `GET /api/songs/:song_id/annotations/` 36 | - `DELETE /api/annotations/:id` 37 | - `GET /api/annotations/:annotationId/comments` //Possible un-nest here 38 | - Return all comments for specific annotation 39 | 40 | ### Tags 41 | 42 | - A note's tags will be included in the note show template 43 | - `GET /api/tags` 44 | - includes query param for typeahead suggestions 45 | - `POST /api/songs/:songId/tags`: add tag to song by name 46 | - `DELETE tags/:tag_name`: remove tag from song by 47 | name 48 | -------------------------------------------------------------------------------- /frontend/components/header/header_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import { signup, login, logout, clearErrors } from '../../actions/session_actions'; 4 | import { 5 | openLoginModal, 6 | openSignupModal, 7 | closeModal 8 | } from '../../actions/login_modal_actions'; 9 | import Header from './header'; 10 | 11 | const mapStateToProps = state => { 12 | return { 13 | currentUser: state.session.currentUser, 14 | errors: state.session.errors, 15 | loginModalOpen: state.loginModal.open, 16 | loginModalType: state.loginModal.type 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = (dispatch, ownProps) => { 21 | return { 22 | signup: (user) => dispatch(signup(user)), 23 | login: (user) => dispatch(login(user)), 24 | logout: () => dispatch(logout()), 25 | clearErrors: () => dispatch(clearErrors()), 26 | openLoginModal: () => dispatch(openLoginModal()), 27 | openSignupModal: () => dispatch(openSignupModal()), 28 | closeModal: () => dispatch(closeModal()), 29 | }; 30 | }; 31 | 32 | export default connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(Header); 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/components.md: -------------------------------------------------------------------------------- 1 | ## Component Hierarchy 2 | 3 | **AuthFormContainer** 4 | - AuthForm 5 | 6 | **HomeContainer** 7 | - Home 8 | 9 | **NavBarContainer** 10 | - Search 11 | - Navigation 12 | 13 | **SongContainer** 14 | - Lyrics 15 | 16 | **AnnotationContainer** 17 | - Annotation 18 | 19 | **SearchResultsContainer** 20 | - Search 21 | 22 | **TagContainer** 23 | - Tags 24 | 25 | **SongIndex** 26 | - SongIndexItem 27 | 28 | 29 | **NewSongContainer** 30 | - NewSong 31 | - NewSongButton 32 | 33 | **Search** 34 | 35 | **NewSong** 36 | - NewSong 37 | 38 | **NewTag** 39 | - NewTag 40 | 41 | **SongSearch** 42 | + AutoSearch 43 | * AutoSearchResults 44 | 45 | **TagsSearch** 46 | + AutoSearch 47 | * AutoSearchResults 48 | 49 | ## Routes 50 | 51 | |Path | Component | 52 | |-------|-------------| 53 | | "/sign-up" | "AuthFormContainer" | 54 | | "/sign-in" | "AuthFormContainer" | 55 | | "/home" | "HomeContainer" | 56 | | "/home/song/:songId" | "SongContainer" | 57 | | "/home/tag/:tagId/song/:songId" | "TagContainer" | 58 | | "/home/search-results" | "SearchResultsContainer" 59 | | "/new-song" | "NewSongContainer" | 60 | | "/search" | "Search" | 61 | | "/new-annotation" | "NewAnnotationContainer" | 62 | | "/new-tag" | "NewTag" | 63 | | "/tag-search" | "TagSearch" | 64 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as SessionApiUtil from '../util/session_api_util'; 2 | 3 | export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER"; 4 | export const RECEIVE_SESSION_ERRORS = "RECEIVE_SESSION_ERRORS"; 5 | export const CLEAR_ERRORS = "CLEAR_ERRORS"; 6 | 7 | const receiveCurrentUser = (user) => { 8 | return { 9 | type: RECEIVE_CURRENT_USER, 10 | user 11 | }; 12 | }; 13 | 14 | const receiveErrors = (errors) => { 15 | return { 16 | type: RECEIVE_SESSION_ERRORS, 17 | errors 18 | }; 19 | }; 20 | 21 | export const clearErrors = () => { 22 | return { 23 | type: CLEAR_ERRORS 24 | }; 25 | }; 26 | 27 | export const login = (user) => dispatch => { 28 | return SessionApiUtil.login(user) 29 | .then(user => dispatch(receiveCurrentUser(user)), 30 | errors => dispatch(receiveErrors(errors.responseJSON))); 31 | }; 32 | 33 | export const signup = (user) => dispatch => { 34 | return SessionApiUtil.signup(user) 35 | .then(user => dispatch(receiveCurrentUser(user)), 36 | errors => dispatch(receiveErrors(errors.responseJSON))); 37 | }; 38 | 39 | export const logout = (user) => dispatch => { 40 | return SessionApiUtil.logout(user) 41 | .then(() => dispatch(receiveCurrentUser(null)), 42 | errors => dispatch(receiveErrors(errors))); 43 | }; 44 | -------------------------------------------------------------------------------- /app/controllers/api/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::CommentsController < ApplicationController 2 | def create 3 | @comment = Comment.new(comment_params) 4 | 5 | if @comment.save 6 | render '/api/comments/show' 7 | else 8 | render json: @comment.errors.full_messages, status: 422 9 | end 10 | end 11 | 12 | def update 13 | @comment = Comment.find(params:id) 14 | if @comment.update_attributes(comment_params) 15 | render '/api/comments/show' 16 | else 17 | render json: @comment.errors.full_messages, status: 422 18 | end 19 | end 20 | 21 | def destroy 22 | @comment = Comment.find(params[:id]) 23 | 24 | if @comment 25 | @comment.destroy 26 | render 'api/comments/show' 27 | else 28 | render json: ["No such comment"], status: 404 29 | end 30 | end 31 | 32 | def index 33 | if params[:song_id] 34 | @comments = Comment.where(commentable_id: params[:song_id], commentable_type: "Song") 35 | else 36 | @comments = Comment.where(commentable_id: params[:annotation_id], commentable_type: "Annotation") 37 | end 38 | render 'api/comments/index' 39 | end 40 | 41 | private 42 | 43 | def comment_params 44 | params.require(:comment).permit(:commentable_id, :commentable_type, :author_id, :body) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/api/songs_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SongsController < ApplicationController 2 | def create 3 | @song = Song.new(song_params) 4 | if @song.save 5 | render "/api/songs/show" 6 | else 7 | render json: @song.errors, status: 422 8 | end 9 | 10 | end 11 | 12 | def index 13 | if params[:query] 14 | @song_titles = Song.search_title_for(params[:query]).limit(5) 15 | @song_artists = Song.search_artist_for(params[:query]).limit(5) 16 | else 17 | @songs = Song.all 18 | render "/api/songs/index" 19 | end 20 | end 21 | 22 | def show 23 | @song = Song.find(params[:id]) 24 | render "/api/songs/show" 25 | end 26 | 27 | def update 28 | @song = Song.find(params[:id]) 29 | 30 | if @song.update_attributes(song_params) 31 | render "/api/songs/show" 32 | else 33 | render json @song.errors.full_messages, status: 422 34 | end 35 | end 36 | 37 | def destroy 38 | @song = Song.find(params[:id]) 39 | 40 | if @song 41 | @song.destroy 42 | render "/api/songs/show" 43 | else 44 | render json: ["No such song"], status: 404 45 | end 46 | end 47 | 48 | private 49 | 50 | def song_params 51 | params.require(:song).permit(:title, :lyrics, :author_id, :artist, :image, :query) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /frontend/reducers/comment_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_COMMENTS, 3 | RECEIVE_COMMENT, 4 | REMOVE_COMMENT, 5 | RECEIVE_COMMENT_ERRORS, 6 | CLEAR_ERRORS, 7 | } from '../actions/comment_actions'; 8 | import merge from 'lodash/merge'; 9 | 10 | let defaultState = { 11 | comments: {}, 12 | errors: [] 13 | }; 14 | 15 | const CommentsReducer = (state = defaultState, action) => { 16 | Object.freeze(state); 17 | 18 | switch(action.type){ 19 | case RECEIVE_COMMENTS: 20 | let receive = Object.assign({}, state); 21 | receive.comments = merge(receive.comments, action.comments); 22 | return receive; 23 | case RECEIVE_COMMENT: 24 | let receiveOne = Object.assign({}, state); 25 | receiveOne.comments = merge(receiveOne.comments, {[action.comment.id]: action.comment}); 26 | return receiveOne; 27 | case REMOVE_COMMENT: 28 | let deleteState = Object.assign({}, state); 29 | delete deleteState.comments[action.id]; 30 | return deleteState; 31 | case RECEIVE_COMMENT_ERRORS: 32 | const errors = action.errors; 33 | return merge({}, state, { errors }); 34 | case CLEAR_ERRORS: 35 | const clearedErrors = Object.assign({}, state); 36 | clearedErrors.errors = []; 37 | return clearedErrors; 38 | default: 39 | return state; 40 | } 41 | }; 42 | 43 | export default CommentsReducer; 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | 4 | var plugins = []; // if using any plugins for both dev and production 5 | var devPlugins = []; // if using any plugins for development 6 | 7 | var prodPlugins = [ 8 | new webpack.DefinePlugin({ 9 | 'process.env': { 10 | 'NODE_ENV': JSON.stringify('production') 11 | } 12 | }), 13 | new webpack.optimize.UglifyJsPlugin({ 14 | compress: { 15 | warnings: true 16 | } 17 | }) 18 | ]; 19 | 20 | plugins = plugins.concat( 21 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins 22 | ); 23 | 24 | 25 | module.exports = { 26 | context: __dirname, 27 | entry: "./frontend/annotator.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 | test: /\.css$/, 45 | loaders: [ 46 | 'style-loader', 'css-loader' 47 | ] 48 | } 49 | ] 50 | }, 51 | devtool: 'source-maps', 52 | resolve: { 53 | extensions: [".js", ".jsx", "*"] 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/songs/songs.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/songs controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | 5 | body { 6 | background-color: #F7F7F7; 7 | } 8 | 9 | .tracks-index { 10 | margin: auto; 11 | margin-top: 50px; 12 | width: 750px; 13 | background-color: #F7F7F7; 14 | border-top: 6px solid black; 15 | } 16 | 17 | .track-listing { 18 | display: flex; 19 | justify-content: flex-start; 20 | padding: 5px 10px; 21 | border-bottom: 1px solid #CCCCCC; 22 | } 23 | 24 | .track-listing:hover { 25 | transition: .2s; 26 | background-color: rgb(233, 233, 233); 27 | } 28 | 29 | 30 | 31 | 32 | .track-rank { 33 | width: 40px; 34 | text-align: center; 35 | align-self: center; 36 | } 37 | 38 | .track-rank h1 { 39 | margin: auto; 40 | } 41 | 42 | .track-info { 43 | display: flex; 44 | flex-direction: column; 45 | margin-left: 15px; 46 | justify-content: center; 47 | } 48 | 49 | .thumbnail { 50 | width: 100px; 51 | height: 100px; 52 | } 53 | 54 | .top h1 { 55 | font-size: 30px; 56 | 57 | } 58 | 59 | .top span { 60 | font-size: 18px; 61 | } 62 | 63 | .track-listing img { 64 | height: 35px; 65 | width: 35px; 66 | display: block; 67 | align-self: center; 68 | } 69 | 70 | .top img { 71 | height: 75px; 72 | width: 75px; 73 | display: block; 74 | } 75 | -------------------------------------------------------------------------------- /app/models/song.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: songs 4 | # 5 | # id :integer not null, primary key 6 | # title :string not null 7 | # lyrics :text not null 8 | # author_id :integer not null 9 | # artist :string not null 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # image_file_name :string 13 | # image_content_type :string 14 | # image_file_size :integer 15 | # image_updated_at :datetime 16 | # 17 | 18 | class Song < ApplicationRecord 19 | include PgSearch 20 | 21 | validates :title, :lyrics, :author_id, :artist, presence: true 22 | validates_uniqueness_of :title, scope: :artist 23 | has_attached_file :image, default_url: "pacific_myth.jpg" 24 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 25 | pg_search_scope :search_title_for, against: :title, using: { tsearch: { any_word: true, prefix: true } } 26 | pg_search_scope :search_artist_for, against: :artist, using: { tsearch: { any_word: true, prefix: true } } 27 | 28 | belongs_to :author, 29 | primary_key: :id, 30 | foreign_key: :author_id, 31 | class_name: :User 32 | 33 | has_many :annotations 34 | has_many :comments, as: :commentable 35 | 36 | 37 | def comment_ids 38 | comments.map do |comment| 39 | comment.id 40 | end 41 | end 42 | 43 | 44 | 45 | end 46 | -------------------------------------------------------------------------------- /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 | 17 | 18 | 19 | * { 20 | /*Reset's every elements apperance*/ 21 | background: none repeat scroll 0 0 transparent; 22 | border: medium none; 23 | border-spacing: 0; 24 | color: inherit; 25 | font-family: sans-serif; 26 | font-size: 16px; 27 | line-height: 1.42rem; 28 | list-style: none outside none; 29 | margin: 0; 30 | padding: 0; 31 | text-align: left; 32 | text-decoration: none; 33 | text-indent: 0; 34 | box-sizing: border-box; 35 | } 36 | 37 | 38 | *:focus { 39 | outline: none; 40 | } 41 | 42 | a:hover { 43 | cursor:pointer; 44 | } 45 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search.scss: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | background-color: white; 3 | width: 100%; 4 | } 5 | 6 | .search-section { 7 | width: 300px; 8 | padding-left: 20px; 9 | position: relative; 10 | 11 | } 12 | 13 | .hidden { 14 | display: none; 15 | } 16 | 17 | .visible { 18 | display: block; 19 | } 20 | 21 | .no-results { 22 | padding: 15px; 23 | } 24 | 25 | .search-result-list { 26 | width: 272px; 27 | display: flex; 28 | flex-direction: column; 29 | position: absolute; 30 | z-index: 20; 31 | background-color: #F7F7F7; 32 | padding: 0; 33 | box-shadow: 5px 5px 5px; 34 | top: 40px; 35 | left: 30px; 36 | } 37 | 38 | .search-result-list.hidden { 39 | display: none; 40 | } 41 | 42 | 43 | .search-thumbnail { 44 | width: 40px; 45 | height: 40px; 46 | margin: 3px; 47 | } 48 | 49 | .search-item { 50 | display: flex; 51 | padding: 0; 52 | margin: 5px 0px; 53 | background-color: white; 54 | } 55 | 56 | .search-item:hover { 57 | background-color: #e9e9e9; 58 | transition: 0.2s; 59 | cursor: pointer; 60 | } 61 | 62 | .search-item:first-child { 63 | margin-top: 0; 64 | } 65 | .search-item:last-child { 66 | margin-bottom: 10px; 67 | } 68 | .search-item a { 69 | display: flex; 70 | padding: 0px; 71 | padding-left: 5px; 72 | } 73 | 74 | .search-item img { 75 | padding: 0 76 | } 77 | 78 | .search-header{ 79 | background-color: white; 80 | background-color: #F7F7F7; 81 | font-weight: lighter; 82 | padding-bottom: 20px; 83 | border-bottom: #CCCCCC; 84 | 85 | } 86 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/comments.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the api/comments controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | 5 | .comments-container { 6 | background-color: white; 7 | width: 60%; 8 | margin: auto; 9 | padding-bottom: 40px; 10 | 11 | } 12 | 13 | .comments{ 14 | 15 | width: 50%; 16 | background-color: #F7F7F7; 17 | margin-left: 20px; 18 | padding: 20px; 19 | } 20 | 21 | .comments h2 { 22 | margin-bottom: 10px; 23 | } 24 | 25 | .comment-form { 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | .comment-body { 31 | border: 2px solid #CCCCCC; 32 | background-color: white; 33 | margin-bottom: 10px; 34 | padding: 5px; 35 | } 36 | 37 | 38 | .comment-body:focus { 39 | border: 2px solid #99A7EE; 40 | } 41 | 42 | .comment-item { 43 | border-top: 1px solid #99A7EE; 44 | padding-left: 10px; 45 | } 46 | 47 | .comment-item:last-child{ 48 | border-bottom: 1px solid #99A7EE; 49 | } 50 | 51 | .comment-item h2 { 52 | margin-top: 5px; 53 | color: #222222; 54 | } 55 | 56 | .comment-item h3 { 57 | margin: 15px 0px; 58 | font-weight: lighter; 59 | } 60 | 61 | .annotation-comment { 62 | width: 100%; 63 | margin: 20px; 64 | } 65 | 66 | .annotation-comment .comments { 67 | width: 90% 68 | } 69 | 70 | .comment-controls { 71 | display: flex; 72 | } 73 | 74 | .comment-controls button { 75 | margin-right: 10px; 76 | color: #9A9A9A; 77 | text-decoration: underline; 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Annotator", 3 | "version": "1.0.0", 4 | "description": "This README would normally document whatever steps are necessary to get the application up and running.", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "postinstall": "webpack" 13 | }, 14 | "engines": { 15 | "node": "6.10.0", 16 | "npm": "3.10.10" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/drj17/Annotator.git" 21 | }, 22 | "keywords": [], 23 | "author": "", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/drj17/Annotator/issues" 27 | }, 28 | "homepage": "https://github.com/drj17/Annotator#readme", 29 | "dependencies": { 30 | "babel-core": "^6.24.1", 31 | "babel-loader": "^6.4.1", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-react": "^6.24.1", 34 | "draft-js": "^0.10.0", 35 | "halogen": "^0.2.0", 36 | "lodash": "^4.17.4", 37 | "react": "^15.5.4", 38 | "react-addons-update": "^15.5.2", 39 | "react-dom": "^15.5.4", 40 | "react-modal": "^1.7.7", 41 | "react-onclickoutside": "^5.11.1", 42 | "react-quill": "^1.0.0-rc.2", 43 | "react-redux": "^5.0.4", 44 | "react-router": "^3.0.5", 45 | "react-spinkit": "^2.1.1", 46 | "redux": "^3.6.0", 47 | "redux-thunk": "^2.2.0", 48 | "style-loader": "^0.17.0", 49 | "webpack": "^2.4.1", 50 | "css-loader": "^0.28.1" 51 | }, 52 | "devDependencies": { 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :integer not null, primary key 6 | # username :string not null 7 | # password_digest :string not null 8 | # session_token :string not null 9 | # created_at :datetime not null 10 | # updated_at :datetime not null 11 | # iq :integer default("0") 12 | # 13 | 14 | class User < ApplicationRecord 15 | validates :username, :password_digest, :session_token, presence: true 16 | validates :password, length: { minimum: 6, allow_nil: true } 17 | attr_reader :password 18 | 19 | after_initialize :ensure_session_token 20 | 21 | has_many :songs 22 | has_many :annotations 23 | has_many :votes 24 | has_many :comments, 25 | primary_key: :id, 26 | foreign_key: :author_id, 27 | class_name: "Comment" 28 | 29 | def self.find_by_credentials(username, password) 30 | user = User.find_by(username: username) 31 | 32 | user && user.is_password?(password) ? user : nil 33 | end 34 | 35 | def password=(password) 36 | @password = password 37 | self.password_digest = BCrypt::Password.create(password) 38 | end 39 | 40 | def is_password?(password) 41 | BCrypt::Password.new(self.password_digest).is_password?(password) 42 | end 43 | 44 | def reset_token! 45 | self.session_token = SecureRandom.urlsafe_base64(16) 46 | self.save! 47 | self.session_token 48 | end 49 | 50 | private 51 | 52 | def ensure_session_token 53 | self.session_token ||= SecureRandom.urlsafe_base64(16) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/songs/track_form.scss: -------------------------------------------------------------------------------- 1 | .new-song-form-container h1 { 2 | text-align: center; 3 | font-size: 20px; 4 | font-weight: bold; 5 | margin-bottom: 20px; 6 | margin-top: 20px; 7 | } 8 | 9 | 10 | .new-song-form { 11 | width: 60%; 12 | margin: auto; 13 | display: flex; 14 | flex-direction: column; 15 | 16 | justify-content: flex-start; 17 | 18 | } 19 | 20 | .new-song-form label { 21 | font-size: 28px; 22 | display: flex; 23 | justify-content: space-between; 24 | flex-direction: column; 25 | margin-bottom: 20px; 26 | } 27 | 28 | .new-song-form input { 29 | font-size: 20px; 30 | background-color: white; 31 | border: 2px solid #CCCCCC; 32 | padding: 2px; 33 | margin-bottom: 15px; 34 | margin-left: 15px; 35 | } 36 | 37 | .input-field { 38 | width: 300px; 39 | position: relative; 40 | } 41 | 42 | .new-song-form input:focus { 43 | border: 2px solid #99A7EE; 44 | } 45 | .new-song-form textarea:focus { 46 | border: 2px solid #99A7EE; 47 | } 48 | 49 | .new-song-form textarea { 50 | background-color: white; 51 | padding: 15px; 52 | width: 60%; 53 | height: 400px; 54 | border: 2px solid #CCCCCC; 55 | margin-left: 15px; 56 | margin-bottom: 20px; 57 | } 58 | 59 | .new-song-form .form-submit { 60 | width: 125px; 61 | border: 2px solid black; 62 | padding: 3px; 63 | text-align: center; 64 | } 65 | 66 | .new-song-form .form-submit:hover { 67 | transition: .2s; 68 | background-color: #CCCCCC; 69 | } 70 | 71 | .preview-image { 72 | margin-left: 15px; 73 | margin-bottom: 15px; 74 | height: 150px; 75 | width: 150px; 76 | } 77 | 78 | .errors { 79 | color: red; 80 | padding-left: 15px; 81 | } 82 | 83 | #upload { 84 | padding: 5px; 85 | display: flex; 86 | justify-content: center; 87 | } 88 | -------------------------------------------------------------------------------- /frontend/components/track_show/track_show_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { deleteSong, updateSong, fetchSong } from '../../actions/song_actions'; 3 | import { fetchAnnotations, 4 | fetchAnnotation, 5 | openAnnotation, 6 | closeAnnotation, 7 | changeAnnotationType } from '../../actions/annotation_actions'; 8 | import TrackShow from './track_show'; 9 | import React from 'react'; 10 | import values from 'lodash/values'; 11 | import { songComments } from '../../reducers/selectors'; 12 | import { fetchSongComments, clearErrors } from '../../actions/comment_actions'; 13 | 14 | 15 | const mapStateToProps = (state, ownProps) => { 16 | return { 17 | currentTrack: state.songs.currentTrack, 18 | currentUser: state.session.currentUser, 19 | loading: state.loading.loading, 20 | trackId: ownProps.params.songId, 21 | annotations: state.annotations.annotations, 22 | currentAnnotation: state.annotations.currentAnnotation, 23 | comments: songComments(state), 24 | open: state.annotations.open 25 | }; 26 | }; 27 | 28 | 29 | const mapDispatchToProps = dispatch => { 30 | return { 31 | fetchSong: (id) => dispatch(fetchSong(id)), 32 | deleteSong: (id) => dispatch(deleteSong(id)), 33 | fetchAnnotations: (id) => dispatch(fetchAnnotations(id)), 34 | fetchAnnotation: (id) => dispatch(fetchAnnotation(id)), 35 | fetchSongComments: (id) => dispatch(fetchSongComments(id)), 36 | closeAnnotation: () => dispatch(closeAnnotation()), 37 | openAnnotation: () => dispatch(openAnnotation()), 38 | clearErrors: () => dispatch(clearErrors()), 39 | changeAnnotationType: (type) => dispatch(changeAnnotationType(type)) 40 | }; 41 | }; 42 | 43 | export default connect( 44 | mapStateToProps, 45 | mapDispatchToProps 46 | )(TrackShow); 47 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend/components/annotations/annotation_container.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | createAnnotation, 4 | updateAnnotation, 5 | deleteAnnotation, 6 | fetchAnnotations, 7 | changeAnnotationType, 8 | openAnnotation, 9 | closeAnnotation 10 | } from '../../actions/annotation_actions'; 11 | import Annotation from './annotation'; 12 | import { annotationComments } from '../../reducers/selectors'; 13 | import { fetchAnnotationComments } from '../../actions/comment_actions'; 14 | import { openLoginModal } from '../../actions/login_modal_actions'; 15 | 16 | const mapStateToProps = (state, ownProps) => { 17 | return { 18 | currentAnnotation: state.annotations.currentAnnotation, 19 | currentTrack: state.songs.currentTrack, 20 | annotationType: state.annotations.annotationType, 21 | currentUser: state.session.currentUser, 22 | selection: ownProps.selection, 23 | position: ownProps.position, 24 | comments: annotationComments(state), 25 | loginOpen: state.loginModal.open 26 | }; 27 | }; 28 | 29 | const mapDispatchToProps = dispatch => { 30 | return { 31 | createAnnotation: (annotation) => dispatch(createAnnotation(annotation)), 32 | deleteAnnotation: (id) => dispatch(deleteAnnotation(id)), 33 | updateAnnotation: (annotation, vote) => dispatch(updateAnnotation(annotation, vote)), 34 | fetchAnnotations: (id) => dispatch(fetchAnnotations(id)), 35 | fetchAnnotationComments: (id) => dispatch(fetchAnnotationComments(id)), 36 | changeAnnotationType: (annotationType) => dispatch(changeAnnotationType(annotationType)), 37 | openAnnotation: () => dispatch(openAnnotation()), 38 | closeAnnotation: () => dispatch(closeAnnotation()), 39 | openLoginModal: () => dispatch(openLoginModal()) 40 | }; 41 | }; 42 | 43 | export default connect( 44 | mapStateToProps, 45 | mapDispatchToProps 46 | )(Annotation); 47 | -------------------------------------------------------------------------------- /frontend/reducers/songs_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ERRORS, 3 | CLEAR_ERRORS, 4 | RECEIVE_SONG, 5 | RECEIVE_ALL_SONGS, 6 | REMOVE_SONG 7 | } from '../actions/song_actions'; 8 | import merge from 'lodash/merge'; 9 | 10 | let defaultState = { 11 | tracks: [], 12 | errors: { 13 | title: "", 14 | artist: "", 15 | lyrics: "" 16 | }, 17 | currentTrack: { comments: [] }, 18 | }; 19 | 20 | import { RECEIVE_COMMENT, REMOVE_COMMENT } from '../actions/comment_actions'; 21 | 22 | const SongsReducer = (state = defaultState, action) => { 23 | 24 | Object.freeze(state); 25 | switch(action.type){ 26 | case RECEIVE_COMMENT: 27 | let receive = merge({}, state); 28 | receive.currentTrack.comments.unshift(action.comment.id); 29 | return receive; 30 | case REMOVE_COMMENT: 31 | let remove = merge({}, state); 32 | remove.currentTrack.comments 33 | = remove.currentTrack.comments.filter((id) => id !== action.comment.id); 34 | return remove; 35 | case RECEIVE_ALL_SONGS: 36 | const receiveAll = Object.assign({}, state); 37 | receiveAll.tracks = action.tracks; 38 | return receiveAll; 39 | case RECEIVE_SONG: 40 | const receiveSong = Object.assign({}, state); 41 | receiveSong.currentTrack = action.track; 42 | return receiveSong; 43 | case REMOVE_SONG: 44 | const removeSong = Object.assign({}, state); 45 | removeSong.tracks.filter(track => track.id === action.track.id); 46 | return removeSong; 47 | case RECEIVE_ERRORS: 48 | const errors = action.errors; 49 | return merge({}, state, { errors }); 50 | case CLEAR_ERRORS: 51 | const clearedErrors = Object.assign({}, state); 52 | clearedErrors.errors = defaultState.errors; 53 | return clearedErrors; 54 | default: 55 | return state; 56 | } 57 | }; 58 | 59 | export default SongsReducer; 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/header/header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderLoggedIn from './header_logged_in'; 3 | import HeaderLoggedOut from './header_logged_out'; 4 | import HeaderNavigation from './header_navigation'; 5 | import { Link } from 'react-router'; 6 | import SearchContainer from '../search/search_container'; 7 | 8 | class Header extends React.Component { 9 | constructor(props){ 10 | super(props); 11 | 12 | } 13 | 14 | 15 | render(){ 16 | if(this.props.currentUser){ 17 | return( 18 |
19 |
20 | 21 |

ANNOTATOR

22 | 27 |
28 | 29 |
30 | ); 31 | } else { 32 | return ( 33 |
34 |
35 | 36 | 37 | 38 |

ANNOTATOR

39 | 50 |
51 | 52 |
53 | ); 54 | } 55 | } 56 | } 57 | 58 | export default Header; 59 | -------------------------------------------------------------------------------- /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 | password_digest | string | not null 10 | session_token | string | not null, indexed, unique 11 | 12 | ## songs 13 | column name | data type | details 14 | ------------|-----------|----------------------- 15 | id | integer | not null, primary key 16 | title | string | not null 17 | body | text | not null 18 | author_id | integer | not null, foreign key (references users), indexed 19 | artist | string | not null, indexed 20 | 21 | ## annotations 22 | column name | data type | details 23 | ------------|-----------|----------------------- 24 | id | integer | not null, primary key 25 | author_id | integer | not null, foreign key (references users), indexed 26 | score | integer | not null, default: 0 27 | description | string | not null 28 | song_id | integer | not null, foreign key (references song), indexed 29 | start_index | integer | not null 30 | end_index | integer | not null 31 | 32 | ## comments 33 | column name | data type | details 34 | ------------|-----------|----------------------- 35 | id | integer | not null, primary key 36 | body | string | not null 37 | author_id | integer | not null, foreign key (references users) 38 | 39 | ## tags 40 | column name | data type | details 41 | ------------|-----------|----------------------- 42 | id | integer | not null, primary key 43 | name | string | not null 44 | 45 | ## taggings 46 | column name | data type | details 47 | ------------|-----------|----------------------- 48 | id | integer | not null, primary key 49 | song_id | integer | not null, foreign key (references songs), indexed, unique [tag_id] 50 | tag_id | integer | not null, foreign key (references tags), indexed 51 | -------------------------------------------------------------------------------- /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 | gem 'pg_search' 14 | # Use Puma as the app server 15 | gem 'puma', '~> 3.0' 16 | # Use SCSS for stylesheets 17 | gem 'sass-rails', '~> 5.0' 18 | # Use Uglifier as compressor for JavaScript assets 19 | gem 'uglifier', '>= 1.3.0' 20 | # Use CoffeeScript for .coffee assets and views 21 | gem 'coffee-rails', '~> 4.2' 22 | # See https://github.com/rails/execjs#readme for more supported runtimes 23 | # gem 'therubyracer', platforms: :ruby 24 | 25 | # Use jquery as the JavaScript library 26 | gem 'jquery-rails' 27 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 28 | gem 'jbuilder', '~> 2.5' 29 | # Use Redis adapter to run Action Cable in production 30 | # gem 'redis', '~> 3.0' 31 | # Use ActiveModel has_secure_password 32 | gem 'bcrypt', '~> 3.1.7' 33 | gem 'annotate' 34 | gem "paperclip", '~> 5.0.0' 35 | gem 'figaro' 36 | gem 'aws-sdk', '>= 2.0' 37 | 38 | 39 | # Use Capistrano for deployment 40 | # gem 'capistrano-rails', group: :development 41 | 42 | 43 | group :development, :test do 44 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console 45 | gem 'byebug', platform: :mri 46 | end 47 | 48 | group :development do 49 | # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. 50 | gem 'web-console', '>= 3.3.0' 51 | gem 'listen', '~> 3.0.5' 52 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 53 | gem 'spring' 54 | gem 'spring-watcher-listen', '~> 2.0.0' 55 | gem 'pry-rails' 56 | end 57 | 58 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 59 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 60 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /frontend/components/annotations/voting/votes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Votes = (props) => { 4 | 5 | let upvoteButton = ""; 6 | let downvoteButton = ""; 7 | let scoreColor; 8 | 9 | let downvoteStyle = {}; 10 | let upvoteStyle = {}; 11 | if(props.currentAnnotation.did_vote){ 12 | if(props.currentAnnotation.direction === 1){ 13 | upvoteStyle = {color: '#22C13E'}; 14 | } else if (props.currentAnnotation.direction === -1){ 15 | downvoteStyle = {color: 'red'}; 16 | } 17 | } 18 | 19 | if(props.currentAnnotation.score === 0){ 20 | scoreColor = "black"; 21 | } else if (props.currentAnnotation.score < 0){ 22 | scoreColor = "red"; 23 | } else { 24 | scoreColor = "#22C13E"; 25 | } 26 | 27 | let symbol = props.currentAnnotation.score > 0 ? "+" : ""; 28 | 29 | const setStyle = (direction) => { 30 | if(direction === "upvote"){ 31 | upvoteStyle = {color: '#22C13E'}; 32 | downvoteStyle = {}; 33 | } else { 34 | downvoteStyle = {color: 'red'}; 35 | upvoteStyle = {}; 36 | } 37 | }; 38 | 39 | if(props.currentUser){ 40 | downvoteButton = ; 43 | upvoteButton = ; 46 | 47 | let upvote = { 48 | user_id: props.currentUser.id, 49 | annotation_id: props.currentAnnotation.id, 50 | value: 1 51 | }; 52 | let downvote = { 53 | user_id: props.currentUser.id, 54 | annotation_id: props.currentAnnotation.id, 55 | value: -1 56 | }; 57 | 58 | const handleVote = (vote) => { 59 | props.updateAnnotation(props.currentAnnotation, vote); 60 | }; 61 | } 62 | 63 | return ( 64 |
65 | {downvoteButton} 66 | Upvote:{symbol}{props.currentAnnotation.score} 67 | {upvoteButton} 68 |
69 | ); 70 | }; 71 | 72 | export default Votes; 73 | -------------------------------------------------------------------------------- /frontend/components/comments/comment_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CommentIndexItem from './comment_index_item'; 3 | 4 | class CommentIndex extends React.Component{ 5 | constructor(props){ 6 | super(props); 7 | this.state = { 8 | text: "" 9 | }; 10 | this.annotationComment = ""; 11 | this.update = this.update.bind(this); 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | if(this.props.overRide){ 14 | this.annotationComment = "annotation-comment"; 15 | } 16 | } 17 | 18 | handleSubmit(e){ 19 | e.preventDefault(); 20 | this.props.createComment({comment:{ 21 | body: this.state.text, 22 | author_id: this.props.currentUser.id, 23 | commentable_id: this.props.commentableId, 24 | commentable_type: this.props.commentableType 25 | } 26 | }); 27 | this.props.clearErrors(); 28 | this.setState({text: ""}); 29 | } 30 | 31 | update(e){ 32 | this.setState({text: e.currentTarget.value}); 33 | } 34 | 35 | render(){ 36 | let comments = []; 37 | let unSortedComments = this.props.comments; 38 | let sortedComments = []; 39 | if(this.props.comments[0]){ 40 | sortedComments = this.props.comments.sort((a, b) => { 41 | return b.id - a.id; 42 | }); 43 | comments = sortedComments.map(comment => 44 | ); 51 | } 52 | 53 | let form = ""; 54 | if(this.props.currentUser){ 55 | form =
56 |