├── 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 |
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 = ;
59 | }
60 |
61 |
62 | return (
63 |
64 |
65 | {this.props.errors}
66 | {form}
67 | Comments:
68 | {comments}
69 |
70 |
71 | );
72 | }
73 | }
74 |
75 | export default CommentIndex;
76 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Annotator
2 | [Heroku](https://annotator-aa.herokuapp.com/)
3 |
4 | [Trello](https://trello.com/b/QmsORFAZ/annotator)
5 | ## Minimum Viable Product
6 |
7 | Annotator is a lyric annotation web application inspired by Genius. It is built on a Ruby on Rails backend with a React/Redux frontend.
8 | By the end of week 9, this app will, at minimum, meet the following criteria.
9 |
10 | - [ ] Hosting on Heroku
11 | - [ ] Account creation and login, as well as demo account login
12 | - [ ] Songs
13 | - [ ] Annotations
14 | - [ ] Voting
15 | - [ ] Search
16 | - [ ] Production ReadMe
17 |
18 | ## Design Docs
19 | * [Components](./components.md)
20 | * [Wireframes](./wireframes)
21 | * [Sample State](./sample_state.md)
22 | * [API-Endpoints](./api_endpoints.md)
23 | * [DB Schema](./schema.md)
24 |
25 |
26 | ## Development Timeline
27 | ### Phase 1: Backend setup and Front End User Authentication (2 Days)
28 | **Objective:** Create a functioning rails backend, as well as a front end user authentication system
29 |
30 |
31 | ### Phase 2: Song Model, API, and Components (1 Days)
32 | **Objective:** Create a song model that can be created/edited/updated/destroyed/ through the API.
33 | Users should be able to see a list of songs they have added.
34 |
35 | ### Phase 3: Annotation Model, API, and Components (2 Days)
36 | **Objective** Create an annotation model that can be created/edited/updated/destroyed/ through the API.
37 | Users should be able to see a list of annotations they have created.
38 |
39 | ### Phase 4: Implement Annotations in Songs (1 days)
40 | **Objectives** Users can highlight a section of text to create a new annotation (if one does not already exist).
41 | Annotations appear only when clicked. Annotations can be upvoted/downvoted.
42 |
43 | ### Phase 5: Implement Annotation Comment Model and API (1 day)
44 | **Objectives** User can create/update/edit/destroy comments on Annotations.
45 |
46 | ### Phase 6: Voting (1 Day)
47 | **Objectives** Users are able to vote on annotations, with upvotes or downvotes.
48 |
49 | ### Phase 7: Search (1 Day)
50 | **Objectives** Users are able to search for specific songs and be taken to their lyric page.
51 |
52 |
53 | ### Bonus Features
54 | - [ ] Sort songs on homepage by number of views (hot songs).
55 | - [ ] Add user rankings based on their total annotation upvote count.
56 | - [ ] Refactor: Create new model, api, and components for Artists.
57 | - [ ] Tags: Allow users to add tags to songs and filter by the tags.
58 |
--------------------------------------------------------------------------------
/frontend/reducers/annotations_reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | RECEIVE_ERRORS,
3 | CLEAR_ERRORS,
4 | RECEIVE_ANNOTATION,
5 | RECEIVE_ALL_ANNOTATIONS,
6 | REMOVE_ANNOTATION,
7 | OPEN_ANNOTATION,
8 | CLOSE_ANNOTATION,
9 | CHANGE_ANNOTATION_TYPE
10 | } from '../actions/annotation_actions';
11 |
12 | import { RECEIVE_COMMENT, REMOVE_COMMENT } from '../actions/comment_actions';
13 | import merge from 'lodash/merge';
14 |
15 | let defaultState = {
16 | annotations: [],
17 | currentAnnotation: {comments: []},
18 | open: false,
19 | annotationType: "show"
20 | };
21 |
22 | const AnnotationsReducer = (state = defaultState, action) => {
23 | Object.freeze(state);
24 | switch(action.type){
25 | case CHANGE_ANNOTATION_TYPE:
26 | let changeState = merge({}, state);
27 | changeState.annotationType = action.annotationType;
28 | return changeState;
29 | case CLOSE_ANNOTATION:
30 | let closeState = merge({}, state);
31 | closeState.open = false;
32 | return closeState;
33 | case OPEN_ANNOTATION:
34 | let openState = merge({}, state);
35 | openState.open = true;
36 | openState.annotationType = "show";
37 | return openState;
38 | case RECEIVE_COMMENT:
39 | let receive = merge({}, state);
40 | receive.currentAnnotation.comments.unshift(action.comment.id);
41 | return receive;
42 | case REMOVE_COMMENT:
43 | let remove = merge({}, state);
44 | remove.currentAnnotation.comments = remove.currentAnnotation.comments.filter((id) => id !== action.comment.id);
45 | return remove;
46 | case RECEIVE_ALL_ANNOTATIONS:
47 | const receiveAll = Object.assign({}, state);
48 | receiveAll.annotations = action.annotations;
49 | return receiveAll;
50 | case RECEIVE_ANNOTATION:
51 | const receiveAnnotation = Object.assign({}, state);
52 | receiveAnnotation.currentAnnotation = action.annotation;
53 | return receiveAnnotation;
54 | case REMOVE_ANNOTATION:
55 | const removeAnnotation = Object.assign({}, state);
56 | removeAnnotation.annotations.filter(annotation => annotation.id === action.annotation.id);
57 | return removeAnnotation;
58 | case RECEIVE_ERRORS:
59 | const errors = action.errors;
60 | return merge({}, state, { errors });
61 | case CLEAR_ERRORS:
62 | const clearedErrors = Object.assign({}, state);
63 | clearedErrors.errors = [];
64 | return clearedErrors;
65 | default:
66 | return state;
67 | }
68 | };
69 |
70 | export default AnnotationsReducer;
71 |
--------------------------------------------------------------------------------
/frontend/actions/song_actions.js:
--------------------------------------------------------------------------------
1 | import * as SongApiUtil from '../util/song_api_util';
2 | import { fetchAnnotations, finishLoadingAnnotations } from './annotation_actions';
3 |
4 | export const RECEIVE_ALL_SONGS = "RECEIVE_ALL_SONGS";
5 | export const RECEIVE_SONG = "RECEIVE_SONG";
6 | export const REMOVE_SONG = "REMOVE_SONG";
7 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
8 | export const CLEAR_ERRORS = "CLEAR_ERRORS";
9 | export const START_LOADING_SONG = "START_LOADING_SONG";
10 |
11 | const receiveAllSongs = (tracks) => {
12 | return {
13 | type: RECEIVE_ALL_SONGS,
14 | tracks: tracks.songs
15 | };
16 | };
17 |
18 | const receiveSong = (track) => {
19 | return {
20 | type: RECEIVE_SONG,
21 | track
22 | };
23 | };
24 |
25 | const removeSong = (track) => {
26 | return {
27 | type: REMOVE_SONG,
28 | track
29 | };
30 | };
31 |
32 | const receiveErrors = (errors) => {
33 | return {
34 | type: RECEIVE_ERRORS,
35 | errors
36 | };
37 | };
38 |
39 | export const clearErrors = () => {
40 | return {
41 | type: CLEAR_ERRORS
42 | };
43 | };
44 |
45 | export const startLoadingSong = () => {
46 | return {
47 | type: START_LOADING_SONG
48 | };
49 | };
50 |
51 | export const fetchSongs = () => dispatch => {
52 | return SongApiUtil.fetchSongs()
53 | .then(tracks => dispatch(receiveAllSongs(tracks)),
54 | errors => dispatch(receiveErrors(errors.responseJSON)));
55 | };
56 | export const fetchSong = (id) => dispatch => {
57 | dispatch(startLoadingSong());
58 | return SongApiUtil.fetchSong(id)
59 | .then(track => dispatch(receiveSong(track)),
60 | errors => dispatch(receiveErrors(errors.responseJSON)))
61 | .then(() => dispatch(fetchAnnotations(id)))
62 | .then(() => dispatch(finishLoadingAnnotations()));
63 | };
64 | export const deleteSong = (id) => dispatch => {
65 | dispatch(startLoadingSong());
66 | return SongApiUtil.deleteSong(id)
67 | .then(track => dispatch(removeSong(track)),
68 | errors => dispatch(receiveErrors(errors.responseJSON)));
69 | };
70 | export const updateSong = (track, id) => dispatch => {
71 | return SongApiUtil.updateSong(track, id)
72 | .then(track => dispatch(receiveSong(track)),
73 | errors => dispatch(receiveErrors(errors.responseJSON)));
74 | };
75 | export const createSong = (track) => dispatch => {
76 | dispatch(startLoadingSong());
77 | return SongApiUtil.createSong(track)
78 | .then(track => dispatch(receiveSong(track)),
79 | errors => dispatch(receiveErrors(errors.responseJSON)));
80 | };
81 |
--------------------------------------------------------------------------------
/frontend/actions/comment_actions.js:
--------------------------------------------------------------------------------
1 | import * as CommentApiUtil from '../util/comment_api_util';
2 |
3 | export const RECEIVE_COMMENTS = "RECEIVE_COMMENTS";
4 | export const REMOVE_COMMENT = "REMOVE_COMMENT";
5 | export const RECEIVE_COMMENT_ERRORS = "RECEIVE_COMMENT_ERRORS";
6 | export const CLEAR_ERRORS = "CLEAR_ERRORS";
7 | export const RECEIVE_COMMENT = "RECEIVE_COMMENT";
8 |
9 | export const receiveAllComments = (comments) => {
10 | return {
11 | type: RECEIVE_COMMENTS,
12 | comments
13 | };
14 | };
15 |
16 | export const receiveComment = (comment) => {
17 | return {
18 | type: RECEIVE_COMMENT,
19 | comment
20 | };
21 | };
22 |
23 | export const removeComment = (comment) => {
24 | return {
25 | type: REMOVE_COMMENT,
26 | comment
27 | };
28 | };
29 |
30 | export const receiveErrors = (errors) => {
31 | return {
32 | type: RECEIVE_COMMENT_ERRORS,
33 | errors
34 | };
35 | };
36 |
37 | export const clearErrors = () => {
38 | return {
39 | type: CLEAR_ERRORS
40 | };
41 | };
42 |
43 | export const fetchSongComments = (id) => dispatch => {
44 | return CommentApiUtil.fetchSongComments(id)
45 | .then(comments => dispatch(receiveAllComments(comments)),
46 | errors => dispatch(receiveErrors(errors.responseJSON)));
47 | };
48 | export const fetchAnnotationComments = (id) => dispatch => {
49 | return CommentApiUtil.fetchAnnotationComments(id)
50 | .then(comments => dispatch(receiveAllComments(comments)),
51 | errors => dispatch(receiveErrors(errors.responseJSON)));
52 | };
53 |
54 | export const fetchComment = (id) => dispatch => {
55 | return CommentApiUtil.fetchComment(id)
56 | .then(comment => dispatch(receiveComment(comment)),
57 | errors => dispatch(receiveErrors(errors.responseJSON)));
58 | };
59 | export const createComment = (comment) => dispatch => {
60 | return CommentApiUtil.createComment(comment)
61 | .then(comment => dispatch(receiveComment(comment)),
62 | errors => dispatch(receiveErrors(errors.responseJSON)));
63 | };
64 | export const updateComment = (comment) => dispatch => {
65 | return CommentApiUtil.updateComment(comment)
66 | .then(comment => dispatch(receiveComment(comment)),
67 | errors => dispatch(receiveErrors(errors.responseJSON)));
68 | };
69 |
70 | export const deleteComment = (id) => dispatch => {
71 | return CommentApiUtil.deleteComment(id)
72 | .then(comment => dispatch(removeComment(comment)),
73 | errors => dispatch(receiveErrors(errors.responseJSON)));
74 |
75 | };
76 |
77 |
78 |
79 |
80 | ////Annotations and songs can hold array of comment ids and pull out only the ones they want.
81 |
--------------------------------------------------------------------------------
/app/controllers/api/annotations_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::AnnotationsController < ApplicationController
2 | def create
3 | @annotation = Annotation.new(annotation_params)
4 |
5 | if @annotation.save
6 | render "/api/annotations/show"
7 | else
8 | render json: @annotation.errors.full_messages, status: 422
9 | end
10 | end
11 |
12 | def index
13 | @annotations = Annotation.where(song_id: params[:song_id])
14 | render "/api/annotations/index"
15 | end
16 |
17 | def show
18 | @annotation = Annotation.find(params[:id])
19 | @did_vote = false
20 | @direction = 0
21 | if current_user
22 | if Vote.exists?(user_id: current_user.id, annotation_id: @annotation.id)
23 | @user_vote = Vote.where(user_id: current_user.id, annotation_id: @annotation.id).first
24 | @did_vote = true
25 | @direction = @user_vote.value
26 | end
27 | end
28 | render "/api/annotations/show"
29 | end
30 |
31 | def update
32 | @annotation = Annotation.find(params[:id])
33 |
34 | if params[:vote]
35 | @vote = Vote.new(params[:vote].permit(:user_id, :annotation_id, :value))
36 | if @vote.save!
37 | @user = User.find(@vote.user_id)
38 | @did_vote = true
39 | @direction = @vote.value
40 | Vote.where('annotation_id = ? AND user_id = ? AND value != ?', @vote.annotation.id, @vote.user.id, @vote.value).destroy_all
41 | @annotation.update_attributes(score: @annotation.user_votes)
42 | render "/api/annotations/show"
43 | end
44 | else
45 | if @annotation.update_attributes(annotation_params)
46 | render "/api/annotations/show"
47 | else
48 | render json @annotation.errors.full_messages, status: 422
49 | end
50 | end
51 | end
52 |
53 | def destroy
54 | @annotation = Annotation.find(params[:id])
55 | @song = Song.find(@annotation.song_id)
56 | if @annotation
57 | @annotation.destroy
58 | render "api/songs/show"
59 | else
60 | render json: ["No such annotation"], status: 404
61 | end
62 | end
63 |
64 | def upvote
65 | vote(1)
66 | end
67 |
68 | def downvote
69 | vote(-1)
70 | end
71 |
72 | private
73 |
74 | def vote(direction)
75 | @annotation = Annotation.find(params[:id])
76 | @vote = @annotation.votes.find_or_initialize_by(user: current_user)
77 |
78 | unless @vote.update(value: direction)
79 | render json @vote.errors.full_messages
80 | end
81 | end
82 |
83 |
84 | def annotation_params
85 | params.require(:annotation).permit(:author_id, :score, :description, :song_id, :start_index, :end_index)
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/api/annotations.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the api/annotations controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 | //
5 |
6 | .annotation p{
7 |
8 | padding: 0px 10px;
9 | font-size: 22px;
10 | width: 100%;
11 | font-weight: lighter;
12 | }
13 |
14 | .annotation {
15 | margin-left: 20px;
16 | border-left: 4px solid #99A7EE;
17 | }
18 |
19 | .create-button {
20 | width: auto;
21 | height: 30px;
22 | border: 2px solid black;
23 | padding: 3px;
24 | margin-left: 10px;
25 | }
26 |
27 |
28 | .DraftEditor-editorContainer{
29 |
30 | }
31 |
32 | .public-DraftEditor-content {
33 | width: 100%;
34 | min-height: 150px;
35 | height: 100%;
36 | background-color: inherit;
37 | padding: 5px;
38 | padding-bottom: 10px;
39 | }
40 |
41 | .public-DraftStyleDefault-block:focus {
42 | border: 2px solid #99A7EE;
43 | }
44 |
45 | .new-annotation-header {
46 | text-align: center;
47 | font-weight: lighter;
48 | margin-bottom: 10px;
49 | }
50 |
51 |
52 | .text-editor {
53 | padding: 10px;
54 | width: 75%;
55 | margin: auto;
56 | }
57 |
58 | .draft-field {
59 | min-height: 150px;
60 |
61 | background-color: #F5F5F5;
62 | }
63 |
64 | .show-annotation {
65 | padding: 10px;
66 | width: 80%
67 | }
68 |
69 | .toolbar {
70 | display: flex;
71 | justify-content: flex-start;
72 | }
73 |
74 | .toolbar span {
75 | margin: 0px 10px;
76 | cursor: pointer;
77 | }
78 |
79 |
80 | .annotation-submit {
81 | margin: 15px 0px;
82 | width: 90px;
83 | height: 35px;
84 | background-color: white;
85 | border: 2px solid #0ECB27;
86 | color: #0ECB27;
87 | text-align: center;
88 |
89 | }
90 |
91 | .annotation-submit:hover {
92 | background-color: #0ECB27;
93 | color: white;
94 | transition: .2s;
95 | }
96 |
97 | .annotation-controls {
98 | display: flex;
99 | justify-content: space-between;
100 | }
101 |
102 | .delete-button {
103 | padding: 5px;
104 | width: 90px;
105 | height: 35px;
106 | background-color: white;
107 | border: 2px solid red;
108 | color: red;
109 | text-align: center;
110 | }
111 |
112 | .cancel-button {
113 | padding: 5px;
114 | width: 90px;
115 | height: 35px;
116 | background-color: white;
117 | border: 2px solid red;
118 | color: red;
119 | text-align: center;
120 | margin: 15px 0px;
121 | }
122 |
123 | .delete-button:hover {
124 | background-color: red;
125 | color: white;
126 | transition: .2s;
127 | }
128 |
129 | .edit-button {
130 | margin-left: auto;
131 | padding: 5px;
132 | width: 90px;
133 | height: 35px;
134 | background-color: white;
135 | border: 2px solid black;
136 | text-align: center
137 | }
138 |
139 | .edit-button:hover {
140 | transition: .2s;
141 | background-color: #CCCCCC;
142 | }
143 |
144 | .annotation-error {
145 | color: red;
146 | }
147 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/header.scss:
--------------------------------------------------------------------------------
1 | body .login-modal {
2 | width: 450px;
3 | height: auto;
4 |
5 | margin: auto;
6 | margin-top: 50px;
7 | background-color: #E9E9E9;
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: space-around;
11 | align-items: center;
12 | padding: 5px;
13 |
14 | }
15 |
16 | button {
17 | cursor: pointer;
18 | }
19 |
20 | .login-modal form {
21 | display: flex;
22 | flex-direction: column;
23 | justify-content: space-between;
24 | width: 420px;
25 | font-size: 20px;
26 | margin-top: 10px;
27 |
28 | }
29 |
30 |
31 |
32 | .field {
33 | font-size: 20px;
34 | background-color: white;
35 | border: 2px solid #CCCCCC;
36 | padding: 2px;
37 | margin: 5px 0px;
38 | }
39 |
40 | .field:focus {
41 | border: 2px solid #99A7EE;
42 | }
43 |
44 | .login-modal .auth-button {
45 | width: auto;
46 | height: 30px;
47 | border: 2px solid black;
48 | padding: 3px;
49 | }
50 |
51 | .login-modal .auth-button:hover {
52 | transition: .2s;
53 | background-color: #CCCCCC;
54 | }
55 |
56 | .left-item {
57 | width: 93%;
58 |
59 | color: #99A7EE;
60 | border-bottom: 2px solid #99A7EE;
61 | }
62 |
63 | .nav-header{
64 | display: flex;
65 | justify-content: space-between;
66 | background-color: #FFFF64;
67 | height: 40px;
68 | align-items: center;
69 | }
70 |
71 | .nav-header *{
72 | padding: 0 8px;
73 | }
74 |
75 | .auth-section {
76 | display: flex;
77 | justify-content: flex-end;
78 | width: 300px;
79 | height: 100%
80 |
81 |
82 | }
83 |
84 | .error{
85 | display: block;
86 | }
87 |
88 | .logo {
89 |
90 | width: 300px;
91 | text-align: center;
92 | text-shadow: -3px 2px black;
93 | color: #FFFF64;
94 | font-size: 25px;
95 | letter-spacing: 6px;
96 | }
97 |
98 | .search {
99 | width: 300px;
100 | }
101 |
102 | .auth-buttons {
103 | margin-top: 5px;
104 | display: flex;
105 | justify-content: space-between;
106 | margin-bottom: 10px;
107 | }
108 |
109 | .nav-button:hover {
110 | border-bottom: 5px solid rgb(236, 53, 100);
111 | transition: .3s;
112 |
113 | }
114 |
115 | .nav-button {
116 | padding-top: 3px;
117 | border-bottom: 5px solid #FFFF64;
118 | font-size: 14px;
119 | }
120 |
121 | .modal-link {
122 | color: #9A9A9A;
123 | text-decoration: underline;
124 | }
125 |
126 | .header-navigation {
127 | background-color: black;
128 | height: 38px;
129 | color: white;
130 | display: flex;
131 | justify-content: center;
132 | color: white;
133 | }
134 |
135 | .header-navigation a {
136 | color: white;
137 | margin: auto;
138 | }
139 |
140 | .nav-links {
141 | display: flex;
142 | align-items: center;
143 | }
144 |
145 | .nav-links > *:hover {
146 | color: #FFFF64;
147 | transition: .3s;
148 | }
149 |
150 | .header-navigation a + a:before {
151 | content: " | ";
152 | padding-left: 5px;
153 | font-size: 16px;
154 | }
155 |
156 | .nav-icon {
157 | text-align: center;
158 | padding-bottom: 1px;
159 | }
160 |
161 | .nav-icon i {
162 | font-size: 20px;
163 | }
164 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # PostgreSQL. Versions 9.1 and up are supported.
2 | #
3 | # Install the pg driver:
4 | # gem install pg
5 | # On OS X with Homebrew:
6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config
7 | # On OS X with MacPorts:
8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
9 | # On Windows:
10 | # gem install pg
11 | # Choose the win32 build.
12 | # Install PostgreSQL and put its /bin directory on your path.
13 | #
14 | # Configure Using Gemfile
15 | # gem 'pg'
16 | #
17 | default: &default
18 | adapter: postgresql
19 | encoding: unicode
20 | # For details on connection pooling, see rails configuration guide
21 | # http://guides.rubyonrails.org/configuring.html#database-pooling
22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
23 |
24 | development:
25 | <<: *default
26 | database: Annotator_development
27 |
28 | # The specified database role being used to connect to postgres.
29 | # To create additional roles in postgres see `$ createuser --help`.
30 | # When left blank, postgres will use the default role. This is
31 | # the same name as the operating system user that initialized the database.
32 | #username: Annotator
33 |
34 | # The password associated with the postgres role (username).
35 | #password:
36 |
37 | # Connect on a TCP socket. Omitted by default since the client uses a
38 | # domain socket that doesn't need configuration. Windows does not have
39 | # domain sockets, so uncomment these lines.
40 | #host: localhost
41 |
42 | # The TCP port the server listens on. Defaults to 5432.
43 | # If your server runs on a different port number, change accordingly.
44 | #port: 5432
45 |
46 | # Schema search path. The server defaults to $user,public
47 | #schema_search_path: myapp,sharedapp,public
48 |
49 | # Minimum log levels, in increasing order:
50 | # debug5, debug4, debug3, debug2, debug1,
51 | # log, notice, warning, error, fatal, and panic
52 | # Defaults to warning.
53 | #min_messages: notice
54 |
55 | # Warning: The database defined as "test" will be erased and
56 | # re-generated from your development database when you run "rake".
57 | # Do not set this db to the same as development or production.
58 | test:
59 | <<: *default
60 | database: Annotator_test
61 |
62 | # As with config/secrets.yml, you never want to store sensitive information,
63 | # like your database password, in your source code. If your source code is
64 | # ever seen by anyone, they now have access to your database.
65 | #
66 | # Instead, provide the password as a unix environment variable when you boot
67 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
68 | # for a full rundown on how to provide these environment variables in a
69 | # production deployment.
70 | #
71 | # On Heroku and other platform providers, you may have a full connection URL
72 | # available as an environment variable. For example:
73 | #
74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
75 | #
76 | # You can use this database configuration with:
77 | #
78 | # production:
79 | # url: <%= ENV['DATABASE_URL'] %>
80 | #
81 | production:
82 | <<: *default
83 | database: Annotator_production
84 | username: Annotator
85 | password: <%= ENV['ANNOTATOR_DATABASE_PASSWORD'] %>
86 |
--------------------------------------------------------------------------------
/frontend/components/search/search_index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import SearchIndexItem from './search_index_item';
3 | import onClickOutside from 'react-onclickoutside';
4 | import { hashHistory } from 'react-router';
5 |
6 | class SearchIndex extends React.Component {
7 | constructor(props){
8 | super(props);
9 |
10 | this.state = {
11 | query: "",
12 | results: props.results,
13 | dropdown: "hidden"
14 | };
15 |
16 | this.update = this.update.bind(this);
17 | this.closeSearch = this.closeSearch.bind(this);
18 | this.toggleDropdown = this.toggleDropdown.bind(this);
19 | }
20 |
21 | handleClickOutside(){
22 | this.closeSearch();
23 | }
24 |
25 | update(e){
26 | this.setState({query: e.currentTarget.value});
27 | this.props.fetchSearchResults(e.currentTarget.value);
28 | }
29 |
30 |
31 | closeSearch(){
32 | this.setState({query: "", dropdown: "hidden"});
33 | this.props.clearResults();
34 | }
35 |
36 | toggleDropdown(){
37 | this.setState({dropdown: "visible"});
38 | let that = this;
39 | document.addEventListener('keypress', (e) => {
40 | if(e.code === "Enter" && that.props.results.length > 0){
41 | hashHistory.push(`/songs/${that.props.results[0].id}`);
42 | that.setState({query: ""});
43 | that.props.clearResults();
44 | that.closeSearch();
45 | document.getElementById('search-bar').blur();
46 | }
47 | });
48 | }
49 | render(){
50 | let searchResultsArtists = "";
51 | let searchResultsTitles = "";
52 | let searchHeaderArtists = "";
53 | let searchHeaderTitles = "";
54 | let artists = this.props.results.songArtists;
55 | let titles = this.props.results.songTitles;
56 | if(artists.length > 0 || titles.length > 0){
57 | if(artists.length > 0){
58 | searchResultsArtists = artists.map(track => );
64 | searchHeaderArtists = Artists
;
65 | }
66 | if(titles.length > 0){
67 | searchResultsTitles = titles.map(track => );
73 | searchHeaderTitles = Titles
;
74 | }
75 | } else if(this.state.query.length > 0){
76 | searchResultsArtists = No Results :(;
77 | }
78 |
79 | return (
80 |
81 |
this.toggleDropdown()}
83 | className="search-bar"
84 | id="search-bar"
85 | onChange={this.update}
86 | placeholder="Search Songs and Artists"
87 | type="text"
88 | value={this.state.query}/>
89 |
90 | {searchHeaderTitles}
91 | {searchResultsTitles}
92 | {searchHeaderArtists}
93 | {searchResultsArtists}
94 |
95 |
96 |
97 | );
98 | }
99 | }
100 |
101 | export default onClickOutside(SearchIndex);
102 |
--------------------------------------------------------------------------------
/frontend/actions/annotation_actions.js:
--------------------------------------------------------------------------------
1 | import * as AnnotationApiUtil from '../util/annotation_api_util';
2 |
3 | export const RECEIVE_ALL_ANNOTATIONS = "RECEIVE_ALL_ANNOTATIONS";
4 | export const RECEIVE_ANNOTATION = "RECEIVE_ANNOTATION";
5 | export const REMOVE_ANNOTATION = "REMOVE_ANNOTATION";
6 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS";
7 | export const CLEAR_ERRORS = "CLEAR_ERRORS";
8 | export const START_LOADING_ANNOTATION = "START_LOADING_ANNOTATION";
9 | export const FINISH_LOADING_ANNOTATIONS = "FINISH_LOADING_ANNOTATIONS";
10 | export const OPEN_ANNOTATION = "OPEN_ANNOTATION";
11 | export const CLOSE_ANNOTATION = "CLOSE_ANNOTATION";
12 | export const CHANGE_ANNOTATION_TYPE = "CHANGE_ANNOTATION_TYPE";
13 |
14 | export const changeAnnotationType = (annotationType) => {
15 | return {
16 | type: CHANGE_ANNOTATION_TYPE,
17 | annotationType
18 | };
19 | };
20 |
21 | export const openAnnotation = () => {
22 | return {
23 | type: OPEN_ANNOTATION
24 | };
25 | };
26 | export const closeAnnotation = () => {
27 | return {
28 | type: CLOSE_ANNOTATION
29 | };
30 | };
31 |
32 | const receiveAllAnnotations = (annotations) => {
33 | return {
34 | type: RECEIVE_ALL_ANNOTATIONS,
35 | annotations
36 | };
37 | };
38 |
39 | const receiveAnnotation = (annotation) => {
40 | return {
41 | type: RECEIVE_ANNOTATION,
42 | annotation
43 | };
44 | };
45 |
46 | const removeAnnotation = (annotation) => {
47 | return {
48 | type: REMOVE_ANNOTATION,
49 | annotation
50 | };
51 | };
52 |
53 | const receiveErrors = (errors) => {
54 | return {
55 | type: RECEIVE_ERRORS,
56 | errors
57 | };
58 | };
59 |
60 | export const clearErrors = () => {
61 | return {
62 | type: CLEAR_ERRORS
63 | };
64 | };
65 |
66 | export const startLoadingAnnotation = () => {
67 | return {
68 | type: START_LOADING_ANNOTATION
69 | };
70 | };
71 |
72 | export const finishLoadingAnnotations = () => {
73 | return {
74 | type: FINISH_LOADING_ANNOTATIONS
75 | };
76 | };
77 |
78 |
79 | export const fetchAnnotations = (songId) => dispatch => {
80 | return AnnotationApiUtil.fetchAnnotations(songId)
81 | .then(annotations => dispatch(receiveAllAnnotations(annotations)),
82 | errors => dispatch(receiveErrors(errors.responseJSON)));
83 | };
84 | export const fetchAnnotation = (id) => dispatch => {
85 | return AnnotationApiUtil.fetchAnnotation(id)
86 | .then(annotation => dispatch(receiveAnnotation(annotation)),
87 | errors => dispatch(receiveErrors(errors.responseJSON)));
88 | };
89 | export const deleteAnnotation = (id) => dispatch => {
90 | return AnnotationApiUtil.deleteAnnotation(id)
91 | .then(annotation => dispatch(removeAnnotation(annotation)),
92 | errors => dispatch(receiveErrors(errors.responseJSON)));
93 | };
94 | export const updateAnnotation = (annotation, vote) => dispatch => {
95 | return AnnotationApiUtil.updateAnnotation(annotation, vote)
96 | .then(annotation => dispatch(receiveAnnotation(annotation)),
97 | errors => dispatch(receiveErrors(errors.responseJSON)));
98 | };
99 | export const createAnnotation = (annotation) => dispatch => {
100 | return AnnotationApiUtil.createAnnotation(annotation)
101 | .then(annotation => dispatch(receiveAnnotation(annotation)),
102 | errors => dispatch(receiveErrors(errors.responseJSON)));
103 | };
104 |
--------------------------------------------------------------------------------
/frontend/components/annotations/annotation_field.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Editor, EditorState, RichUtils, convertToRaw, convertFromRaw } from 'draft-js';
3 | import 'draft-js/dist/Draft.css';
4 |
5 | class AnnotationField extends React.Component {
6 | constructor(props){
7 | super(props);
8 | this.state = {
9 | errors: ""
10 | };
11 | this.state = {editorState: EditorState.createEmpty()};
12 |
13 |
14 | this.onChange = (editorState) => this.setState({editorState});
15 | this.handleSubmit = this.handleSubmit.bind(this);
16 |
17 | this._onBoldClick = this._onBoldClick.bind(this);
18 | this._onItalicsClick = this._onItalicsClick.bind(this);
19 | this._onUnderlineClick = this._onUnderlineClick.bind(this);
20 |
21 | }
22 |
23 | componentDidMount(){
24 | if(this.props.formType === "edit"){
25 | let raw = convertFromRaw(JSON.parse(this.props.currentAnnotation.description));
26 | this.setState({
27 | editorState: EditorState.createWithContent(raw)
28 | });
29 | }
30 | }
31 |
32 | _onBoldClick() {
33 | this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'));
34 | }
35 | _onItalicsClick() {
36 | this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'ITALIC'));
37 | }
38 | _onUnderlineClick() {
39 | this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'UNDERLINE'));
40 | }
41 |
42 | handleSubmit() {
43 | let text = this.state.editorState.getCurrentContent().getPlainText();
44 | if(text.length === 0){
45 | let error = Annotation Can't Be Blank;
46 | this.setState({errors: error});
47 | } else {
48 | let raw = convertToRaw(this.state.editorState.getCurrentContent());
49 | let string = JSON.stringify(raw);
50 | if(this.props.formType === "new"){
51 | this.props.createAnnotation({
52 | author_id: this.props.user.id,
53 | description: string,
54 | song_id: this.props.track.id,
55 | start_index: this.props.selection[0],
56 | end_index: this.props.selection[1]
57 | }).then(() => this.props.fetchAnnotations(this.props.track.id));
58 | } else {
59 | this.props.updateAnnotation({
60 | id: this.props.currentAnnotation.id,
61 | description: string
62 | });
63 | }
64 | this.props.closeAnnotation();
65 | }
66 |
67 | }
68 | render() {
69 | let header = this.props.formType === "new" ? "Drop Some Knowledge" : "Edit This Annotation";
70 |
71 | return(
72 |
73 |
{header}
74 |
75 | this._onBoldClick()}>
76 | this._onItalicsClick()}>
77 | this._onUnderlineClick()}>
78 |
79 | {this.state.errors}
80 |
81 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | );
92 | }
93 | }
94 |
95 | export default AnnotationField;
96 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20170427153934) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "annotations", force: :cascade do |t|
19 | t.integer "author_id", null: false
20 | t.integer "score", default: 0, null: false
21 | t.text "description", null: false
22 | t.integer "song_id", null: false
23 | t.integer "start_index", null: false
24 | t.integer "end_index", null: false
25 | t.datetime "created_at", null: false
26 | t.datetime "updated_at", null: false
27 | t.index ["start_index", "end_index", "song_id"], name: "index_annotations_on_start_index_and_end_index_and_song_id", unique: true, using: :btree
28 | end
29 |
30 | create_table "comments", force: :cascade do |t|
31 | t.integer "author_id", null: false
32 | t.integer "commentable_id", null: false
33 | t.text "body", null: false
34 | t.datetime "created_at", null: false
35 | t.datetime "updated_at", null: false
36 | t.string "commentable_type"
37 | t.index ["body"], name: "index_comments_on_body", using: :btree
38 | end
39 |
40 | create_table "songs", force: :cascade do |t|
41 | t.string "title", null: false
42 | t.text "lyrics", null: false
43 | t.integer "author_id", null: false
44 | t.string "artist", null: false
45 | t.datetime "created_at", null: false
46 | t.datetime "updated_at", null: false
47 | t.string "image_file_name"
48 | t.string "image_content_type"
49 | t.integer "image_file_size"
50 | t.datetime "image_updated_at"
51 | t.index ["title", "artist"], name: "index_songs_on_title_and_artist", unique: true, using: :btree
52 | end
53 |
54 | create_table "users", force: :cascade do |t|
55 | t.string "username", null: false
56 | t.string "password_digest", null: false
57 | t.string "session_token", null: false
58 | t.datetime "created_at", null: false
59 | t.datetime "updated_at", null: false
60 | t.integer "iq", default: 0
61 | t.index ["username"], name: "index_users_on_username", unique: true, using: :btree
62 | end
63 |
64 | create_table "votes", force: :cascade do |t|
65 | t.integer "user_id", null: false
66 | t.integer "annotation_id", null: false
67 | t.integer "value", default: 1
68 | t.datetime "created_at", null: false
69 | t.datetime "updated_at", null: false
70 | t.index ["user_id", "annotation_id", "value"], name: "index_votes_on_user_id_and_annotation_id_and_value", unique: true, using: :btree
71 | end
72 |
73 | end
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Annotator
2 |
3 | [Annotator Live Demo](http://annotator.me)
4 |
5 | Annotator is a full-stack web application inspired by Genius. It allows
6 | users to add annotations to lyrics and view other users explanations.
7 |
8 | Annotator is built with a Ruby on Rails backend, which utilizes a PostgreSQL
9 | database, and uses React.js with a Redux framework on the frontend.
10 |
11 | ## Features & Implementation
12 |
13 | ### Songs
14 |
15 | Songs are stored in the database with columns for `id`, `lyrics`, `artist`
16 | and `author_id`. Any user may view the song lyrics, but to create or delete a song,
17 | users must be signed-in. Users may only delete songs that they have personally added
18 | to Annotator. Songs are shown on the homepage, with the top three songs having
19 | a greater presence.
20 |
21 | #### Viewing Songs
22 |
23 | 
24 | 
25 |
26 | Users can view song lyrics either by clicking the song on the homepage,
27 | or utilizing the search feature in the upper-left-hand corner of the page.
28 |
29 | 
30 |
31 | #### Adding Songs
32 |
33 | Users can add songs by clicking the "New Song" button at the top of the page,
34 | below the logo. Songs require a title, an artist, and lyrics. Optionally,
35 | users can upload album artwork for song that will be saved to Amazon Web Services.
36 |
37 | ### Annotations
38 |
39 | Annotations are stored in the database with columns for `id`, `song_id`, `author_id`,
40 | `score`, `start_index`, and `end_index`. Any user may view annotations but to create,
41 | edit, or delete annotations the user must be signed in.
42 |
43 |
44 | #### Viewing Annotations
45 |
46 | Lyrics that have associated annotations will show up with a grey highlighting.
47 | Users can click on these lyrics to reveal an annotation on the right hand side of the page.
48 |
49 | 
50 |
51 | Users can vote on annotations using the thumbs-up or thumbs-down icons. Users can also
52 | comment on annotations using the text-field below the annotation. If the current user
53 | created the annotation, there will be options to edit or delete the annotation.
54 |
55 | #### Creating Annotations
56 |
57 | Users can create annotations by selecting text that does not already include
58 | an annotation. This will prompt the user to start creating an annotation if
59 | they are signed-in, otherwise, it will first prompt the user to login.
60 |
61 | 
62 |
63 | #### Comments
64 |
65 | Comments are stored in the database with columns for `id`, `commentable_id`,
66 | `commentable_type`, and `author_id`.
67 |
68 | ### Viewing Comments
69 | Both songs and annotations have comment sections, where users can view the
70 | messages let by others, newest first.
71 |
72 | ### Creating Comments
73 | Both songs and annotations have a text field for users to add their comments.
74 | On submission the comment will automatically be placed at the top of the comment
75 | list
76 |
77 | 
78 |
79 | ## Future Directions for the Project
80 |
81 | In addition to Annotators current features, there are more features in the works, including:
82 |
83 | ### User Profiles
84 |
85 | Users will have profiles pages, where they can upload a profile picture and keep track of
86 | the songs and annotations they have submitted. Users will also have a score based on
87 | the total number of votes for their annotations
88 |
89 | ### Song Sorting
90 |
91 | Genius sorts the songs on their homepage by the number of view over the last 24 hours. The feature
92 | ensures a dynamic home page, and I plan on implementing it.
93 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/api/songs/song_show.scss:
--------------------------------------------------------------------------------
1 | .track-page {
2 | width: 100%;
3 | margin: auto;
4 | background-color: #F7F7F7;
5 | }
6 |
7 |
8 |
9 | .track-page-header {
10 | display: flex;
11 | height: 275px;
12 | justify-content: center;
13 | align-items: center;
14 | padding: 25px;
15 | background-size: cover;
16 | background-position: center;
17 |
18 | }
19 |
20 |
21 | .album-info {
22 | width: 65%;
23 | display: flex;
24 | justify-content: flex-start;
25 | }
26 |
27 | .album-info img {
28 | width: 170px;
29 | height: 170px;
30 | }
31 |
32 | .track-page-info {
33 | display: flex;
34 | flex-direction: column;
35 | height: 140px;
36 | padding: 15px;
37 | }
38 |
39 | .user-controls a{
40 | color: #FFFF64;
41 | margin-right: 5px;
42 | background-color: rgba(0, 0, 0, 0.6);
43 | padding: 6px 8px;
44 | border: 2px solid black;
45 |
46 | }
47 |
48 | #delete {
49 | color: white;
50 | color: #FFFF64;
51 | margin-right: 5px;
52 | background-color: rgba(0, 0, 0, 0.6);
53 | padding: 4px 6px;
54 | border: 2px solid black;
55 | }
56 |
57 | .user-controls a:hover {
58 | color: white;
59 | transition: .4s;
60 | }
61 |
62 | #delete:hover {
63 | color: white;
64 | transition: .4s;
65 | }
66 |
67 | .user-controls{
68 | margin-top: auto;
69 | }
70 |
71 | .track-page-info h1 {
72 | font-size: 38px;
73 | color: #FFFF64;
74 | padding-top: 10px;
75 | padding-bottom: 10px;
76 | }
77 |
78 | .track-page-info span {
79 | font-size: 20px;
80 | color: white;
81 | }
82 |
83 | .track-annotations {
84 | width: 45%;
85 | }
86 |
87 | .track-body {
88 | padding: 15px;
89 |
90 | position: relative;
91 | width: 60%;
92 | display: flex;
93 | justify-content: space-between;
94 | margin: auto;
95 | background-color: white;
96 | }
97 |
98 | .track-lyrics {
99 | width: 60%;
100 | font-size: 22px;
101 | white-space: pre-wrap;
102 | background-color: white;
103 | line-height: 28px;
104 | font-weight: lighter;
105 | }
106 |
107 | .track-lyrics p {
108 | width: 90%;
109 | }
110 |
111 |
112 |
113 |
114 | .annotated {
115 | font-size: 20px;
116 | line-height: 28px;
117 | font-weight: lighter;
118 | background-color: #E9E9E9;
119 | }
120 |
121 | .potential-annotation {
122 | background-color: yellow;
123 | font-size: 20px;
124 | line-height: 28px;
125 | font-weight: lighter;
126 | }
127 |
128 | .normal {
129 | font-size: 20px;
130 | line-height: 28px;
131 | font-weight: lighter;
132 | }
133 |
134 | .annotated:hover {
135 | background-color: yellow;
136 | transition: .4s;
137 | cursor: pointer;
138 | }
139 |
140 | .loader,
141 | .loader:after {
142 | border-radius: 50%;
143 | width: 10em;
144 | height: 10em;
145 | }
146 |
147 | .loader {
148 |
149 | margin: 60px auto;
150 | font-size: 10px;
151 | position: relative;
152 | text-indent: -9999em;
153 | border-top: 1.1em solid rgba(0,0,0, 0.2);
154 | border-right: 1.1em solid rgba(0,0,0, 0.2);
155 | border-bottom: 1.1em solid rgba(0,0,0, 0.2);
156 | border-left: 1.1em solid #000000;
157 | -webkit-transform: translateZ(0);
158 | -ms-transform: translateZ(0);
159 | transform: translateZ(0);
160 | -webkit-animation: load8 1.1s infinite linear;
161 | animation: load8 1.1s infinite linear;
162 | }
163 | @-webkit-keyframes load8 {
164 | 0% {
165 | -webkit-transform: rotate(0deg);
166 | transform: rotate(0deg);
167 | }
168 | 100% {
169 | -webkit-transform: rotate(360deg);
170 | transform: rotate(360deg);
171 | }
172 | }
173 | @keyframes load8 {
174 | 0% {
175 | -webkit-transform: rotate(0deg);
176 | transform: rotate(0deg);
177 | }
178 | 100% {
179 | -webkit-transform: rotate(360deg);
180 | transform: rotate(360deg);
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Disable serving static files from the `/public` folder by default since
18 | # Apache or NGINX already handles this.
19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
20 |
21 | # Compress JavaScripts and CSS.
22 | config.assets.js_compressor = :uglifier
23 | # config.assets.css_compressor = :sass
24 |
25 | # Do not fallback to assets pipeline if a precompiled asset is missed.
26 | config.assets.compile = false
27 |
28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
29 |
30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
31 | # config.action_controller.asset_host = 'http://assets.example.com'
32 |
33 | # Specifies the header that your server uses for sending files.
34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
36 |
37 | # Mount Action Cable outside main process or domain
38 | # config.action_cable.mount_path = nil
39 | # config.action_cable.url = 'wss://example.com/cable'
40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
41 |
42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
43 | # config.force_ssl = true
44 |
45 | # Use the lowest log level to ensure availability of diagnostic information
46 | # when problems arise.
47 | config.log_level = :debug
48 |
49 | # Prepend all log lines with the following tags.
50 | config.log_tags = [ :request_id ]
51 |
52 | # Use a different cache store in production.
53 | # config.cache_store = :mem_cache_store
54 |
55 | # Use a real queuing backend for Active Job (and separate queues per environment)
56 | # config.active_job.queue_adapter = :resque
57 | # config.active_job.queue_name_prefix = "Annotator_#{Rails.env}"
58 | config.action_mailer.perform_caching = false
59 |
60 | # Ignore bad email addresses and do not raise email delivery errors.
61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
62 | # config.action_mailer.raise_delivery_errors = false
63 |
64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
65 | # the I18n.default_locale when a translation cannot be found).
66 | config.i18n.fallbacks = true
67 |
68 | # Send deprecation notices to registered listeners.
69 | config.active_support.deprecation = :notify
70 |
71 | # Use default logging formatter so that PID and timestamp are not suppressed.
72 | config.log_formatter = ::Logger::Formatter.new
73 |
74 | # Use a different logger for distributed setups.
75 | # require 'syslog/logger'
76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
77 |
78 | if ENV["RAILS_LOG_TO_STDOUT"].present?
79 | logger = ActiveSupport::Logger.new(STDOUT)
80 | logger.formatter = config.log_formatter
81 | config.logger = ActiveSupport::TaggedLogging.new(logger)
82 | end
83 |
84 | # Do not dump schema after migrations.
85 | config.active_record.dump_schema_after_migration = false
86 | end
87 |
--------------------------------------------------------------------------------
/frontend/components/header/header_logged_out.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Modal from 'react-modal';
3 | import { hashHistory } from 'react-router';
4 | import update from 'react-addons-update';
5 |
6 | class headerLoggedOut extends React.Component {
7 | constructor(props){
8 | super(props);
9 | this.state = {
10 | user: {username: "", password: ""},
11 | };
12 | this.update = this.update.bind(this);
13 | this.handleSubmit = this.handleSubmit.bind(this);
14 | this.loginDemo = this.loginDemo.bind(this);
15 | }
16 |
17 | update(field) {
18 | return e => this.setState({
19 | user: update(this.state.user, {[field]: {$set: e.currentTarget.value}})
20 | });
21 | }
22 |
23 |
24 |
25 |
26 | handleSubmit(e) {
27 | e.preventDefault();
28 | const user = this.state.user;
29 | if(this.props.loginModalType === "login"){
30 | this.props.login({user})
31 | .then(() => this.props.clearErrors())
32 | .then(() => this.props.closeModal());
33 |
34 | } else {
35 | this.props.signup({user})
36 | .then(() => this.props.closeModal());
37 | }
38 | }
39 |
40 | loginDemo(e) {
41 | e.preventDefault();
42 |
43 | this.props.login({user: {username: "demo_user", password: "password"}})
44 | .then(() => this.props.closeModal());
45 | }
46 |
47 | openModal(type){
48 | return e => {
49 | e.preventDefault();
50 | if(type === "login"){
51 | this.props.openLoginModal();
52 | } else {
53 | this.props.openSignupModal();
54 | }
55 | this.props.clearErrors();
56 | };
57 | }
58 |
59 | componentWillMount() {
60 | Modal.setAppElement('body');
61 | }
62 |
63 |
64 |
65 | render (){
66 | let errors;
67 | if(this.props.errors){
68 | errors = this.props.errors.map(error => {error});
69 | }
70 | let formText;
71 | let oppositeModal;
72 | let buttonText;
73 | let linkText;
74 | if (this.props.loginModalType=== "login"){
75 | formText = "Log in to";
76 | oppositeModal = "signup";
77 | buttonText = "Log in";
78 | linkText = "CREATE AN ACCOUNT";
79 | } else {
80 | formText = "Sign up for";
81 | oppositeModal = "login";
82 | buttonText = "Sign up";
83 | linkText = "LOG IN";
84 | }
85 |
86 | return (
87 |
117 | );
118 | }
119 | }
120 |
121 | export default headerLoggedOut;
122 |
--------------------------------------------------------------------------------
/frontend/components/track_form/track_form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { hashHistory } from 'react-router';
3 |
4 |
5 | class TrackForm extends React.Component {
6 | constructor(props){
7 | super(props);
8 | this.state = {
9 | title: "",
10 | lyrics: "",
11 | artist: "",
12 | author_id: props.currentUser.id,
13 | errors: this.props.errors
14 | };
15 |
16 |
17 | this.update = this.update.bind(this);
18 | this.handleSubmit = this.handleSubmit.bind(this);
19 | this.uploadFile = this.uploadFile.bind(this);
20 |
21 |
22 | }
23 |
24 | componentDidMount(){
25 | if(this.props.formType === "edit"){
26 | this.props.fetchSong(this.props.songId)
27 | .then(() => this.setState(this.props.currentTrack));
28 | }
29 | }
30 |
31 | componentWillReceiveProps(newProps){
32 | if(!newProps.params.songId){
33 | return;
34 | } else if (parseInt(newProps.params.songId) !== this.props.currentTrack.id){
35 | this.props.fetchSong(newProps.params.songId)
36 | .then(() => this.setState(this.props.currentTrack));
37 | }
38 | }
39 |
40 |
41 | update(field){
42 | return e => this.setState({
43 | [field]: e.target.value
44 | });
45 | }
46 |
47 | uploadFile(e){
48 | var reader = new FileReader();
49 | var file = e.currentTarget.files[0];
50 | reader.onloadend = function() {
51 | this.setState({ imageUrl: reader.result, imageFile: file});
52 | }.bind(this);
53 | if (file) {
54 | reader.readAsDataURL(file);
55 | } else {
56 | this.setState({ imageUrl: "", imageFile: null });
57 | }
58 |
59 | }
60 |
61 |
62 | handleSubmit(e){
63 | e.preventDefault();
64 | this.props.clearErrors();
65 | let file = this.state.imageFile;
66 | let formData = new FormData();
67 | formData.append("song[title]", this.state.title);
68 | formData.append("song[lyrics]", this.state.lyrics);
69 | formData.append("song[artist]", this.state.artist);
70 | formData.append("song[author_id]", this.state.author_id);
71 | if(this.state.imageFile){
72 | formData.append("song[image]", file);
73 | }
74 | this.props.action(formData, this.props.currentTrack.id)
75 | .then(() => {
76 | hashHistory.push(`songs/${this.props.currentTrack.id}`);});
77 | }
78 |
79 | render() {
80 | if(this.props.loading){
81 | return(
82 |
83 | );
84 | }
85 | const { title, lyrics, artist } = this.state;
86 | const formText = this.props.formType === "new" ? "Add a New Song" : `Edit ${title}`;
87 | const submitText = this.props.formType === "new" ? "Add Song" : "Submit Edits";
88 |
89 |
90 |
91 | let img = "";
92 | if(this.state.imageFile){
93 | img =
;
94 | } else if (this.props.currentTrack && this.props.formType !== "new"){
95 | img =
;
96 | }
97 | return (
98 |
119 | );
120 | }
121 | }
122 |
123 | export default TrackForm;
124 |
--------------------------------------------------------------------------------
/frontend/components/annotations/annotation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import onClickOutside from 'react-onclickoutside';
3 | import { Editor, EditorState, convertFromRaw } from 'draft-js';
4 | import AnnotationField from './annotation_field';
5 | import VotesContainer from './voting/votes_container';
6 | import CommentContainer from '../comments/comment_container';
7 |
8 | class Annotation extends React.Component {
9 | constructor(props){
10 | super(props);
11 |
12 | this.state = {
13 | type: this.props.annotationType,
14 | currentAnnotation: this.props.currentAnnotation,
15 | position: this.props.position,
16 | formOpen: false,
17 | editorState: EditorState.createEmpty(),
18 | readOnly: true,
19 | };
20 |
21 | this.readOnly = "true";
22 | this.onChange = (editorState) => this.setState({editorState});
23 |
24 | this.newAnnotation = this.newAnnotation.bind(this);
25 | this.showAnnotation = this.showAnnotation.bind(this);
26 |
27 | }
28 |
29 | handleClickOutside() {
30 | if(!this.props.loginOpen){
31 | this.props.closeAnnotation();
32 | }
33 | }
34 |
35 | componentDidMount(){
36 | if(this.props.currentAnnotation.id){
37 | this.props.fetchAnnotationComments(this.props.currentAnnotation.id);
38 | let raw = convertFromRaw(JSON.parse(this.props.currentAnnotation.description));
39 | this.setState({
40 | editorState: EditorState.createWithContent(raw),
41 | });
42 | }
43 | }
44 |
45 | componentWillReceiveProps(newProps){
46 | if(newProps.currentAnnotation.id){
47 | let raw = convertFromRaw(JSON.parse(newProps.currentAnnotation.description));
48 | this.setState({
49 | editorState: EditorState.createWithContent(raw),
50 | type: newProps.annotationType
51 | });
52 | }
53 | }
54 |
55 | newAnnotation() {
56 |
57 | if(this.state.formOpen){
58 | return(
59 |
74 | );
75 | } else {
76 | let startAnnotationButton = ;
80 | if(this.props.currentUser){
81 | startAnnotationButton = ;
85 | }
86 | return (
87 |
88 | {startAnnotationButton}
89 |
90 | );
91 | }
92 | }
93 |
94 | handleDelete(){
95 | this.props.deleteAnnotation(this.props.currentAnnotation.id)
96 | .then(() => this.props.fetchAnnotations(this.props.currentTrack.id))
97 | .then(() => this.props.closeAnnotation());
98 | }
99 |
100 | handleEdit() {
101 | this.props.changeAnnotationType("edit");
102 | }
103 |
104 | showAnnotation() {
105 | let deleteButton = "";
106 | let editButton = "";
107 | let author = "";
108 | if(this.props.currentUser && this.props.currentUser.id === this.props.currentAnnotation.author_id){
109 | deleteButton = ;
110 | editButton = ;
111 | } else {
112 | author = Submitted by: {this.props.currentAnnotation.author};
113 | }
114 | return (
115 |
116 |
120 | {author}
121 |
122 |
123 | {deleteButton}
124 |
125 | {editButton}
126 |
127 |
133 |
134 | );
135 | }
136 |
137 | editAnnotation(){
138 | return (
139 |
140 |
141 |
153 |
154 | );
155 | }
156 |
157 | render(){
158 | if(this.props.annotationType === "new"){
159 | return this.newAnnotation();
160 | } else if (this.props.annotationType === "show"){
161 | return this.showAnnotation();
162 | } else if (this.props.annotationType === "edit"){
163 | return this.editAnnotation();
164 | }
165 | }
166 | }
167 |
168 | export default onClickOutside(Annotation);
169 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (5.0.2)
5 | actionpack (= 5.0.2)
6 | nio4r (>= 1.2, < 3.0)
7 | websocket-driver (~> 0.6.1)
8 | actionmailer (5.0.2)
9 | actionpack (= 5.0.2)
10 | actionview (= 5.0.2)
11 | activejob (= 5.0.2)
12 | mail (~> 2.5, >= 2.5.4)
13 | rails-dom-testing (~> 2.0)
14 | actionpack (5.0.2)
15 | actionview (= 5.0.2)
16 | activesupport (= 5.0.2)
17 | rack (~> 2.0)
18 | rack-test (~> 0.6.3)
19 | rails-dom-testing (~> 2.0)
20 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
21 | actionview (5.0.2)
22 | activesupport (= 5.0.2)
23 | builder (~> 3.1)
24 | erubis (~> 2.7.0)
25 | rails-dom-testing (~> 2.0)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
27 | activejob (5.0.2)
28 | activesupport (= 5.0.2)
29 | globalid (>= 0.3.6)
30 | activemodel (5.0.2)
31 | activesupport (= 5.0.2)
32 | activerecord (5.0.2)
33 | activemodel (= 5.0.2)
34 | activesupport (= 5.0.2)
35 | arel (~> 7.0)
36 | activesupport (5.0.2)
37 | concurrent-ruby (~> 1.0, >= 1.0.2)
38 | i18n (~> 0.7)
39 | minitest (~> 5.1)
40 | tzinfo (~> 1.1)
41 | annotate (2.6.5)
42 | activerecord (>= 2.3.0)
43 | rake (>= 0.8.7)
44 | arel (7.1.4)
45 | aws-sdk (2.9.10)
46 | aws-sdk-resources (= 2.9.10)
47 | aws-sdk-core (2.9.10)
48 | aws-sigv4 (~> 1.0)
49 | jmespath (~> 1.0)
50 | aws-sdk-resources (2.9.10)
51 | aws-sdk-core (= 2.9.10)
52 | aws-sigv4 (1.0.0)
53 | bcrypt (3.1.11)
54 | bindex (0.5.0)
55 | builder (3.2.3)
56 | byebug (9.0.6)
57 | climate_control (0.1.0)
58 | cocaine (0.5.8)
59 | climate_control (>= 0.0.3, < 1.0)
60 | coderay (1.1.1)
61 | coffee-rails (4.2.1)
62 | coffee-script (>= 2.2.0)
63 | railties (>= 4.0.0, < 5.2.x)
64 | coffee-script (2.4.1)
65 | coffee-script-source
66 | execjs
67 | coffee-script-source (1.12.2)
68 | concurrent-ruby (1.0.5)
69 | erubis (2.7.0)
70 | execjs (2.7.0)
71 | ffi (1.9.18)
72 | figaro (1.1.1)
73 | thor (~> 0.14)
74 | globalid (0.3.7)
75 | activesupport (>= 4.1.0)
76 | i18n (0.8.1)
77 | jbuilder (2.6.3)
78 | activesupport (>= 3.0.0, < 5.2)
79 | multi_json (~> 1.2)
80 | jmespath (1.3.1)
81 | jquery-rails (4.3.1)
82 | rails-dom-testing (>= 1, < 3)
83 | railties (>= 4.2.0)
84 | thor (>= 0.14, < 2.0)
85 | listen (3.0.8)
86 | rb-fsevent (~> 0.9, >= 0.9.4)
87 | rb-inotify (~> 0.9, >= 0.9.7)
88 | loofah (2.0.3)
89 | nokogiri (>= 1.5.9)
90 | mail (2.6.4)
91 | mime-types (>= 1.16, < 4)
92 | method_source (0.8.2)
93 | mime-types (3.1)
94 | mime-types-data (~> 3.2015)
95 | mime-types-data (3.2016.0521)
96 | mimemagic (0.3.2)
97 | mini_portile2 (2.1.0)
98 | minitest (5.10.1)
99 | multi_json (1.12.1)
100 | nio4r (2.0.0)
101 | nokogiri (1.7.1)
102 | mini_portile2 (~> 2.1.0)
103 | paperclip (5.0.0)
104 | activemodel (>= 4.2.0)
105 | activesupport (>= 4.2.0)
106 | cocaine (~> 0.5.5)
107 | mime-types
108 | mimemagic (~> 0.3.0)
109 | pg (0.20.0)
110 | pg_search (2.0.1)
111 | activerecord (>= 4.2)
112 | activesupport (>= 4.2)
113 | arel (>= 6)
114 | pry (0.10.4)
115 | coderay (~> 1.1.0)
116 | method_source (~> 0.8.1)
117 | slop (~> 3.4)
118 | pry-rails (0.3.5)
119 | pry (>= 0.9.10)
120 | puma (3.8.2)
121 | rack (2.0.1)
122 | rack-test (0.6.3)
123 | rack (>= 1.0)
124 | rails (5.0.2)
125 | actioncable (= 5.0.2)
126 | actionmailer (= 5.0.2)
127 | actionpack (= 5.0.2)
128 | actionview (= 5.0.2)
129 | activejob (= 5.0.2)
130 | activemodel (= 5.0.2)
131 | activerecord (= 5.0.2)
132 | activesupport (= 5.0.2)
133 | bundler (>= 1.3.0, < 2.0)
134 | railties (= 5.0.2)
135 | sprockets-rails (>= 2.0.0)
136 | rails-dom-testing (2.0.2)
137 | activesupport (>= 4.2.0, < 6.0)
138 | nokogiri (~> 1.6)
139 | rails-html-sanitizer (1.0.3)
140 | loofah (~> 2.0)
141 | railties (5.0.2)
142 | actionpack (= 5.0.2)
143 | activesupport (= 5.0.2)
144 | method_source
145 | rake (>= 0.8.7)
146 | thor (>= 0.18.1, < 2.0)
147 | rake (12.0.0)
148 | rb-fsevent (0.9.8)
149 | rb-inotify (0.9.8)
150 | ffi (>= 0.5.0)
151 | sass (3.4.23)
152 | sass-rails (5.0.6)
153 | railties (>= 4.0.0, < 6)
154 | sass (~> 3.1)
155 | sprockets (>= 2.8, < 4.0)
156 | sprockets-rails (>= 2.0, < 4.0)
157 | tilt (>= 1.1, < 3)
158 | slop (3.6.0)
159 | spring (2.0.1)
160 | activesupport (>= 4.2)
161 | spring-watcher-listen (2.0.1)
162 | listen (>= 2.7, < 4.0)
163 | spring (>= 1.2, < 3.0)
164 | sprockets (3.7.1)
165 | concurrent-ruby (~> 1.0)
166 | rack (> 1, < 3)
167 | sprockets-rails (3.2.0)
168 | actionpack (>= 4.0)
169 | activesupport (>= 4.0)
170 | sprockets (>= 3.0.0)
171 | thor (0.19.4)
172 | thread_safe (0.3.6)
173 | tilt (2.0.7)
174 | tzinfo (1.2.3)
175 | thread_safe (~> 0.1)
176 | uglifier (3.2.0)
177 | execjs (>= 0.3.0, < 3)
178 | web-console (3.5.0)
179 | actionview (>= 5.0)
180 | activemodel (>= 5.0)
181 | bindex (>= 0.4.0)
182 | railties (>= 5.0)
183 | websocket-driver (0.6.5)
184 | websocket-extensions (>= 0.1.0)
185 | websocket-extensions (0.1.2)
186 |
187 | PLATFORMS
188 | ruby
189 |
190 | DEPENDENCIES
191 | annotate
192 | aws-sdk (>= 2.0)
193 | bcrypt (~> 3.1.7)
194 | byebug
195 | coffee-rails (~> 4.2)
196 | figaro
197 | jbuilder (~> 2.5)
198 | jquery-rails
199 | listen (~> 3.0.5)
200 | paperclip (~> 5.0.0)
201 | pg (~> 0.18)
202 | pg_search
203 | pry-rails
204 | puma (~> 3.0)
205 | rails (~> 5.0.2)
206 | sass-rails (~> 5.0)
207 | spring
208 | spring-watcher-listen (~> 2.0.0)
209 | tzinfo-data
210 | uglifier (>= 1.3.0)
211 | web-console (>= 3.3.0)
212 |
213 | BUNDLED WITH
214 | 1.14.6
215 |
--------------------------------------------------------------------------------
/frontend/components/track_show/track_show.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, hashHistory } from 'react-router';
3 | import AnnotationContainer from '../annotations/annotation_container';
4 | import CommentContainer from '../comments/comment_container';
5 | import { findOffset,
6 | orderAnnotations,
7 | isValidAnnotation,
8 | uniqueId
9 | } from '../../util/annotations_util';
10 |
11 |
12 |
13 | class TrackShow extends React.Component {
14 | constructor(props){
15 | super(props);
16 | this.getSelection = this.getSelection.bind(this);
17 | this.state = {
18 | currentAnnotation: this.props.currentAnnotation,
19 | annotations: this.props.annotations,
20 | selection: [],
21 | annotationPosition: ""
22 | };
23 | this.selection = "";
24 |
25 | this.populateAnnotations = this.populateAnnotations.bind(this);
26 | }
27 |
28 | componentDidMount(){
29 | this.props.fetchSong(this.props.trackId)
30 | .then(() => this.props.fetchSongComments(this.props.trackId));
31 | }
32 |
33 | componentWillUnmount(){
34 | this.props.closeAnnotation();
35 | }
36 |
37 | componentWillReceiveProps(newProps){
38 | if(newProps.location !== this.props.location){
39 | this.setState({annotationOpen: false});
40 | }
41 | if(newProps.params.songId){
42 | if(newProps.params.songId !== this.props.params.songId){
43 |
44 | this.props.fetchSong(newProps.params.songId)
45 | .then(() => this.props.fetchSongComments(this.props.params.songId));
46 | }
47 | }
48 | }
49 |
50 | getSelection(e) {
51 | let parent = document.getSelection().anchorNode.parentElement;
52 | let start = document.getSelection().anchorOffset;
53 | let end = start + document.getSelection().toString().length;
54 | let yPos = e.pageY;
55 |
56 | if(end < start){
57 | let temp = end;
58 | start = end;
59 | end = start;
60 | }
61 |
62 | let offset = findOffset(parent);
63 | let range = [start + offset, end + offset];
64 |
65 | if(isValidAnnotation(range, this.props.annotations)){
66 | this.props.openAnnotation();
67 | this.props.changeAnnotationType("new");
68 | this.setState({selection: range, annotationPosition: yPos});
69 | }
70 | }
71 |
72 |
73 | populateAnnotations() {
74 | let lyricsContainer = [];
75 | let offset = 0;
76 | let orderedAnnotations = orderAnnotations(this.props.annotations);
77 |
78 | orderedAnnotations.forEach((annotation) => {
79 | lyricsContainer.push(
80 | {this.props.currentTrack.lyrics.slice(offset, annotation.start_index)}
81 | );
82 | lyricsContainer.push(
86 | {this.props.currentTrack.lyrics.slice(annotation.start_index, annotation.end_index)}
87 | );
88 | offset = annotation.end_index;
89 |
90 | });
91 |
92 | lyricsContainer.push(
93 | {this.props.currentTrack.lyrics.slice(offset, this.props.currentTrack.lyrics.length)}
94 | );
95 |
96 | return lyricsContainer;
97 | }
98 |
99 | openAnnotation(id){
100 | return e => {
101 |
102 | this.props.clearErrors();
103 | let yPos = e.pageY;
104 | return (
105 | this.props.fetchAnnotation(id)
106 | .then(() => this.props.openAnnotation())
107 | .then(() => this.setState({
108 | currentAnnotation: this.props.currentAnnotation,
109 | annotationPosition: yPos
110 | }))
111 | );
112 | };
113 |
114 |
115 | }
116 |
117 | render(){
118 | let top = this.state.annotationType === "new" ? 360 : 400;
119 | if(this.state.annotationPosition - top < 0){
120 | top = 370;
121 | }
122 | let style = {
123 | position: "absolute",
124 | top: this.state.annotationPosition-top,
125 | right: '0px'
126 | };
127 | let annotation = "";
128 | if(this.props.open){
129 | annotation = ;
133 | }
134 | if(this.props.open && this.props.currentUser){
135 | annotation = ;
141 | }
142 | let imgUrl = this.props.currentTrack.image_url;
143 | let deleteButton = "";
144 | let editLink = "";
145 | if(this.props.currentUser){
146 | if(this.props.currentTrack.author_id === this.props.currentUser.id){
147 | deleteButton = ;
151 | }
152 | }
153 | let styles = {
154 | backgroundImage: 'linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(' + imgUrl + ')'
155 | };
156 | if(this.props.loading || !this.props.currentTrack.id){
157 | return(
158 |
159 | );
160 | } else {
161 | return(
162 |
163 |
164 |
165 |
166 |
167 | {this.props.currentTrack.title}
168 | {this.props.currentTrack.artist}
169 |
170 | {editLink}
171 | {deleteButton}
172 |
173 |
174 |
175 |
176 |
177 |
178 | {this.populateAnnotations()}
179 |
180 |
181 |
182 |
187 |
188 | );}
189 | }
190 | }
191 |
192 | export default TrackShow;
193 |
--------------------------------------------------------------------------------