├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── playlisting.rb │ ├── album.rb │ ├── artist.rb │ ├── playlist.rb │ ├── track.rb │ └── user.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── KL.jpg │ │ ├── wiz.jpg │ │ ├── 10day.jpg │ │ ├── TPAB.png │ │ ├── birds.png │ │ ├── damn.png │ │ ├── evol.png │ │ ├── fire.png │ │ ├── moon.jpg │ │ ├── paak.jpg │ │ ├── play.png │ │ ├── FUTURE.png │ │ ├── chance.png │ │ ├── future2.jpg │ │ ├── good_kid.png │ │ ├── kendrick.jpg │ │ ├── malibu.jpg │ │ ├── micfire.png │ │ ├── outkast.jpg │ │ ├── playbar.png │ │ ├── search.png │ │ ├── travis.jpg │ │ ├── kid-cudi-1.jpg │ │ ├── stankonia.png │ │ ├── we_got_it.jpg │ │ ├── handle_change.png │ │ ├── search_input.png │ │ ├── Rolling Papers.png │ │ ├── new_images │ │ │ ├── issa.png │ │ │ ├── rick.jpg │ │ │ ├── calvin.jpg │ │ │ ├── rollin.jpg │ │ │ ├── slide.jpg │ │ │ ├── yogotti.jpg │ │ │ ├── 21savage.jpg │ │ │ ├── rake_it_up.jpg │ │ │ └── traptraptrap.jpg │ │ ├── playlist_default.jpg │ │ ├── tribe_called_quest.jpg │ │ └── speakerbox:the love below.png │ ├── fonts │ │ ├── ProximaNova-Bold.otf │ │ ├── ProximaNova-Black.otf │ │ ├── ProximaNova-BoldIt.otf │ │ ├── ProximaNova-Light.otf │ │ ├── ProximaNova-Regular.otf │ │ ├── ProximaNova-Extrabold.otf │ │ ├── ProximaNova-RegItalic.otf │ │ ├── ProximaNova-Semibold.otf │ │ ├── ProximaNova-LightItalic.otf │ │ ├── ProximaNova-RegularItalic.otf │ │ └── ProximaNova-SemiboldItalic.otf │ ├── stylesheets │ │ ├── api │ │ │ ├── albums.scss │ │ │ ├── search.scss │ │ │ ├── tracks.scss │ │ │ ├── playlistings.scss │ │ │ ├── users.scss │ │ │ ├── add_track.scss │ │ │ ├── home.scss │ │ │ ├── playlists.scss │ │ │ ├── sessions.scss │ │ │ ├── artists.scss │ │ │ ├── modal.scss │ │ │ └── playlist_show.scss │ │ ├── static_pages.scss │ │ ├── application.scss │ │ ├── search.scss │ │ ├── reset.scss │ │ └── playbar.scss │ └── javascripts │ │ ├── users.coffee │ │ ├── api │ │ ├── albums.coffee │ │ ├── search.coffee │ │ ├── tracks.coffee │ │ ├── users.coffee │ │ ├── artists.coffee │ │ ├── playlists.coffee │ │ ├── sessions.coffee │ │ └── playlistings.coffee │ │ ├── search.coffee │ │ ├── static_pages.coffee │ │ └── application.js ├── controllers │ ├── concerns │ │ └── .keep │ ├── static_pages_controller.rb │ ├── api │ │ ├── tracks_controller.rb │ │ ├── albums_controller.rb │ │ ├── artists_controller.rb │ │ ├── sessions_controller.rb │ │ ├── search_controller.rb │ │ ├── playlists_controller.rb │ │ ├── playlistings_controller.rb │ │ └── users_controller.rb │ └── application_controller.rb ├── helpers │ ├── users_helper.rb │ ├── search_helper.rb │ ├── api │ │ ├── users_helper.rb │ │ ├── albums_helper.rb │ │ ├── artists_helper.rb │ │ ├── search_helper.rb │ │ ├── sessions_helper.rb │ │ ├── tracks_helper.rb │ │ ├── playlists_helper.rb │ │ └── playlistings_helper.rb │ ├── application_helper.rb │ └── static_pages_helper.rb └── views │ ├── api │ ├── users │ │ ├── show.json.jbuilder │ │ └── _user.json.jbuilder │ ├── playlists │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ ├── albums │ │ ├── index.json.builder │ │ └── show.json.jbuilder │ ├── tracks │ │ └── index.json.jbuilder │ ├── artists │ │ ├── index.json.jbuilder │ │ └── show.json.jbuilder │ └── search │ │ └── index.json.jbuilder │ ├── layouts │ └── application.html.erb │ └── static_pages │ └── root.html.erb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── test ├── helpers │ └── .keep ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── album_test.rb │ ├── track_test.rb │ ├── user_test.rb │ ├── artist_test.rb │ ├── playlist_test.rb │ └── playlisting_test.rb ├── controllers │ ├── .keep │ ├── search_controller_test.rb │ ├── users_controller_test.rb │ ├── api │ │ ├── albums_controller_test.rb │ │ ├── search_controller_test.rb │ │ ├── tracks_controller_test.rb │ │ ├── users_controller_test.rb │ │ ├── artists_controller_test.rb │ │ ├── playlists_controller_test.rb │ │ ├── sessions_controller_test.rb │ │ └── playlistings_controller_test.rb │ └── static_pages_controller_test.rb ├── fixtures │ ├── .keep │ ├── albums.yml │ ├── artists.yml │ ├── playlists.yml │ ├── tracks.yml │ ├── users.yml │ └── playlistings.yml ├── integration │ └── .keep └── test_helper.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ └── .keep ├── docs ├── wireframes │ ├── index.png │ ├── Search.png │ ├── playlist.png │ ├── About Artist.png │ ├── Searh Result.png │ ├── Artist Overview.png │ ├── Profile Setting.png │ └── Related Artists.png ├── api-endpoints.md ├── sample-state.md ├── component-hierarchy.md ├── README.md └── schema.md ├── config ├── initializers │ ├── paperclip.rb │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── to_time_preserves_timezone.rb │ ├── wrap_parameters.rb │ └── inflections.rb ├── boot.rb ├── environment.rb ├── routes.rb ├── locales │ └── en.yml ├── secrets.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── database.yml ├── bin ├── bundle ├── rake ├── rails ├── spring └── setup ├── frontend ├── util │ ├── track_api_util.js │ ├── search_api_util.js │ ├── album_api_util.js │ ├── artist_api_util.js │ ├── session_api_util.js │ ├── playlist_api_util.js │ └── route_util.jsx ├── reducers │ ├── selectors.js │ ├── albums_reducer.js │ ├── artists_reducer.js │ ├── browse_reducer.js │ ├── search_reducer.js │ ├── root_reducer.js │ ├── tracks_reducer.js │ ├── session_reducer.js │ ├── playlists_reducer.js │ └── audio_reducer.js ├── components │ ├── home │ │ ├── audioplayer.jsx │ │ ├── home_container.js │ │ ├── search │ │ │ ├── album_results.jsx │ │ │ ├── playlist_results.jsx │ │ │ └── search.jsx │ │ ├── playlists │ │ │ ├── playlists_container.js │ │ │ ├── playlists.jsx │ │ │ ├── add_track.jsx │ │ │ └── playlist_show.jsx │ │ ├── modal │ │ │ ├── add_track_modal.jsx │ │ │ └── modal.jsx │ │ ├── browse │ │ │ ├── browse_nav.jsx │ │ │ ├── browse_artists.jsx │ │ │ └── browse_albums.jsx │ │ ├── music_nav.jsx │ │ ├── home.jsx │ │ ├── albums │ │ │ ├── albums.jsx │ │ │ └── album_show.jsx │ │ └── artists │ │ │ ├── artists.jsx │ │ │ └── artist_show.jsx │ ├── root.jsx │ ├── App.jsx │ └── session_form │ │ ├── session_form_container.js │ │ └── session_form.jsx ├── actions │ ├── track_actions.js │ ├── search_actions.jsx │ ├── audio_actions.js │ ├── album_actions.js │ ├── artist_actions.js │ ├── session_actions.js │ └── playlist_actions.js ├── store │ └── store.js └── spitfire.jsx ├── config.ru ├── db ├── migrate │ ├── 20170621140815_fix_tracks.rb │ ├── 20170621160508_fix_track_album_index.rb │ ├── 20170621005922_create_artists.rb │ ├── 20170623182332_add_attachment_image_to_albums.rb │ ├── 20170624002113_add_attachment_audio_to_tracks.rb │ ├── 20170625011021_add_attachment_image_to_artists.rb │ ├── 20170622183405_add_attachment_image_to_playlists.rb │ ├── 20170621161201_create_playlists.rb │ ├── 20170621010608_create_albums.rb │ ├── 20170621004735_create_tracks.rb │ ├── 20170619235629_create_users.rb │ └── 20170622135355_create_playlistings.rb └── schema.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── webpack.config.js ├── package.json ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/search_helper.rb: -------------------------------------------------------------------------------- 1 | module SearchHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/users_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/albums_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::AlbumsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/artists_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::ArtistsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/search_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SearchHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/sessions_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::SessionsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/tracks_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::TracksHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/static_pages_helper.rb: -------------------------------------------------------------------------------- 1 | module StaticPagesHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/playlists_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::PlaylistsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/api/playlistings_helper.rb: -------------------------------------------------------------------------------- 1 | module Api::PlaylistingsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/api/users/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.partial! "api/users/user", user: @user 2 | -------------------------------------------------------------------------------- /app/assets/images/KL.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/KL.jpg -------------------------------------------------------------------------------- /app/assets/images/wiz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/wiz.jpg -------------------------------------------------------------------------------- /docs/wireframes/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/index.png -------------------------------------------------------------------------------- /app/assets/images/10day.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/10day.jpg -------------------------------------------------------------------------------- /app/assets/images/TPAB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/TPAB.png -------------------------------------------------------------------------------- /app/assets/images/birds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/birds.png -------------------------------------------------------------------------------- /app/assets/images/damn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/damn.png -------------------------------------------------------------------------------- /app/assets/images/evol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/evol.png -------------------------------------------------------------------------------- /app/assets/images/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/fire.png -------------------------------------------------------------------------------- /app/assets/images/moon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/moon.jpg -------------------------------------------------------------------------------- /app/assets/images/paak.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/paak.jpg -------------------------------------------------------------------------------- /app/assets/images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/play.png -------------------------------------------------------------------------------- /docs/wireframes/Search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/Search.png -------------------------------------------------------------------------------- /app/assets/images/FUTURE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/FUTURE.png -------------------------------------------------------------------------------- /app/assets/images/chance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/chance.png -------------------------------------------------------------------------------- /app/assets/images/future2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/future2.jpg -------------------------------------------------------------------------------- /app/assets/images/good_kid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/good_kid.png -------------------------------------------------------------------------------- /app/assets/images/kendrick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/kendrick.jpg -------------------------------------------------------------------------------- /app/assets/images/malibu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/malibu.jpg -------------------------------------------------------------------------------- /app/assets/images/micfire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/micfire.png -------------------------------------------------------------------------------- /app/assets/images/outkast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/outkast.jpg -------------------------------------------------------------------------------- /app/assets/images/playbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/playbar.png -------------------------------------------------------------------------------- /app/assets/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/search.png -------------------------------------------------------------------------------- /app/assets/images/travis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/travis.jpg -------------------------------------------------------------------------------- /docs/wireframes/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/playlist.png -------------------------------------------------------------------------------- /app/assets/images/kid-cudi-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/kid-cudi-1.jpg -------------------------------------------------------------------------------- /app/assets/images/stankonia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/stankonia.png -------------------------------------------------------------------------------- /app/assets/images/we_got_it.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/we_got_it.jpg -------------------------------------------------------------------------------- /docs/wireframes/About Artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/About Artist.png -------------------------------------------------------------------------------- /docs/wireframes/Searh Result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/Searh Result.png -------------------------------------------------------------------------------- /app/assets/images/handle_change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/handle_change.png -------------------------------------------------------------------------------- /app/assets/images/search_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/search_input.png -------------------------------------------------------------------------------- /docs/wireframes/Artist Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/Artist Overview.png -------------------------------------------------------------------------------- /docs/wireframes/Profile Setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/Profile Setting.png -------------------------------------------------------------------------------- /docs/wireframes/Related Artists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/docs/wireframes/Related Artists.png -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-Bold.otf -------------------------------------------------------------------------------- /app/assets/images/Rolling Papers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/Rolling Papers.png -------------------------------------------------------------------------------- /app/assets/images/new_images/issa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/issa.png -------------------------------------------------------------------------------- /app/assets/images/new_images/rick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/rick.jpg -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-Black.otf -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-BoldIt.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-BoldIt.otf -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-Light.otf -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-Regular.otf -------------------------------------------------------------------------------- /app/assets/images/new_images/calvin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/calvin.jpg -------------------------------------------------------------------------------- /app/assets/images/new_images/rollin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/rollin.jpg -------------------------------------------------------------------------------- /app/assets/images/new_images/slide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/slide.jpg -------------------------------------------------------------------------------- /app/assets/images/new_images/yogotti.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/yogotti.jpg -------------------------------------------------------------------------------- /app/assets/images/playlist_default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/playlist_default.jpg -------------------------------------------------------------------------------- /app/assets/images/tribe_called_quest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/tribe_called_quest.jpg -------------------------------------------------------------------------------- /config/initializers/paperclip.rb: -------------------------------------------------------------------------------- 1 | Paperclip.options[:content_type_mappings] = { 2 | :mp3 => "application/octet-stream" 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-Extrabold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-Extrabold.otf -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-RegItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-RegItalic.otf -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-Semibold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-Semibold.otf -------------------------------------------------------------------------------- /app/assets/images/new_images/21savage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/21savage.jpg -------------------------------------------------------------------------------- /app/controllers/static_pages_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticPagesController < ApplicationController 2 | def root 3 | 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-LightItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-LightItalic.otf -------------------------------------------------------------------------------- /app/assets/images/new_images/rake_it_up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/rake_it_up.jpg -------------------------------------------------------------------------------- /app/assets/images/new_images/traptraptrap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/new_images/traptraptrap.jpg -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-RegularItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-RegularItalic.otf -------------------------------------------------------------------------------- /app/assets/fonts/ProximaNova-SemiboldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/fonts/ProximaNova-SemiboldItalic.otf -------------------------------------------------------------------------------- /app/assets/images/speakerbox:the love below.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stenkoff/Spitfire/HEAD/app/assets/images/speakerbox:the love below.png -------------------------------------------------------------------------------- /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/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /frontend/util/track_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchTracks = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/tracks', 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/reducers/selectors.js: -------------------------------------------------------------------------------- 1 | const fetchAllTracks = ({ tracks }) => ( 2 | Object.keys(tracks).map(key => tracks[key]) 3 | ); 4 | 5 | export default fetchAllTracks; 6 | -------------------------------------------------------------------------------- /test/models/album_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AlbumTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/track_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TrackTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UserTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /test/models/artist_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ArtistTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/models/playlist_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PlaylistTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /config/initializers/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: '_Spitfire_session' 4 | -------------------------------------------------------------------------------- /frontend/util/search_api_util.js: -------------------------------------------------------------------------------- 1 | export const search = search => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/search', 5 | data: { search } 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /test/models/playlisting_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class PlaylistingTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /db/migrate/20170621140815_fix_tracks.rb: -------------------------------------------------------------------------------- 1 | class FixTracks < ActiveRecord::Migration 2 | def change 3 | remove_index :albums, :artist_id 4 | add_index :albums, :artist_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/search_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class SearchControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/users_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 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/albums_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::AlbumsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/search_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::SearchControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/tracks_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::TracksControllerTest < ActionController::TestCase 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 < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20170621160508_fix_track_album_index.rb: -------------------------------------------------------------------------------- 1 | class FixTrackAlbumIndex < ActiveRecord::Migration 2 | def change 3 | remove_index :tracks, :album_id 4 | add_index :tracks, :album_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/controllers/api/artists_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::ArtistsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/playlists_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::PlaylistsControllerTest < ActionController::TestCase 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 < ActionController::TestCase 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 < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/api/playlistings_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Api::PlaylistingsControllerTest < ActionController::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/albums.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Albums 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/search.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Search 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/tracks.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Tracks controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/views/api/playlists/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @playlists.each do |playlist| 2 | json.set! playlist.id do 3 | json.extract! playlist, :id, :name, :user 4 | json.image_url asset_path(playlist.image.url) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/static_pages.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the StaticPages controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/models/playlisting.rb: -------------------------------------------------------------------------------- 1 | class Playlisting < ActiveRecord::Base 2 | validates :playlist, :track, :ord, presence: true 3 | validates :ord, uniqueness: { scope: :playlist } 4 | 5 | belongs_to :playlist 6 | belongs_to :track 7 | end 8 | -------------------------------------------------------------------------------- /app/views/api/albums/index.json.builder: -------------------------------------------------------------------------------- 1 | json.albums do 2 | json.array!(@albums) do |album| 3 | json.extract! album, :id, :name 4 | json.artist album.artist.name 5 | json.image_url asset_path(album.image.url) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/playlistings.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Playlistings controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /db/migrate/20170621005922_create_artists.rb: -------------------------------------------------------------------------------- 1 | class CreateArtists < ActiveRecord::Migration 2 | def change 3 | create_table :artists do |t| 4 | t.string :name, null: false 5 | t.timestamps null: false 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/assets/javascripts/users.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/home/audioplayer.jsx: -------------------------------------------------------------------------------- 1 | // class AudioPlayer extends React.Component { 2 | // constructor() { 3 | // 4 | // } 5 | // 6 | // 7 | // 9 | // } 10 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/albums.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/search.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/tracks.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/search.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/models/album.rb: -------------------------------------------------------------------------------- 1 | class Album < ActiveRecord::Base 2 | belongs_to :artist 3 | has_many :tracks 4 | 5 | has_attached_file :image, default_url: 'playlist_default.jpg' 6 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/api/artists.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/playlists.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/assets/javascripts/api/playlistings.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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/controllers/api/tracks_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::TracksController < ApplicationController 2 | def index 3 | @tracks = Track.all 4 | render :index 5 | end 6 | 7 | # def track_params 8 | # params.require(:track).permit(:title, :album_id, :ord) 9 | # end 10 | end 11 | -------------------------------------------------------------------------------- /app/models/artist.rb: -------------------------------------------------------------------------------- 1 | class Artist < ActiveRecord::Base 2 | validates :name, presence: true 3 | has_many :albums 4 | has_many :tracks, through: :albums 5 | 6 | has_attached_file :image, default_url: 'chance.png' 7 | validates_attachment_content_type :image, content_type: /\Aimage\/.*\Z/ 8 | end 9 | -------------------------------------------------------------------------------- /frontend/util/album_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchAlbum = id => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: `/api/albums/${id}` 5 | }); 6 | }; 7 | 8 | export const fetchAlbums = () => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: '/api/albums' 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../../config/application', __FILE__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /db/migrate/20170623182332_add_attachment_image_to_albums.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToAlbums < ActiveRecord::Migration 2 | def self.up 3 | change_table :albums do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :albums, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170624002113_add_attachment_audio_to_tracks.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentAudioToTracks < ActiveRecord::Migration 2 | def self.up 3 | change_table :tracks do |t| 4 | t.attachment :audio 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :tracks, :audio 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170625011021_add_attachment_image_to_artists.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToArtists < ActiveRecord::Migration 2 | def self.up 3 | change_table :artists do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :artists, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170622183405_add_attachment_image_to_playlists.rb: -------------------------------------------------------------------------------- 1 | class AddAttachmentImageToPlaylists < ActiveRecord::Migration 2 | def self.up 3 | change_table :playlists do |t| 4 | t.attachment :image 5 | end 6 | end 7 | 8 | def self.down 9 | remove_attachment :playlists, :image 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20170621161201_create_playlists.rb: -------------------------------------------------------------------------------- 1 | class CreatePlaylists < ActiveRecord::Migration 2 | def change 3 | create_table :playlists do |t| 4 | t.string :name, null: false 5 | t.integer :user_id, null: false 6 | t.timestamps null: false 7 | end 8 | add_index :playlists, :user_id 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/api/tracks/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | @tracks.each do |track| 2 | json.set! track.id do 3 | json.extract! track, :id, :title, :album_id, :ord 4 | json.audio asset_path(track.audio.url) 5 | json.artist track.artist.name 6 | json.artist_id track.artist.id 7 | json.image_url asset_path(track.album.image.url) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20170621010608_create_albums.rb: -------------------------------------------------------------------------------- 1 | class CreateAlbums < ActiveRecord::Migration 2 | def change 3 | create_table :albums do |t| 4 | t.string :name, null: false 5 | t.integer :artist_id, null: false 6 | 7 | t.timestamps null: false 8 | end 9 | add_index :albums, :artist_id, unique: true; 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/albums_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::AlbumsController < ApplicationController 2 | def index 3 | @albums = Album.all 4 | render :index 5 | end 6 | 7 | def show 8 | @album = Album.find(params[:id]) 9 | render :show 10 | end 11 | 12 | def album_params 13 | params.require(:album).permit(:name, :artist_id) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/root.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | const Root = ({ store }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default Root; 15 | -------------------------------------------------------------------------------- /frontend/util/artist_api_util.js: -------------------------------------------------------------------------------- 1 | export const fetchArtists = () => { 2 | return $.ajax({ 3 | method: 'GET', 4 | url: '/api/artists' 5 | }); 6 | }; 7 | 8 | export const fetchArtist = id => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: `/api/artists/${id}` 12 | }); 13 | }; 14 | 15 | // export const fetchArtistsForUser = () { 16 | // 17 | // } 18 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spitfire 5 | <%= stylesheet_link_tag 'application', media: 'all' %> 6 | <%= javascript_include_tag 'application' %> 7 | <%= favicon_link_tag 'micfire.png' %> 8 | <%= csrf_meta_tags %> 9 | 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /db/migrate/20170621004735_create_tracks.rb: -------------------------------------------------------------------------------- 1 | class CreateTracks < ActiveRecord::Migration 2 | def change 3 | create_table :tracks do |t| 4 | t.string :title, null: false 5 | t.integer :album_id, null: false 6 | t.integer :ord, null: false 7 | 8 | t.timestamps null: false 9 | end 10 | add_index :tracks, :album_id, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20170619235629_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 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 null: false 9 | end 10 | add_index :users, :username, unique: true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /frontend/actions/track_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/track_api_util'; 2 | 3 | export const RECEIVE_TRACKS = 'RECEIVE_TRACKS'; 4 | 5 | export const receiveTracks = tracks => ({ 6 | type: RECEIVE_TRACKS, 7 | tracks 8 | }); 9 | 10 | export const fetchTracks = tracks => dispatch => { 11 | return APIUtil.fetchTracks() 12 | .then(tracks => dispatch(receiveTracks(tracks))); 13 | }; 14 | -------------------------------------------------------------------------------- /app/views/api/artists/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.artists do 2 | json.array!(@artists) do |artist| 3 | json.extract! artist, :id, :name 4 | json.image_url asset_path(artist.image.url) 5 | end 6 | end 7 | 8 | json.albums do 9 | json.array!(@albums) do |album| 10 | json.extract! album, :id, :name 11 | json.artist album.artist.name 12 | json.image_url asset_path(album.image.url) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/albums.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/artists.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/playlists.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/tracks.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /app/views/static_pages/root.html.erb: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /test/fixtures/playlistings.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /frontend/actions/search_actions.jsx: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/search_api_util'; 2 | 3 | export const RECEIVE_SEARCH = 'RECEIVE_SEARCH'; 4 | 5 | export const receiveSearch = results => { 6 | return { 7 | type: RECEIVE_SEARCH, 8 | results 9 | } 10 | } 11 | 12 | export const fetchSearch = search => dispatch => { 13 | return APIUtil.search(search) 14 | .then(results => dispatch(receiveSearch(results))); 15 | }; 16 | -------------------------------------------------------------------------------- /db/migrate/20170622135355_create_playlistings.rb: -------------------------------------------------------------------------------- 1 | class CreatePlaylistings < ActiveRecord::Migration 2 | def change 3 | create_table :playlistings do |t| 4 | t.integer :track_id, null: false 5 | t.integer :playlist_id, null: false 6 | t.integer :ord, null: false 7 | t.timestamps null: false 8 | end 9 | add_index :playlistings, :track_id 10 | add_index :playlistings, :playlist_id 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/api/artists_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ArtistsController < ApplicationController 2 | def index 3 | @artists = Artist.all.includes(:albums) 4 | @albums = Album.all.includes(:artist) 5 | render :index 6 | end 7 | 8 | def show 9 | @artist = Artist.includes(:albums, :tracks).find(params[:id]) 10 | render :show 11 | end 12 | 13 | def artist_params 14 | params.require(:artist).permit(:name) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/components/home/home_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { logout } from '../../actions/session_actions'; 3 | import Home from './home'; 4 | 5 | const mapStateToProps = state => ({ 6 | currentUser: state.session.currentUser 7 | }); 8 | 9 | const mapDispatchToProps = dispatch => ({ 10 | logout: () => dispatch(logout()) 11 | }); 12 | 13 | export default connect( 14 | mapStateToProps, 15 | mapDispatchToProps 16 | )(Home); 17 | -------------------------------------------------------------------------------- /app/models/playlist.rb: -------------------------------------------------------------------------------- 1 | class Playlist < ActiveRecord::Base 2 | validates :user, :name, presence: true; 3 | 4 | belongs_to :user 5 | has_many :playlistings 6 | has_many :tracks, -> {order('playlistings.ord ASC')}, through: :playlistings 7 | 8 | has_attached_file :image, default_url: 'playlist_default.jpg' 9 | validates_attachment_content_type :image, content_type: /\Aimage\/.*Z/ 10 | end 11 | 12 | 13 | # styles: { medium: “300X300>”, thumb: “100X100>” }, 14 | -------------------------------------------------------------------------------- /app/models/track.rb: -------------------------------------------------------------------------------- 1 | class Track < ActiveRecord::Base 2 | validates :title, :album_id, :ord, presence: true; 3 | validates :ord, uniqueness: { scope: :album_id} 4 | 5 | belongs_to :album 6 | has_many :playlistings 7 | has_many :playlists, through: :playlistings 8 | has_one :artist, through: :album 9 | 10 | has_attached_file :audio 11 | validates_attachment_content_type :audio, content_type: [/\Aaudio\/.*\Z/, "audio/mpeg", "audio/mp3", "audio/x-mpeg"] 12 | end 13 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | namespace :api, defaults: {format: :json} do 3 | resources :users, only: [:create, :show] 4 | resources :playlists 5 | resources :albums, only: [:index, :show] 6 | resources :artists, only: [:index, :show] 7 | resource :session, only: [:create, :destroy] 8 | resources :playlistings, only: [:create] 9 | resources :search, only: [:index] 10 | delete '/playlistings/kill', to: 'playlistings#kill' 11 | end 12 | 13 | root "static_pages#root" 14 | end 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | import { receiveCurrentUser, receiveErrors } from '../actions/session_actions'; 2 | 3 | export const signup = user => { 4 | return $.ajax({ 5 | method: 'POST', 6 | url: '/api/users', 7 | data: { user } 8 | }); 9 | }; 10 | 11 | export const login = user => { 12 | return $.ajax({ 13 | method: 'POST', 14 | url: '/api/session', 15 | data: { user } 16 | }); 17 | }; 18 | 19 | export const logout = () => { 20 | return $.ajax({ 21 | method: 'delete', 22 | url: '/api/session' 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/to_time_preserves_timezone.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Preserve the timezone of the receiver when calling to `to_time`. 4 | # Ruby 2.4 will change the behavior of `to_time` to preserve the timezone 5 | # when converting to an instance of `Time` instead of the previous behavior 6 | # of converting to the local system timezone. 7 | # 8 | # Rails 5.0 introduced this config option so that apps made with earlier 9 | # versions of Rails are not affected when upgrading. 10 | ActiveSupport.to_time_preserves_timezone = true 11 | -------------------------------------------------------------------------------- /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 | - `PATCH /api/users/:id` 15 | 16 | ### Session 17 | 18 | - `POST /api/session` 19 | - `DELETE /api/session` 20 | 21 | ### Playlists 22 | 23 | - `GET /api/playlists` 24 | - `POST /api/playlists` 25 | - `GET /api/playlists/:id` 26 | - `PATCH /api/playlists/:id` 27 | - `DELETE /api/playlists/:id` 28 | 29 | ### Albums 30 | - `GET /api/albums/:id` 31 | 32 | ### Artists 33 | - `GET /api/artists/:id` 34 | -------------------------------------------------------------------------------- /frontend/reducers/albums_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 2 | import { RECEIVE_ALBUM } from '../actions/album_actions'; 3 | import merge from 'lodash/merge'; 4 | 5 | const AlbumsReducer = (state = {}, action) => { 6 | Object.freeze(state) 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER: 9 | return merge({}, state, action.albums); 10 | case RECEIVE_ALBUM: 11 | return merge({}, state, { [action.album.id]: action.album} ) 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default AlbumsReducer; 18 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/controllers/api/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SessionsController < ApplicationController 2 | 3 | def create 4 | @user = User.find_by_credentials( 5 | params[:user][:username], 6 | params[:user][:password] 7 | ) 8 | if @user 9 | login(@user) 10 | render "api/users/show" 11 | else 12 | render json: ["Invalid username/password"], status: 401 13 | end 14 | end 15 | 16 | def destroy 17 | @user = current_user 18 | if @user 19 | logout 20 | render json: {} 21 | else 22 | render json: ["No user signed in"], status: 404 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /app/views/api/albums/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.album do 2 | json.extract! @album, :id, :name, :artist_id 3 | json.artist @album.artist.name 4 | json.track_ids @album.tracks.pluck(:id) 5 | json.image_url asset_path(@album.image.url) 6 | end 7 | 8 | json.tracks do 9 | @album.tracks.each do |track| 10 | json.set! track.id do 11 | json.extract! track, :title, :id, :album_id 12 | json.audio asset_path(track.audio.url) 13 | json.artist track.artist.name 14 | json.artist_id track.artist.id 15 | json.image_url asset_path(track.album.image.url) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /frontend/reducers/artists_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 2 | import { RECEIVE_ARTIST, RECEIVE_ARTISTS } from '../actions/artist_actions'; 3 | import merge from 'lodash/merge'; 4 | 5 | const ArtistsReducer = (state = {}, action) => { 6 | Object.freeze(state) 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER: 9 | return merge({}, state, action.artists); 10 | case RECEIVE_ARTIST: 11 | return merge({}, state, { [action.artist.id]: action.artist} ) 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default ArtistsReducer; 18 | -------------------------------------------------------------------------------- /app/views/api/playlists/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.playlist do 2 | json.extract! @playlist, :id, :name 3 | json.track_ids @playlist.tracks.pluck(:id) 4 | json.image_url asset_path(@playlist.image.url) 5 | json.creator @playlist.user.username 6 | end 7 | 8 | json.tracks do 9 | @playlist.tracks.each_with_index do |track, i| 10 | json.set! track.id do 11 | json.extract! track, :title, :id, :album_id 12 | json.artist track.artist.name 13 | json.artist_id track.artist.id 14 | json.audio asset_path(track.audio.url) 15 | json.image_url asset_path(track.album.image.url) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /frontend/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import RootReducer from '../reducers/root_reducer'; 3 | import thunk from 'redux-thunk'; 4 | 5 | const middlewares = [thunk]; 6 | 7 | if (process.env.NODE_ENV !== 'production') { 8 | // must use 'require' (import only allowed at top of file) 9 | const { createLogger } = require('redux-logger'); 10 | middlewares.push(createLogger()); 11 | } 12 | 13 | const configureStore = (preloadedState = {}) => ( 14 | createStore( 15 | RootReducer, 16 | preloadedState, 17 | applyMiddleware(...middlewares) 18 | ) 19 | ) 20 | 21 | export default configureStore; 22 | -------------------------------------------------------------------------------- /.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 | !/log/.keep 13 | /tmp 14 | node_modules/ 15 | bundle.js 16 | bundle.js.map 17 | .byebug_history 18 | .DS_Store 19 | npm-debug.log 20 | app/assets/tracks/ 21 | 22 | # Ignore application configuration 23 | /config/application.yml 24 | -------------------------------------------------------------------------------- /docs/sample-state.md: -------------------------------------------------------------------------------- 1 | ```javascript 2 | { 3 | session: { 4 | currentUser: { 5 | id: 1, 6 | username: 'username', 7 | following: [1,2] 8 | }, 9 | }, 10 | 11 | tracks { 12 | 1: { 13 | id: 1, 14 | title: 'title', 15 | album_id: 1 16 | } 17 | }, 18 | 19 | playlists: { 20 | 1: { 21 | id: 1, 22 | name: 'name', 23 | tracks: [1,2] 24 | } 25 | }, 26 | 27 | artists: { 28 | 1: { 29 | id: 1, 30 | name: 'name', 31 | tracks: [1,2,3] 32 | } 33 | } 34 | 35 | albums: { 36 | 1: { 37 | id: 1, 38 | title: 'title', 39 | tracks: [1,2,3] 40 | } 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /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/api/search_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SearchController < ApplicationController 2 | def index 3 | if params[:search].present? 4 | @playlists = Playlist.where('lower(name) LIKE (?)', "%#{params[:search].downcase}%") 5 | @albums = Album.where('lower(name) LIKE (?)', "%#{params[:search].downcase}%").includes(:artist) 6 | @artists = Artist.where('lower(name) LIKE (?)', "%#{params[:search].downcase}%").includes(:albums) 7 | @tracks = Track.where('lower(title) LIKE (?)', "%#{params[:search].downcase}%").includes(:album, :artist) 8 | else 9 | @playlists = Playlist.none 10 | @albums = Album.none 11 | @artists = Artist.none 12 | @tracks = Track.none 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /frontend/actions/audio_actions.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_CURRENT_SONG = 'RECEIVE_CURRENT_SONG'; 2 | export const RECEIVE_SONGS = 'RECEIVE_SONGS'; 3 | export const SKIP = 'SKIP'; 4 | 5 | export const receiveSong = track => { 6 | return { 7 | type: RECEIVE_CURRENT_SONG, 8 | track 9 | } 10 | } 11 | export const receiveSongs = tracks => { 12 | return { 13 | type: RECEIVE_SONGS, 14 | tracks 15 | } 16 | } 17 | 18 | export const skip = () => { 19 | return { 20 | type: SKIP 21 | } 22 | } 23 | 24 | export const playTrack = (track) => { 25 | return track => dispatch(receiveSong(track)); 26 | } 27 | 28 | 29 | // export const playTracks = (tracks) => { 30 | // return tracks => dispatch(receiveSongs(track)); 31 | // } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /frontend/reducers/browse_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_ARTISTS } from '../actions/artist_actions'; 2 | import { RECEIVE_ALBUMS } from '../actions/album_actions'; 3 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 4 | 5 | 6 | const BrowseReducer = (state = {}, action) => { 7 | Object.freeze(state) 8 | switch (action.type) { 9 | case RECEIVE_ARTISTS: 10 | let artists = action.artists.slice(); 11 | let albums = action.albums.slice(); 12 | return {artists, albums}; 13 | return action.artists; 14 | case RECEIVE_ALBUMS: 15 | return action.albums; 16 | case RECEIVE_CURRENT_USER: 17 | return {}; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default BrowseReducer; 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '4.2.8' 4 | gem 'pg', '~> 0.15' 5 | gem 'sass-rails', '~> 5.0' 6 | gem 'uglifier', '>= 1.3.0' 7 | gem 'coffee-rails', '~> 4.1.0' 8 | gem 'jquery-rails' 9 | gem 'jbuilder', '~> 2.0' 10 | gem 'sdoc', '~> 0.4.0', group: :doc 11 | gem 'bcrypt' 12 | gem 'paperclip' 13 | gem 'faker', '~> 1.6', '>= 1.6.6' 14 | gem 'figaro' 15 | gem 'aws-sdk' 16 | gem 'font-awesome-sass' 17 | 18 | group :development, :test do 19 | gem 'byebug' 20 | end 21 | 22 | group :development do 23 | gem 'web-console', '~> 2.0' 24 | gem 'spring' 25 | gem 'better_errors' 26 | gem 'binding_of_caller' 27 | gem 'pry-rails' 28 | gem 'quiet_assets' 29 | end 30 | 31 | group :production do 32 | gem 'rails_12factor' 33 | end 34 | -------------------------------------------------------------------------------- /frontend/actions/album_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/album_api_util'; 2 | export const RECEIVE_ALBUM = 'RECEIVE_ALBUM'; 3 | export const RECEIVE_ALBUMS = 'RECEIVE_ALBUMS'; 4 | 5 | export const receiveAlbum = ({album, tracks}) => { 6 | return { 7 | type: RECEIVE_ALBUM, 8 | album, 9 | tracks 10 | } 11 | }; 12 | 13 | export const receiveAlbums = ({albums}) => { 14 | return { 15 | type: RECEIVE_ALBUMS, 16 | albums 17 | } 18 | }; 19 | 20 | export const fetchAlbum = id => dispatch => { 21 | return APIUtil.fetchAlbum(id) 22 | .then(album => dispatch(receiveAlbum(album))); 23 | }; 24 | 25 | export const fetchAlbums = () => dispatch => { 26 | return APIUtil.fetchAlbums() 27 | .then(albums => dispatch(receiveAlbums(albums))); 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/reducers/search_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_SEARCH } from '../actions/search_actions'; 2 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 3 | import merge from 'lodash/merge'; 4 | 5 | const SearchReducer = (state = {}, action) => { 6 | Object.freeze(state) 7 | switch (action.type) { 8 | case RECEIVE_SEARCH: 9 | let playlists = action.results.playlists.slice(); 10 | let albums = action.results.albums.slice(); 11 | let artists = action.results.artists.slice(); 12 | let tracks = action.results.tracks.slice(); 13 | return {playlists, albums, artists, tracks}; 14 | case RECEIVE_CURRENT_USER: 15 | return {}; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default SearchReducer; 22 | -------------------------------------------------------------------------------- /app/controllers/api/playlists_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::PlaylistsController < ApplicationController 2 | def index 3 | @playlists = Playlist.all 4 | render :index 5 | end 6 | 7 | def show 8 | @playlist = Playlist.includes(:playlistings).find(params[:id]) 9 | render :show 10 | end 11 | 12 | def create 13 | @playlist = Playlist.new(playlist_params) 14 | if @playlist.save 15 | render :show 16 | else 17 | render json: @playlist.errors.full_messages, status: 422 18 | end 19 | end 20 | 21 | def destroy 22 | @playlist = Playlist.find(params[:id]) 23 | @playlist.delete 24 | @playlists = Playlist.all 25 | render :index 26 | end 27 | 28 | def playlist_params 29 | params.require(:playlist).permit(:user_id, :name) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /frontend/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import SessionReducer from './session_reducer'; 3 | import TracksReducer from './tracks_reducer'; 4 | import PlaylistsReducer from './playlists_reducer'; 5 | import AlbumsReducer from './albums_reducer'; 6 | import ArtistsReducer from './artists_reducer'; 7 | import AudioReducer from './audio_reducer'; 8 | import SearchReducer from './search_reducer'; 9 | import BrowseReducer from './browse_reducer'; 10 | 11 | const RootReducer = combineReducers({ 12 | session: SessionReducer, 13 | playlists: PlaylistsReducer, 14 | tracks: TracksReducer, 15 | albums: AlbumsReducer, 16 | artists: ArtistsReducer, 17 | queue: AudioReducer, 18 | search: SearchReducer, 19 | browse: BrowseReducer, 20 | }); 21 | 22 | export default RootReducer; 23 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | helper_method :current_user, :logged_in? 6 | 7 | private 8 | 9 | def current_user 10 | return nil unless session[:session_token] 11 | @current_user ||= User.find_by(session_token: session[:session_token]) 12 | end 13 | 14 | def logged_in? 15 | !!current_user 16 | end 17 | 18 | def login(user) 19 | session[:session_token] = user.reset_session_token! 20 | @current_user = user 21 | end 22 | 23 | def logout 24 | current_user.reset_session_token! 25 | session[:session_token] = nil 26 | @current_user = nil 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /frontend/actions/artist_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/artist_api_util'; 2 | export const RECEIVE_ARTIST = 'RECEIVE_ARTIST'; 3 | export const RECEIVE_ARTISTS = 'RECEIVE_ARTISTS'; 4 | 5 | export const receiveArtist = ({artist, tracks}) => { 6 | return { 7 | type: RECEIVE_ARTIST, 8 | artist, 9 | tracks 10 | } 11 | }; 12 | 13 | export const receiveArtists = ({artists, albums}) => { 14 | return { 15 | type: RECEIVE_ARTISTS, 16 | artists, 17 | albums 18 | } 19 | }; 20 | 21 | export const fetchArtist = id => dispatch => { 22 | return APIUtil.fetchArtist(id) 23 | .then(artist => dispatch(receiveArtist(artist))); 24 | }; 25 | 26 | export const fetchArtists = () => dispatch => { 27 | return APIUtil.fetchArtists() 28 | .then(artists => dispatch(receiveArtists(artists))); 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/reducers/tracks_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_PLAYLIST, REMOVE_PLAYLIST } from '../actions/playlist_actions'; 2 | import merge from 'lodash/merge'; 3 | import { RECEIVE_ALBUM} from '../actions/album_actions'; 4 | import { RECEIVE_ARTIST} from '../actions/artist_actions'; 5 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 6 | 7 | const TracksReducer = (state = {}, action) => { 8 | Object.freeze(state) 9 | switch (action.type) { 10 | case RECEIVE_PLAYLIST: 11 | return merge({}, action.tracks) 12 | case RECEIVE_ALBUM: 13 | return merge({}, action.tracks) 14 | case RECEIVE_ARTIST: 15 | return merge({}, action.tracks) 16 | case RECEIVE_CURRENT_USER: 17 | return {}; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default TracksReducer; 24 | -------------------------------------------------------------------------------- /frontend/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HomeContainer from './home/home_container'; 3 | import PlaylistsContainer from './home/playlists/playlists_container'; 4 | import SessionFormContainer from './session_form/session_form_container'; 5 | import PlaylistShow from './home/playlists/playlist_show'; 6 | import Albums from './home/albums/albums'; 7 | import { Switch, Route } from 'react-router-dom'; 8 | import { AuthRoute, ProtectedRoute } from '../util/route_util'; 9 | 10 | const App = () => { 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 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 styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | 17 | @import "font-awesome-sprockets"; 18 | @import "font-awesome"; 19 | -------------------------------------------------------------------------------- /frontend/components/home/search/album_results.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | class AlbumResults extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return( 11 |
12 |

Playlists

13 | { 14 | playlists.map(playlist => 15 |
16 | 17 |

{playlist.name}

18 |

{playlist.creator}

19 |
) 20 | } 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default AlbumResults; 27 | -------------------------------------------------------------------------------- /frontend/components/home/search/playlist_results.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | class PlaylistResults extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return( 11 |
12 |

Playlists

13 | { 14 | playlists.map(playlist => 15 |
16 | 17 |

{playlist.name}

18 |

{playlist.creator}

19 |
) 20 | } 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default PlaylistResults; 27 | -------------------------------------------------------------------------------- /frontend/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CURRENT_USER, RECEIVE_ERRORS, CLEAR_ERRORS, LOGOUT_CURRENT_USER } from '../actions/session_actions'; 2 | import merge from 'lodash/merge'; 3 | 4 | const defaultState = Object.freeze({ 5 | currentUser: null, 6 | errors: [] 7 | }); 8 | 9 | const SessionReducer = (state = defaultState, action) => { 10 | Object.freeze(state) 11 | switch (action.type) { 12 | case RECEIVE_CURRENT_USER: 13 | return merge({}, state, {currentUser: action.currentUser, errors: []}); 14 | case LOGOUT_CURRENT_USER: 15 | return merge({}, state, {currentUser: action.currentUser, errors: []}); 16 | case RECEIVE_ERRORS: 17 | return merge({}, state, {errors: action.errors}); 18 | case CLEAR_ERRORS: 19 | return defaultState; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | export default SessionReducer; 26 | -------------------------------------------------------------------------------- /frontend/reducers/playlists_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_PLAYLISTS, RECEIVE_PLAYLIST, REMOVE_PLAYLIST } from '../actions/playlist_actions'; 2 | import { RECEIVE_CURRENT_USER, LOGOUT_CURRENT_USER } from '../actions/session_actions'; 3 | import merge from 'lodash/merge'; 4 | 5 | const PlaylistsReducer = (state = {}, action) => { 6 | Object.freeze(state) 7 | switch (action.type) { 8 | case RECEIVE_CURRENT_USER: 9 | return merge({}, state, action.playlists); 10 | case LOGOUT_CURRENT_USER: 11 | return merge({}, action.playlists); 12 | case RECEIVE_PLAYLISTS: 13 | return action.user.playlists 14 | case RECEIVE_PLAYLIST: 15 | return merge({}, state, { [action.playlist.id]: action.playlist} ) 16 | case REMOVE_PLAYLIST: 17 | return merge({}, action.playlists) 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default PlaylistsReducer; 24 | -------------------------------------------------------------------------------- /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 | 5 | 6 | // .username { 7 | // border: 1px solid red; 8 | // // position: absolute; 9 | // 10 | // // width: 220px; 11 | // // display: flex; 12 | // // justify-content: space-between; 13 | // // flex-direction: column; 14 | // // flex-wrap: wrap; 15 | // // position: fixed; 16 | // // bottom: 100; 17 | // // height: 100%; 18 | // // flex-grow: 1; 19 | // } 20 | // 21 | // .username:hover { 22 | // color: white; 23 | // } 24 | // 25 | // .playbar { 26 | // position: fixed; 27 | // bottom: 0; 28 | // left: 0; 29 | // width: 100%; 30 | // height: 100px; 31 | // margin-top: 1px; 32 | // z-index: 100; 33 | // background-color: #282828; 34 | // } 35 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /app/views/api/search/index.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.playlists do 2 | json.array!(@playlists) do |playlist| 3 | json.extract! playlist, :id, :name, :user 4 | json.image_url asset_path(playlist.image.url) 5 | end 6 | end 7 | 8 | json.albums do 9 | json.array!(@albums) do |album| 10 | json.extract! album, :id, :name 11 | json.artist album.artist.name 12 | json.image_url asset_path(album.image.url) 13 | end 14 | end 15 | 16 | json.artists do 17 | json.array!(@artists) do |artist| 18 | json.extract! artist, :id, :name 19 | json.image_url asset_path(artist.image.url) 20 | end 21 | end 22 | 23 | json.tracks do 24 | json.array!(@tracks) do |track| 25 | json.extract! track, :id, :title, :album_id, :ord 26 | json.audio asset_path(track.audio.url) 27 | json.artist track.artist.name 28 | json.artist_id track.artist.id 29 | json.image_url asset_path(track.album.image.url) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /frontend/components/home/playlists/playlists_container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Playlists from './playlists'; 4 | import { createPlaylist } from '../../../actions/playlist_actions' 5 | // import { fetchPlaylistsForUser } from '../../actions/playlist_actions'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | const user = state.session.currentUser; 9 | const playlist_ids = Object.keys(state.playlists) 10 | return { 11 | playlists: playlist_ids.map(id => state.playlists[id]), 12 | user: state.session.currentUser 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch, ownProps) => { 17 | return { 18 | createPlaylist: playlist => dispatch(createPlaylist(playlist)), 19 | // fetchPlaylists: playlist => dispatch(fetchPlaylists(playlist)) 20 | }; 21 | }; 22 | 23 | export default connect( 24 | mapStateToProps, 25 | mapDispatchToProps) 26 | (Playlists); 27 | -------------------------------------------------------------------------------- /frontend/components/session_form/session_form_container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { login, signup, clearErrors } from '../../actions/session_actions'; 4 | import SessionForm from './session_form'; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | return { 8 | loggedIn: Boolean(state.session.currentUser), 9 | errors: state.session.errors, 10 | formType: ownProps.location.pathname 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch, ownProps) => { 15 | const formType = ownProps.location.pathname; 16 | const processForm = (formType === '/login') ? login : signup; 17 | return { 18 | processForm: user => dispatch(processForm(user)), 19 | clearErrors: () => dispatch(clearErrors()), 20 | login: user => dispatch(login(user)) 21 | }; 22 | }; 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps) 27 | (SessionForm); 28 | -------------------------------------------------------------------------------- /frontend/reducers/audio_reducer.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import { RECEIVE_CURRENT_SONG, RECEIVE_SONGS, SKIP } from '../actions/audio_actions'; 3 | import { RECEIVE_CURRENT_USER } from '../actions/session_actions'; 4 | 5 | const AudioReducer = (state = [], action) => { 6 | Object.freeze(state) 7 | let newState = []; 8 | switch (action.type) { 9 | case RECEIVE_CURRENT_SONG: 10 | // newState = state.slice(); 11 | newState.push(action.track) 12 | // newState = queue: [action.track] 13 | return newState; 14 | case RECEIVE_SONGS: 15 | // newState = state.slice(); 16 | newState = newState.concat(action.tracks); 17 | // return newState; 18 | return newState; 19 | case RECEIVE_CURRENT_USER: 20 | return []; 21 | // case SKIP: 22 | // newState = state.slice(1); 23 | // return newState; 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default AudioReducer; 30 | -------------------------------------------------------------------------------- /app/views/api/artists/show.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.artist do 2 | json.extract! @artist, :id, :name 3 | # json.track_ids @artist.tracks.pluck(:id) 4 | json.image_url asset_path(@artist.image.url) 5 | end 6 | 7 | # json.tracks do 8 | # @artist.tracks.each do |track| 9 | # json.set! track.id do 10 | # json.extract! track, :title 11 | # json.audio asset_path(track.audio.url) 12 | # end 13 | # end 14 | # end 15 | 16 | 17 | json.albums do 18 | json.array!(@artist.albums) do |album| 19 | json.extract! album, :id, :name 20 | json.artist album.artist.name 21 | json.image_url asset_path(album.image.url) 22 | end 23 | end 24 | 25 | json.tracks do 26 | json.array!(@artist.tracks[0..4]) do |track| 27 | json.extract! track, :id, :title, :album_id, :ord 28 | json.audio asset_path(track.audio.url) 29 | json.artist track.artist.name 30 | json.artist_id track.artist.id 31 | json.image_url asset_path(track.album.image.url) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/api/playlistings_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::PlaylistingsController < ApplicationController 2 | 3 | def create 4 | @playlisting = Playlisting.new(playlisting_params) 5 | @playlist = Playlist.find(@playlisting.playlist_id) 6 | if Playlisting.all.length > 0 7 | @playlisting.ord = Playlisting.last.ord + 1 8 | else 9 | @playlisting.ord = 1 10 | end 11 | @playlisting.save 12 | render json: {} 13 | end 14 | 15 | def destroy 16 | @playlisting = Playlisting.find(params[:id]) 17 | @playlist = Playlist.find(@playlisting.playlist_id) 18 | @playlisting.delete 19 | render "api/playlists/show" 20 | end 21 | 22 | def kill 23 | @playlist = Playlist.find(params[:playlist_id].to_i) 24 | @playlist.playlistings.where(track_id: params[:track_id].to_i).destroy_all 25 | render "api/playlists/show" 26 | end 27 | 28 | def playlisting_params 29 | params.require(:playlisting).permit(:playlist_id, :track_id) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApplicationController 2 | 3 | def create 4 | @user = User.new(user_params) 5 | if @user.save 6 | login(@user) 7 | render :show 8 | else 9 | render json: @user.errors.full_messages, status: 422 10 | end 11 | end 12 | 13 | def show 14 | @user = User.find(params[:id]).includes(:playlist, :tracks, :albums) 15 | end 16 | 17 | def edit 18 | @user = User.find(params[:id]) 19 | render :edit 20 | end 21 | 22 | def update 23 | @user = User.find(params[:id]) 24 | if @user.id == current_user.id && @user.update(user_params) 25 | render :show 26 | else 27 | render json: @user.errors.full_messages, status: 422 28 | render :edit 29 | end 30 | end 31 | 32 | def destroy 33 | @user = User.find(params[:id]) 34 | @user.delete 35 | redirect_to new_api_session_url 36 | end 37 | 38 | private 39 | 40 | def user_params 41 | params.require(:user).permit(:username, :password) 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /app/views/api/users/_user.json.jbuilder: -------------------------------------------------------------------------------- 1 | json.user do 2 | json.extract! user, :id, :username 3 | json.playlist_ids user.playlists.pluck(:id) 4 | json.album_ids user.albums.pluck(:id).uniq 5 | json.artist_ids user.artists.pluck(:id).uniq 6 | end 7 | 8 | json.playlists do 9 | user.playlists.each do |playlist| 10 | json.set! playlist.id do 11 | json.extract! playlist, :id, :name, :user_id 12 | json.creator playlist.user.username 13 | json.image_url asset_path(playlist.image.url) 14 | end 15 | end 16 | end 17 | 18 | json.albums do 19 | user.albums.each do |album| 20 | json.set! album.id do 21 | json.extract! album, :id, :name, :artist_id 22 | json.artist album.artist.name 23 | json.image_url asset_path(album.image.url) 24 | end 25 | end 26 | end 27 | 28 | json.artists do 29 | user.artists.each do |artist| 30 | json.set! artist.id do 31 | json.extract! artist, :id, :name 32 | json.image_url asset_path(artist.image.url) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /frontend/util/playlist_api_util.js: -------------------------------------------------------------------------------- 1 | // export const fetchPlaylists = user => { 2 | // return $.ajax({ 3 | // method: 'GET', 4 | // url: `/api/users/${user.id}` 5 | // }); 6 | // }; 7 | 8 | export const fetchPlaylist = id => { 9 | return $.ajax({ 10 | method: 'GET', 11 | url: `/api/playlists/${id}` 12 | }); 13 | }; 14 | 15 | export const createPlaylist = playlist => { 16 | return $.ajax({ 17 | method: 'POST', 18 | url: '/api/playlists', 19 | data: { playlist } 20 | }); 21 | }; 22 | 23 | export const deletePlaylist = id => { 24 | return $.ajax({ 25 | method: 'DELETE', 26 | url: `/api/playlists/${id}` 27 | }); 28 | }; 29 | 30 | export const addTrack = playlisting => { 31 | return $.ajax({ 32 | method: 'POST', 33 | url: '/api/playlistings', 34 | data: { playlisting } 35 | }); 36 | }; 37 | 38 | export const removeTrack = playlisting => { 39 | return $.ajax({ 40 | method: 'DELETE', 41 | url: `/api/playlistings/kill`, 42 | data: playlisting 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/spitfire.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import configureStore from './store/store' 4 | import Root from './components/root'; 5 | import {fetchPlaylist} from './actions/playlist_actions' 6 | import {fetchAlbum} from './actions/album_actions' 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | let store; 10 | if (window.currentUser) { 11 | const user = window.currentUser 12 | const preloadedState = { 13 | session: { currentUser: user.user }, 14 | playlists: user.playlists, 15 | albums: user.albums, 16 | artists: user.artists 17 | }; 18 | store = configureStore(preloadedState); 19 | delete window.currentUser; 20 | } else { 21 | store = configureStore(); 22 | } 23 | 24 | // window.store = store; 25 | // window.dispatch = store.dispatch; 26 | // window.fetchPlaylist = fetchPlaylist; 27 | // window.fetchAlbum = fetchAlbum; 28 | const root = document.getElementById('root'); 29 | ReactDOM.render(, root); 30 | }); 31 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: da11b6b090efa86006e0879099c526d97cc059193dc89443788d6839ed2b5b713df10806352731a81763b60546e2a49c2a04e6204123a376883955c926070473 15 | 16 | test: 17 | secret_key_base: 147f2474c9b880656cddff81ac9f780c8cfb5080cc99a73f54d41ddb46c053b221ea5a739059e486548ddb3ab237876799ecb4de0e96c57c50ed2d5a5137e6f0 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 | -------------------------------------------------------------------------------- /frontend/util/route_util.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, withRouter, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | 5 | const Auth = ({component: Component, path, loggedIn, exact}) => { 6 | return ( 7 | ( 8 | !loggedIn ? ( 9 | 10 | ) : ( 11 | 12 | ) 13 | )}/> 14 | ); 15 | }; 16 | 17 | const Protected = ({component: Component, path, loggedIn, exact}) => { 18 | return ( 19 | { 20 | let component; 21 | if (loggedIn) { 22 | component = 23 | } else { 24 | component = 25 | } 26 | return component 27 | }}/> 28 | ) 29 | }; 30 | 31 | const mapStateToProps = state => { 32 | return {loggedIn: Boolean(state.session.currentUser)}; 33 | }; 34 | 35 | export const AuthRoute = withRouter(connect(mapStateToProps, null)(Auth)); 36 | export const ProtectedRoute = withRouter(connect(mapStateToProps)(Protected)); 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require("webpack"); 3 | 4 | var plugins = []; 5 | var devPlugins = []; 6 | var prodPlugins = [ 7 | new webpack.DefinePlugin({ 8 | 'process.env': { 9 | 'NODE_ENV': JSON.stringify('production') 10 | } 11 | }), 12 | new webpack.optimize.UglifyJsPlugin({ 13 | compress: { 14 | warnings: true 15 | } 16 | }) 17 | ]; 18 | 19 | plugins = plugins.concat( 20 | process.env.NODE_ENV === 'production' ? prodPlugins : devPlugins 21 | ) 22 | 23 | module.exports = { 24 | context: __dirname, 25 | entry: "./frontend/spitfire.jsx", 26 | output: { 27 | path: path.resolve(__dirname, 'app', 'assets', 'javascripts'), 28 | filename: "bundle.js" 29 | }, 30 | plugins: plugins, 31 | module: { 32 | loaders: [ 33 | { 34 | test: [/\.jsx?$/, /\.js?$/], 35 | exclude: /node_modules/, 36 | loader: 'babel-loader', 37 | query: { 38 | presets: ['es2015', 'react'] 39 | } 40 | } 41 | ] 42 | }, 43 | devtool: 'source-maps', 44 | resolve: { 45 | extensions: [".js", ".jsx", "*"] 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /app/assets/stylesheets/search.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | } 6 | 7 | .search-header { 8 | padding: 25px; 9 | padding-bottom: 0px; 10 | background: #242424; 11 | display: flex; 12 | flex-direction: column; 13 | min-height: 115px; 14 | } 15 | .search-header label { 16 | font-size: 14px; 17 | letter-spacing: 1px; 18 | margin-bottom: 8px; 19 | min-width: 315px; 20 | color: #fff; 21 | } 22 | 23 | .search-input { 24 | background: transparent; 25 | color: #fff; 26 | line-height: 1.2; 27 | font-size: 62px; 28 | width: 100%; 29 | box-sizing: border-box; 30 | font-family: 'ProximaNova', sans-serif; 31 | font-weight: bold; 32 | letter-spacing: 1px; 33 | } 34 | .search-input::-webkit-input-placeholder { 35 | color: #666666; 36 | } 37 | 38 | 39 | .results { 40 | background: #0b0b0b; 41 | height: 100%; 42 | } 43 | 44 | .search-tracks-container { 45 | display: flex; 46 | justify-content: center; 47 | // padding-bottom: 30px; 48 | } 49 | h1.search-tracks-container { 50 | padding-left: 20px; 51 | } 52 | h2.search-tracks-container { 53 | padding-left: 20px; 54 | } 55 | -------------------------------------------------------------------------------- /app/assets/stylesheets/reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | attr_reader :password 3 | validates :username, :password_digest, :session_token, presence: true 4 | validates :username, uniqueness: true 5 | validates :password, length: {minimum: 6}, allow_nil: :true 6 | after_initialize :ensure_session_token 7 | 8 | has_many :playlists 9 | has_many :tracks, through: :playlists 10 | has_many :albums, through: :tracks; 11 | has_many :artists, through: :tracks 12 | 13 | def self.find_by_credentials(username, password) 14 | user = User.find_by(username: username) 15 | user && user.is_password?(password) ? user : nil 16 | end 17 | 18 | def password=(password) 19 | self.password_digest = BCrypt::Password.create(password) 20 | @password = password 21 | end 22 | 23 | def is_password?(password) 24 | BCrypt::Password.new(self.password_digest).is_password?(password) 25 | end 26 | 27 | def reset_session_token! 28 | self.session_token ||= SecureRandom::urlsafe_base64(16) 29 | self.save! 30 | self.session_token 31 | end 32 | 33 | private 34 | def ensure_session_token 35 | self.session_token ||= SecureRandom::urlsafe_base64(16) 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /frontend/components/home/modal/add_track_modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class AddTrackModal extends React.Component { 4 | 5 | close(e) { 6 | e.preventDefault(); 7 | if (this.props.onClose) { 8 | this.props.onClose() 9 | } 10 | } 11 | render() { 12 | if (this.props.isOpen === false) { 13 | return null; 14 | } 15 | 16 | let modalStyle = { 17 | position: 'absolute', 18 | top: '50%', 19 | left: '50%', 20 | transform: 'translate(-50%, -50%)', 21 | zIndex: '9999', 22 | height: '100%', 23 | width: '100%' 24 | } 25 | 26 | let backdropStyle = { 27 | position: 'fixed', 28 | width: '100%', 29 | height: '100vh', 30 | top: '0px', 31 | left: '0px', 32 | zIndex: '9998', 33 | background: 'rgba(0, 0, 0, 0.9)', 34 | } 35 | 36 | return ( 37 |
38 |
39 | {this.props.children} 40 |
41 | {!this.props.noBackdrop && 42 |
} 43 |
44 | ); 45 | } 46 | } 47 | 48 | export default AddTrackModal; 49 | -------------------------------------------------------------------------------- /frontend/components/home/browse/browse_nav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Link, Redirect, NavLink } from 'react-router-dom'; 3 | import { AuthRoute, ProtectedRoute } from '../../../util/route_util'; 4 | import BrowseArtists from './browse_artists'; 5 | import BrowseAlbums from './browse_albums'; 6 | 7 | const BrowseNav = () => { 8 | 9 | return ( 10 | 11 |
12 |
13 |
    14 |
  • ARTISTS
  • 16 |
  • ALBUMS
  • 18 |
19 |

Browse

20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | ); 29 | 30 | } 31 | 32 | export default BrowseNav; 33 | -------------------------------------------------------------------------------- /frontend/components/home/modal/modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Modal extends React.Component { 4 | 5 | close(e) { 6 | e.preventDefault(); 7 | if (this.props.onClose) { 8 | this.props.onClose() 9 | } 10 | } 11 | render() { 12 | if (this.props.isOpen === false) { 13 | return null; 14 | } 15 | 16 | let modalStyle = { 17 | position: 'fixed', 18 | // position: 'absolute', 19 | top: '50%', 20 | left: '50%', 21 | transform: 'translate(-50%, -50%)', 22 | zIndex: '9999', 23 | height: '100%', 24 | // width: '100%' 25 | } 26 | 27 | let backdropStyle = { 28 | position: 'fixed', 29 | // position: 'absolute', 30 | width: '100%', 31 | // height: '100vh', 32 | top: '0px', 33 | left: '0px', 34 | bottom: '0px', 35 | zIndex: '9998', 36 | background: 'rgba(0, 0, 0, 0.9)', 37 | // display: 'flex' 38 | } 39 | 40 | return ( 41 |
42 |
43 | {this.props.children} 44 |
45 | {!this.props.noBackdrop && 46 |
} 47 |
48 | ); 49 | } 50 | } 51 | 52 | export default Modal; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Spitfire", 3 | "version": "1.0.0", 4 | "description": "== README", 5 | "main": "webpack.config.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "test" 9 | }, 10 | "engines": { 11 | "node": "6.10.1", 12 | "npm": "3.10.10" 13 | }, 14 | "dependencies": { 15 | "babel-core": "^6.25.0", 16 | "babel-loader": "^7.0.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "babel-preset-react": "^6.24.1", 19 | "react": "^15.6.1", 20 | "react-dom": "^15.6.1", 21 | "react-redux": "^5.0.5", 22 | "react-router": "^4.1.1", 23 | "react-router-dom": "^4.1.1", 24 | "redux": "^3.7.0", 25 | "redux-thunk": "^2.2.0", 26 | "save": "^2.3.1", 27 | "webpack": "^3.0.0" 28 | }, 29 | "devDependencies": { 30 | "redux-logger": "^3.0.6" 31 | }, 32 | "scripts": { 33 | "test": "echo \"Error: no test specified\" && exit 1", 34 | "webpack": "webpack --watch", 35 | "postinstall": "webpack" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/stenkoff/Spitfire.git" 40 | }, 41 | "keywords": [], 42 | "author": "", 43 | "license": "ISC", 44 | "bugs": { 45 | "url": "https://github.com/stenkoff/Spitfire/issues" 46 | }, 47 | "homepage": "https://github.com/stenkoff/Spitfire#readme" 48 | } 49 | -------------------------------------------------------------------------------- /docs/component-hierarchy.md: -------------------------------------------------------------------------------- 1 | ## Component Hierarchy 2 | **AuthFormContainer** 3 | - AuthForm 4 | 5 | **HomeContainer** 6 | - Sidebar 7 | - Playbar 8 | 9 | **MyMusicContainer** 10 | - MyPlaylists 11 | - MySongs 12 | - MyAlbums 13 | - MyArtists 14 | 15 | **Playlst Container** 16 | - Playlist 17 | - CreatePlayList 18 | 19 | **ArtistContainer** 20 | - Overview 21 | - RelatedArtist 22 | 23 | **SearchContainer** 24 | - Search 25 | 26 | **AddToPlaylistContainer** 27 | - AddToPlaylist 28 | 29 | **ProfileSettingContainer** 30 | - ProfileSettings 31 | 32 | ## Bonus 33 | **DiscoverContainer** 34 | - Discover 35 | 36 | 37 | |Path | Component | 38 | |-------|-------------| 39 | | "/sign-up" | "AuthFormContainer" | 40 | | "/sign-in" | "AuthFormContainer" | 41 | | "/mymusic/playlists" | "MyPlaylists" | 42 | | "/mymusic/songs" | "MySongs" | 43 | | "/mymusic/albums" | "MyAlbums" | 44 | | "/mymusic/artists" | "MyArtists" | 45 | | "/playlists/:playlistId" | "PlaylistContainer" | 46 | | "/albums/:albumId" | "AlbumContainer" | 47 | | "/artists/:artistId" | "ArtistContainer" | 48 | | "/search/playlists" | "SearchContainer" | 49 | | "/search/songs" | "SearchContainer" | 50 | | "/search/albums" | "SearchContainer" | 51 | | "/search/artists" | "SearchContainer" | 52 | | "/settings" | "ProfileSettingsContainer" 53 | -------------------------------------------------------------------------------- /frontend/actions/session_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/session_api_util' 2 | 3 | export const RECEIVE_CURRENT_USER = "RECEIVE_CURRENT_USER"; 4 | export const RECEIVE_ERRORS = "RECEIVE_ERRORS"; 5 | export const CLEAR_ERRORS = 'CLEAR ERRORS'; 6 | export const LOGOUT_CURRENT_USER = 'LOGOUT_CURRENT_USER'; 7 | 8 | export const receiveCurrentUser = ({user, playlists, artists, albums}) => ({ 9 | type: RECEIVE_CURRENT_USER, 10 | currentUser: user, 11 | playlists, 12 | artists, 13 | albums 14 | }); 15 | export const logoutCurrentUser = ({user, playlists}) => ({ 16 | type: LOGOUT_CURRENT_USER, 17 | currentUser: user, 18 | playlists, 19 | }); 20 | 21 | export const receiveErrors = errors => ({ 22 | type: RECEIVE_ERRORS, 23 | errors 24 | }); 25 | 26 | export const clearErrors = () => ({ 27 | type: CLEAR_ERRORS 28 | }) 29 | 30 | export const signup = user => dispatch => { 31 | return APIUtil.signup(user) 32 | .then(user => dispatch(receiveCurrentUser(user)), 33 | error => dispatch(receiveErrors(error.responseJSON))); 34 | }; 35 | 36 | export const login = user => dispatch => { 37 | return APIUtil.login(user) 38 | .then(user => dispatch(receiveCurrentUser(user)), 39 | error => dispatch(receiveErrors(error.responseJSON))); 40 | }; 41 | 42 | export const logout = () => dispatch => { 43 | return APIUtil.logout().then(() => dispatch(logoutCurrentUser({user: null, playlists: {}}))) 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/components/home/music_nav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Link, Redirect, NavLink } from 'react-router-dom'; 3 | import PlaylistsContainer from './playlists/playlists_container'; 4 | import PlaylistShow from './playlists/playlist_show'; 5 | import Albums from './albums/albums'; 6 | import AlbumShow from './albums/album_show'; 7 | import Artists from './artists/artists'; 8 | import ArtistShow from './artists/artist_show'; 9 | import { AuthRoute, ProtectedRoute } from '../../util/route_util'; 10 | 11 | const MusicNav = () => { 12 | 13 | 14 | return ( 15 | 16 |
17 |
18 |
    19 |
  • PLAYLISTS
  • 20 |
  • ARTISTS
  • 21 |
  • ALBUMS
  • 22 |
23 |
24 | 25 | {/* */} 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | 33 | } 34 | 35 | export default MusicNav 36 | -------------------------------------------------------------------------------- /frontend/components/home/browse/browse_artists.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchArtists } from '../../../actions/artist_actions'; 4 | import { Link } from 'react-router-dom' 5 | 6 | class BrowseArtists extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | this.props.fetchArtists(); 13 | } 14 | 15 | 16 | render() { 17 | let artists = this.props.artists 18 | if (artists) { 19 | return ( 20 |
21 |
22 | { 23 | artists.map(artist => 24 |
25 | 26 | 27 |

{artist.name}

28 | 29 |
) 30 | } 31 |
32 |
33 | ) 34 | } else { 35 | return null; 36 | } 37 | } 38 | } 39 | 40 | 41 | const mapDispatchToProps = dispatch => { 42 | return { 43 | fetchArtists: () => dispatch(fetchArtists()) 44 | }; 45 | }; 46 | const mapStateToProps = (state, ownProps) => { 47 | return { 48 | artists: state.browse.artists 49 | }; 50 | }; 51 | 52 | export default connect(mapStateToProps, mapDispatchToProps)(BrowseArtists); 53 | // export default Browse; 54 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Spitfire 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | config.paperclip_defaults = { 24 | :storage => :s3, 25 | :s3_credentials => { 26 | :bucket => ENV["s3_bucket"], 27 | :access_key_id => ENV["s3_access_key_id"], 28 | :secret_access_key => ENV["s3_secret_access_key"], 29 | :s3_region => ENV["s3_region"] 30 | } 31 | } 32 | 33 | # Do not swallow errors in after_commit/after_rollback callbacks. 34 | config.active_record.raise_in_transactional_callbacks = true 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /frontend/components/home/browse/browse_albums.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchArtists } from '../../../actions/artist_actions'; 4 | import { Link } from 'react-router-dom' 5 | 6 | class BrowseAlbums extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | this.props.fetchArtists(); 13 | } 14 | // componentWillReceiveProps() { 15 | // this.props.fetchAlbums(); 16 | // } 17 | 18 | render() { 19 | let albums = this.props.albums 20 | if (albums) { 21 | return ( 22 |
23 |
24 | { 25 | albums.map(album => 26 |
27 | 28 | 30 |

{album.name}

31 | 32 |

{album.artist}

33 |
) 34 | } 35 |
36 |
37 | ) 38 | } else { 39 | return null; 40 | } 41 | } 42 | } 43 | 44 | 45 | const mapDispatchToProps = dispatch => { 46 | return { 47 | fetchArtists: () => dispatch(fetchArtists()) 48 | }; 49 | }; 50 | const mapStateToProps = (state, ownProps) => { 51 | return { 52 | albums: state.browse.albums 53 | }; 54 | }; 55 | 56 | export default connect(mapStateToProps, mapDispatchToProps)(BrowseAlbums); 57 | // export default Browse; 58 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/add_track.scss: -------------------------------------------------------------------------------- 1 | .add-to-playlist { 2 | display: flex; 3 | justify-content: center; 4 | flex-direction: column; 5 | align-items: center; 6 | &:hover { 7 | color: #fff; 8 | } 9 | } 10 | 11 | .add-btn { 12 | background: transparent; 13 | font-size: 16px; 14 | justify-content: center; 15 | padding-bottom: 1px; 16 | padding-right: 30px; 17 | align-items: center; 18 | font-weight: 100; 19 | color: #a0a0a0; 20 | vertical-align: top; 21 | &:hover { 22 | color: #fff; 23 | cursor: pointer; 24 | } 25 | } 26 | .playlist-options { 27 | display: flex; 28 | flex-wrap: wrap; 29 | flex-direction: row; 30 | justify-content: center; 31 | // margin: 15px; 32 | margin-top: 20px; 33 | // background: rgba(0, 0, 0, 0.9); 34 | margin-left: 20px; 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | // height: 100vh; 39 | } 40 | // #add-track { 41 | // display: flex; 42 | // flex-direction: column; 43 | // align-content: center; 44 | // align-items: center; 45 | // background-color: transparent; 46 | // // width: 100%; 47 | // 48 | // // height: 100vh; 49 | // justify-content: center; 50 | // // position: absolute; 51 | // } 52 | 53 | .add-to-playlist h1 { 54 | font-size: 54px; 55 | font-weight: bold; 56 | letter-spacing: 1px; 57 | height: auto; 58 | color: #fff; 59 | margin: 30px 90px 30px 90px; 60 | text-align: center; 61 | } 62 | 63 | 64 | #add-to-pl-header { 65 | font-family: 'ProximaNova', sans-serif; 66 | font-size: 54px; 67 | // font-weight: bold; 68 | letter-spacing: 1px; 69 | height: auto; 70 | color: #fff; 71 | margin: 30px 90px 30px 90px; 72 | text-align: center; 73 | } 74 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Spitfire 2 | 3 | Minimum Viable Product 4 | 5 | ## Minimum Viable Product 6 | 7 | My project is a web application inspired by Spotify and built using Ruby on Rails and React/Redux. By the end of Week 9, this app will, at a minimum, satisfy the following criteria with smooth, bug-free navigation, adequate seed data and sufficient CSS styling: 8 | 9 | - [ ] New account creation, login, and guest/demo login 10 | - [ ] A production README 11 | - [ ] Hosting on Heroku 12 | - [ ] Song/Playlist CRUD 13 | - [ ] Search 14 | - [ ] Continuous play while navigating site 15 | - [ ] Following playlists/Friending users 16 | - [ ] Bonus: Radio (shuffle play) 17 | - [ ] Bonus: Explore Page 18 | 19 | ## Design Docs 20 | * [View Wireframes][wireframes] 21 | * [React Components][components] 22 | * [API endpoints][api-endpoints] 23 | * [DB schema][schema] 24 | * [Sample State][sample-state] 25 | 26 | [wireframes]: docs/wireframes 27 | [components]: docs/component-hierarchy.md 28 | [sample-state]: docs/sample-state.md 29 | [api-endpoints]: docs/api-endpoints.md 30 | [schema]: docs/schema.md 31 | 32 | 33 | ## Implementation Timeline 34 | 35 | ### Phase 1: Backend setup and Front End User Authentication (2 days) 36 | 37 | **Objective:** Functioning rails project with front-end Authentication 38 | 39 | ### Phase 2: Playlist CRUD (3 days) 40 | 41 | **Objective:** Users can create, update, read and destroy playlists 42 | 43 | 44 | ### Phase 4: Search and make playlists (3 days) 45 | 46 | **Objective:** Users can search and add songs to playlists 47 | 48 | 49 | ### Phase 5: Continuous play (1 day) 50 | 51 | **Objective:** Audio can be streamed continuously while navigating the site 52 | 53 | 54 | ### Bonus Features (TBD) 55 | - [ ] radio/shuffle 56 | - [ ] explore page 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spitfire 2 | [Live!](http://spitfired.herokuapp.com) 3 | 4 | Spitfire is a full stack music-streaming application inspired by the popular web application, Spotify. Spitfire implements Ruby on Rails backend, PostgreSQL database and React/Redux on the frontend. 5 | 6 | # Features and Implementation 7 | 8 | ## Asynchronous Music Playback 9 | ![alt text](./app/assets/images/playbar.png) 10 | 11 | Users can play music continuously while navigating throughout the site. Continuous playback is achieved by making the audioplayer a top level component, so as users browse the site, the audioplayer is rendered on every page and the stream of music is never disrupted. The audioplayer displays the progress of the current song by setting the width of the bar equal to percentage of song's progress. This progress is calculated using the current time of the song and the total duration of the song. Users can play a single song or add multiple songs to a queue, and they can navigate forward and backwards through the queue. Additionally, users can adjust the volume of the audioplayer by clicking on the volume bar. 12 | 13 | ## Search 14 | ![alt text](./app/assets/images/search.png) 15 | 16 | Users can search for playlists, artists, albums and songs. Search results are displayed and updated immediately, as a user types. On every keystroke, an onChange event handler sends an API request to the backend, which uses an ActiveRecord query to check the database for case-insensitive matches in any of the four categories, and immediately returns the results to the frontend. 17 | 18 | ![alt text](./app/assets/images/search_input.png) 19 | ![alt text](./app/assets/images/handle_change.png) 20 | 22 | -------------------------------------------------------------------------------- /frontend/actions/playlist_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from '../util/playlist_api_util'; 2 | 3 | export const RECEIVE_PLAYLISTS = 'RECEIVE_PLAYLISTS'; 4 | export const RECEIVE_PLAYLIST = 'RECEIVE_PLAYLIST'; 5 | export const REMOVE_PLAYLIST = 'REMOVE_PLAYLIST'; 6 | 7 | export const receivePlaylists = playlists => ({ 8 | type: RECEIVE_PLAYLISTS, 9 | playlists 10 | }); 11 | 12 | export const receivePlaylist = ({playlist, tracks}) => { 13 | return { 14 | type: RECEIVE_PLAYLIST, 15 | playlist, 16 | tracks 17 | } 18 | }; 19 | 20 | export const removePlaylist = playlists => { 21 | return { 22 | type: REMOVE_PLAYLIST, 23 | playlists 24 | } 25 | }; 26 | 27 | export const fetchPlaylistsForUser = (user) => dispatch => { 28 | return APIUtil.fetchPlaylists(user) 29 | .then(user => dispatch(receivePlaylists(user))); 30 | }; 31 | 32 | export const fetchPlaylist = id => dispatch => { 33 | return APIUtil.fetchPlaylist(id) 34 | .then(playlist => dispatch(receivePlaylist(playlist))); 35 | }; 36 | 37 | export const createPlaylist = playlist => dispatch => { 38 | return APIUtil.createPlaylist(playlist) 39 | .then(playlist => dispatch(receivePlaylist(playlist))); 40 | }; 41 | 42 | export const deletePlaylist = id => dispatch => { 43 | return APIUtil.deletePlaylist(id) 44 | .then(playlists => dispatch(removePlaylist(playlists))); 45 | }; 46 | 47 | // export const addTrack = playlisting => dispatch => { 48 | // return APIUtil.addTrack(playlisting) 49 | // .then(playlist => dispatch(receivePlaylist(playlist))); 50 | // }; 51 | 52 | export const addTrack = playlisting => { 53 | return APIUtil.addTrack(playlisting); 54 | }; 55 | 56 | export const removeTrack = playlisting => dispatch => { 57 | return APIUtil.removeTrack(playlisting) 58 | .then(playlist => dispatch(receivePlaylist(playlist))); 59 | }; 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /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 | ## tracks 13 | column name | data type | details 14 | ------------|-----------|----------------------- 15 | id | integer | not null, primary key 16 | title | string | not null 17 | album_id | integer | not null, foreign key (references albums), indexed 18 | ord | integer | 19 | 20 | ## artists 21 | column name | data type | details 22 | ------------|-----------|----------------------- 23 | id | integer | not null, primary key 24 | name | string | not null 25 | 26 | ## albums 27 | column name | data type | details 28 | ------------|-----------|----------------------- 29 | id | integer | not null, primary key 30 | name | string | not null 31 | artist_id | integer | not null, foreign key (references artists), indexed 32 | 33 | ## playlisting 34 | column name | data type | details 35 | ------------|-----------|----------------------- 36 | id | integer | not null, primary key 37 | track_id | integer | not null, foreign key (references tracks), indexed 38 | playlist_id | integer | not null, foreign key (references artists), indexed 39 | ord | integer | not null 40 | 41 | ## playlists 42 | column name | data type | details 43 | ------------|-----------|----------------------- 44 | id | integer | not null, primary key 45 | name | string | not null 46 | user_id | integer | not null, foreign key (references users) 47 | 48 | ## user_follows 49 | column name | data type | details 50 | ------------|-----------|----------------------- 51 | id | integer | not null, primary key 52 | follower_id | integer | not null, foreign key (references users) 53 | followed_id | integer | not null, foreign key (references users) 54 | 55 | ## playlist_follows 56 | column name | data type | details 57 | ------------|-----------|----------------------- 58 | id | integer | not null, primary key 59 | user_id | integer | not null, foreign key 60 | playlist_id | integer | not null, foreign key (references playlists) 61 | 62 | 68 | -------------------------------------------------------------------------------- /frontend/components/home/playlists/playlists.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PlaylistShow from './playlist_show'; 4 | import Modal from '../modal/modal'; 5 | 6 | class Playlists extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | modal: false, 11 | playlistName: '' 12 | }; 13 | this.handleChange = this.handleChange.bind(this); 14 | this.handleSubmit = this.handleSubmit.bind(this); 15 | } 16 | 17 | render() { 18 | const playlists = this.props.playlists 19 | 20 | return ( 21 |
22 | 23 |
24 | 25 | this.closeModal()}> 26 |
27 | 28 |

Create new playlist

29 |
30 | 36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 | { 45 | playlists.map(playlist => 46 |
47 | 48 |

{playlist.name}

49 |

{playlist.creator}

50 |
) 51 | } 52 |
53 | 54 |
55 | ); 56 | } 57 | 58 | openModal() { 59 | this.setState({ modal: true }); 60 | } 61 | 62 | closeModal() { 63 | this.setState({ modal: false }); 64 | } 65 | 66 | handleChange() { 67 | return e => { 68 | this.setState({playlistName: e.currentTarget.value}); 69 | }; 70 | } 71 | 72 | handleSubmit(e) { 73 | e.preventDefault(); 74 | if (!!this.state.playlistName) { 75 | const playlist = {user_id: this.props.user.id, name: this.state.playlistName}; 76 | this.props.createPlaylist(playlist) 77 | .then(({playlist}) => { 78 | return this.props.history.push(`playlists/${playlist.id}`) 79 | } 80 | ); 81 | } 82 | } 83 | } 84 | 85 | export default Playlists; 86 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/home.scss: -------------------------------------------------------------------------------- 1 | 2 | .browse-artists { 3 | color: white; 4 | font-size: 24px; 5 | text-align: center; 6 | } 7 | 8 | .music-nav-active { 9 | color: white; 10 | } 11 | .home { 12 | min-height:100%; 13 | position: relative; 14 | font-family: 'ProximaNova', sans-serif; 15 | color: #a0a0a0; 16 | display: flex; 17 | background-color: #181818; 18 | background-image: linear-gradient(rgb(64, 73, 89), rgb(6, 7, 8) 85%); 19 | } 20 | 21 | .container { 22 | padding-bottom: 100px; 23 | display: flex; 24 | width: 100%; 25 | } 26 | 27 | .music { 28 | display: flex; 29 | flex-direction: column; 30 | width: 100%; 31 | } 32 | 33 | .sidebar { 34 | min-width: 170px; 35 | display: flex; 36 | flex-direction: column; 37 | justify-content: space-between; 38 | padding: 25px; 39 | background-color: rgba(0,0,0,.5); 40 | overflow: auto; 41 | position: sticky; 42 | top: 0; 43 | height: 100%; 44 | max-height: calc(100vh - 140px); 45 | } 46 | 47 | .sidebar-top { 48 | display: flex; 49 | flex-direction: column; 50 | justify-content: space-around; 51 | min-height: 175px; 52 | } 53 | 54 | #logo { 55 | border-bottom: 1px solid; 56 | margin-top: 24px; 57 | padding-bottom: 24px; 58 | &:hover { 59 | color: white; 60 | } 61 | } 62 | #search a { 63 | padding-bottom: 19px; 64 | padding-top: 19px; 65 | display: flex; 66 | justify-content: space-between; 67 | &:hover { 68 | color: white; 69 | } 70 | } 71 | 72 | #line { 73 | border-bottom: 1px solid; 74 | } 75 | 76 | #browse { 77 | padding-bottom: 19px; 78 | padding-top: 19px; 79 | color: #a0a0a0; 80 | width: 100%; 81 | } 82 | 83 | #music { 84 | margin-bottom: 19px; 85 | color: #a0a0a0; 86 | text-decoration: none; 87 | width: 100%; 88 | } 89 | 90 | #music a, #browse a { 91 | width: 100%; 92 | display: flex; 93 | &:hover { 94 | color: white; 95 | } 96 | } 97 | 98 | .green { 99 | color: #1db954; 100 | } 101 | 102 | .sidebar-bottom { 103 | font-size: 14px; 104 | font-family: 'ProximaNova', sans-serif; 105 | } 106 | 107 | .sidebar-bottom button { 108 | background-color: transparent; 109 | color: #a0a0a0; 110 | border: 1px solid; 111 | border-radius: 20px; 112 | padding: 3%; 113 | vertical-align: center; 114 | font-family: 'ProximaNova', sans-serif; 115 | font-size: 14px; 116 | width: 100% 117 | } 118 | 119 | .sidebar-bottom div { 120 | padding: 10px; 121 | text-align: center; 122 | } 123 | 124 | .bottom:hover, #search:hover, #music:hover { 125 | color: white; 126 | } 127 | 128 | #user { 129 | border-bottom: 1px solid; 130 | margin-top: 19px; 131 | font-size: 16px; 132 | } 133 | 134 | #disclaimer { 135 | font-size: 11px; 136 | padding-top: 5px; 137 | } 138 | 139 | html, body, #root { 140 | margin:0; 141 | padding:0; 142 | height:100%; 143 | background: black; 144 | } 145 | 146 | .body { 147 | margin:0; 148 | padding:0; 149 | height:100%; 150 | } 151 | -------------------------------------------------------------------------------- /frontend/components/home/playlists/add_track.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import PlaylistShow from './playlist_show'; 4 | import AddTrackModal from '../modal/add_track_modal'; 5 | import { connect } from 'react-redux'; 6 | import { addTrack } from '../../../actions/playlist_actions'; 7 | 8 | class AddTrack extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | modal: false, 13 | }; 14 | this.addTrack = this.addTrack.bind(this); 15 | this.closeModal = this.closeModal.bind(this); 16 | this.openModal = this.openModal.bind(this); 17 | } 18 | 19 | // componentWillReceiveProps(nextProps) { 20 | // if (nextProps.match.params.id != this.props.playlist.id) { 21 | // this.props.fetchPlaylist(nextProps.match.params.id) 22 | // } 23 | // } 24 | 25 | render() { 26 | const playlists = this.props.playlists 27 | return ( 28 |
29 | 30 |
31 | 32 | this.closeModal()}> 33 |
34 | 35 |

Add to playlist

36 |
37 |
38 | { 39 | playlists.map(playlist => 40 |
41 | this.addTrack(e, playlist)}/> 44 |

{playlist.name}

45 |

{playlist.creator}

46 |
) 47 | } 48 |
49 |
50 |
51 |
52 | 53 | ); 54 | } 55 | 56 | openModal() { 57 | window.scrollTo(0,0) 58 | this.setState({ modal: true }); 59 | } 60 | 61 | closeModal() { 62 | this.setState({ modal: false }); 63 | } 64 | 65 | addTrack(e, playlist) { 66 | // (e) => { 67 | e.preventDefault(); 68 | const playlisting = { 69 | track_id: this.props.track.id, 70 | playlist_id: playlist.id 71 | } 72 | this.props.addTrack(playlisting) 73 | .then(() => this.closeModal()) 74 | // } 75 | } 76 | } 77 | 78 | 79 | const mapStateToProps = state => { 80 | // const ids = state.session.currentUser.playlist_ids; 81 | const ids = Object.keys(state.playlists) 82 | return { 83 | playlists: ids.map(id => state.playlists[id]), 84 | addTrack: playlisting => addTrack(playlisting) 85 | }; 86 | }; 87 | 88 | // const mapDispatchToProps = dispatch => { 89 | // return { 90 | // 91 | // }; 92 | // }; 93 | 94 | export default connect( 95 | mapStateToProps) 96 | (AddTrack); 97 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/playlists.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Playlists controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | .albums { 5 | color: black; 6 | } 7 | 8 | .main { 9 | width: 100%; 10 | // background-image: linear-gradient(rgb(64, 73, 89), rgb(6, 7, 8) 8%); 11 | display: flex; 12 | flex-direction: column; 13 | 14 | } 15 | 16 | .music-bar { 17 | display: flex; 18 | width: 100vw; 19 | justify-content: center; 20 | width: 100%; 21 | } 22 | 23 | .music-bar li { 24 | margin: 37px; 25 | font-size: 12px; 26 | letter-spacing: 1px; 27 | &:hover { 28 | color: white; 29 | } 30 | &:after { 31 | content: ""; 32 | height: 2px; 33 | width: 18px; 34 | position: relative; 35 | top: .5em; 36 | display: block; 37 | margin: 0 auto; 38 | } 39 | &:hover:after { 40 | background-color: #1ed760; 41 | } 42 | // &:active { 43 | // border-bottom: 2px solid #1ed660; 44 | // 45 | // } 46 | } 47 | 48 | 49 | // .music-bar li::before { 50 | // content: ""; 51 | // width: 0px; 52 | // } 53 | // .music-bar li::after { 54 | // content: ""; 55 | // height: 2px; 56 | // width: 18px; 57 | // position: relative; 58 | // top: .5em; 59 | // display: block; 60 | // margin: 0 auto; 61 | // background: #1ed760; 62 | // } 63 | 64 | .new-playlist { 65 | width: 100%; 66 | display: flex; 67 | justify-content: center; 68 | margin-top: 10px; 69 | margin-bottom: 10px; 70 | } 71 | 72 | .new-playlist button { 73 | background-color: #1db954; 74 | letter-spacing: 2px; 75 | color: white; 76 | padding: 9px 22px;; 77 | border-radius: 20px; 78 | font-size: 11px; 79 | min-width: 130px; 80 | // position: absolute; 81 | &:hover { 82 | padding-left: 23px; 83 | padding-right: 23px; 84 | box-sizing: border-box; 85 | transition: .15s ease; 86 | background-color: #1ed660; 87 | color: white; 88 | } 89 | } 90 | 91 | .playlist-section { 92 | display: flex; 93 | flex-wrap: wrap; 94 | flex-direction: row; 95 | justify-content: flex-start; 96 | margin: 15px; 97 | margin-top: 40px; 98 | } 99 | 100 | .playlist-item { 101 | margin: 10px; 102 | text-align: center; 103 | } 104 | 105 | .playlist-item h1 { 106 | margin-top: 15px; 107 | margin-bottom: 5px; 108 | font-size: 14px; 109 | color: #fff; 110 | 111 | } 112 | .playlist-item a { 113 | text-decoration: none; 114 | color: #fff; 115 | } 116 | 117 | .playlist-item h2 { 118 | margin-bottom: 5px; 119 | font-size: 12px; 120 | } 121 | 122 | .playlist-image { 123 | object-fit: cover; 124 | height: 250px; 125 | width: auto; 126 | } 127 | 128 | @media only screen and (max-width: 1065px) { 129 | .playlist-image { 130 | height: 225px; 131 | width: auto; 132 | } 133 | } 134 | 135 | @media only screen and (max-width: 1000px) { 136 | .playlist-image { 137 | height: 200px; 138 | width: auto; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /frontend/components/session_form/session_form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | 4 | class SessionForm extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | username: "", 9 | password: "" 10 | }; 11 | 12 | this.handleChange = this.handleChange.bind(this); 13 | this.handleSubmit = this.handleSubmit.bind(this); 14 | this.demoLogin = this.demoLogin.bind(this); 15 | } 16 | 17 | handleChange(field) { 18 | return e => { 19 | this.setState({[field]: e.currentTarget.value}); 20 | }; 21 | } 22 | 23 | handleSubmit(e) { 24 | e.preventDefault(); 25 | const user = Object.assign({}, this.state); 26 | this.props.processForm(user); 27 | } 28 | 29 | demoLogin(e) { 30 | e.preventDefault(); 31 | this.props.login({ 32 | username: 'demo', 33 | password: 'password' 34 | }); 35 | } 36 | 37 | renderErrors() { 38 | return ( 39 | this.props.errors.map((err, i)=> ( 40 |
  • 41 | {err} 42 |
  • )) 43 | ); 44 | } 45 | 46 | componentDidMount(nextProps) { 47 | this.props.clearErrors(); 48 | } 49 | 50 | render() { 51 | const formType = this.props.formType === '/login' ? 'LOGIN' : 'SIGN UP'; 52 | const form2 = this.props.formType === '/login' ? "Don't have an account? Sign up here!" : 'Already have an account? Log in here.'; 53 | const url = this.props.formType === '/login' ? '/signup' : '/login'; 54 | 55 | return ( 56 |
    57 |
    58 |
    59 |

    Spitfire

    60 | 61 |
    62 | 68 | 69 | 76 | 77 |
    78 | 79 | 80 |
    81 | {form2} 82 |
    83 |
    84 |
      {this.renderErrors()}
    85 |
    86 |
    87 | 88 |
    89 |

    Get the right music,

    90 |

    right now

    91 |

    Listen to millions of songs for free.

    92 |

    ✓    Search & discover music you'll love

    93 |

    ✓    Create playlists of your favorite music

    94 |
    95 |
    96 |
    97 | ); 98 | } 99 | } 100 | 101 | export default withRouter(SessionForm); 102 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see rails configuration guide 21 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: 5 23 | 24 | development: 25 | <<: *default 26 | database: Spitfire_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: Spitfire 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: Spitfire_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: Spitfire_production 84 | username: Spitfire 85 | password: <%= ENV['SPITFIRE_DATABASE_PASSWORD'] %> 86 | -------------------------------------------------------------------------------- /frontend/components/home/home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, Link, Redirect, NavLink } from 'react-router-dom'; 3 | import PlaylistsContainer from './playlists/playlists_container'; 4 | import PlaylistShow from './playlists/playlist_show'; 5 | import Albums from './albums/albums'; 6 | import AlbumShow from './albums/album_show'; 7 | import Artists from './artists/artists'; 8 | import ArtistShow from './artists/artist_show'; 9 | import MusicNav from './music_nav'; 10 | import BrowseNav from './browse/browse_nav'; 11 | import PlayBar from './playbar'; 12 | import Search from './search/search'; 13 | import { AuthRoute, ProtectedRoute } from '../../util/route_util'; 14 | 15 | class Home extends React.Component { 16 | constructor(props){ 17 | super(props); 18 | } 19 | 20 | render(){ 21 | if (this.props.currentUser) { 22 | return ( 23 |
    24 | 25 |
    26 |
    27 |
      28 | 31 | 34 |
    • 35 |
    • 36 | Browse 37 |
    • 38 |
    • 39 | Your Music 40 |
    • 41 |
    42 | 43 |
    44 |
    {this.props.currentUser.username}
    45 |
    46 | 47 |
    48 |
    For creative purposes only
    49 |
    50 |
    51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
    67 | 68 |
    69 | 70 | ); 71 | } else { 72 | return ( 73 |
    74 |
    75 | ); 76 | } 77 | } 78 | } 79 | 80 | export default Home; 81 | -------------------------------------------------------------------------------- /frontend/components/home/albums/albums.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import Modal from '../modal/modal'; 5 | import { createPlaylist } from '../../../actions/playlist_actions' 6 | 7 | class Albums extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | modal: false, 12 | playlistName: '' 13 | }; 14 | this.handleChange = this.handleChange.bind(this); 15 | this.handleSubmit = this.handleSubmit.bind(this); 16 | } 17 | 18 | render() { 19 | const albums = this.props.albums 20 | return ( 21 |
    22 | 23 |
    24 | 25 | this.closeModal()}> 26 | 27 |

    Create new playlist

    28 | 34 |
    35 | 36 | 37 |
    38 |
    39 |
    40 | 41 |
    42 | { 43 | albums.map(album => 44 |
    45 | 46 | 48 |

    {album.name}

    49 | 50 |

    {album.artist}

    51 |
    ) 52 | } 53 |
    54 | 55 |
    56 | ); 57 | } 58 | 59 | openModal() { 60 | this.setState({ modal: true }); 61 | } 62 | 63 | closeModal() { 64 | this.setState({ modal: false }); 65 | } 66 | 67 | handleChange() { 68 | return e => { 69 | this.setState({playlistName: e.currentTarget.value}); 70 | }; 71 | } 72 | 73 | handleSubmit(e) { 74 | e.preventDefault(); 75 | if (!!this.state.playlistName) { 76 | const playlist = {user_id: this.props.user.id, name: this.state.playlistName}; 77 | this.props.createPlaylist(playlist) 78 | .then(({playlist}) => { 79 | return this.props.history.push(`playlists/${playlist.id}`) 80 | } 81 | ); 82 | } 83 | } 84 | 85 | } 86 | 87 | const mapStateToProps = (state, ownProps) => { 88 | const user = state.session.currentUser; 89 | return { 90 | albums: user.album_ids.map(id => state.albums[id]), 91 | user: state.session.currentUser 92 | }; 93 | }; 94 | const mapDispatchToProps = (dispatch, ownProps) => { 95 | return { 96 | createPlaylist: playlist => dispatch(createPlaylist(playlist)), 97 | }; 98 | }; 99 | export default connect( 100 | mapStateToProps, 101 | mapDispatchToProps) 102 | (Albums); 103 | ; 104 | -------------------------------------------------------------------------------- /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 | 5 | @font-face { 6 | font-family: 'ProximaNova'; 7 | src: asset-url('ProximaNova-Regular.otf'); 8 | } 9 | 10 | @font-face { 11 | font-family: 'ProximaNova-Light'; 12 | src: asset-url('ProximaNova-Light.otf'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'ProximaNova-Bold'; 17 | src: asset-url('ProximaNova-Bold.otf'); 18 | } 19 | 20 | h1, h2, h3, p, label, input, div, section, ul, li, input:focus, button { 21 | margin: 0; 22 | border: 0; 23 | padding: 0; 24 | outline: none; 25 | } 26 | .sess { 27 | display: flex; 28 | color: white; 29 | font-family: 'ProximaNova', sans-serif; 30 | background-color: black; 31 | padding: 13%; 32 | margin: auto; 33 | align-items: center; 34 | justify-content: center; 35 | // height: 100%; 36 | position: absolute; 37 | bottom: 0; 38 | top: 0; 39 | left: 0; 40 | right: 0; 41 | // height: 100%; 42 | // justify-content: center; 43 | } 44 | 45 | .session-wrap { 46 | display: flex; 47 | } 48 | 49 | .session { 50 | padding-right: 45px; 51 | min-width: 300px; 52 | margin-left: 70px; 53 | 54 | justify-content: center; 55 | flex-direction: column; 56 | display: flex; 57 | } 58 | .session h1 { 59 | text-align: center; 60 | margin-bottom: 20px; 61 | font-size: 40px; 62 | 63 | } 64 | 65 | .sessionForm { 66 | display: flex; 67 | flex-direction: column; 68 | } 69 | 70 | label.formItem { 71 | display: flex; 72 | flex-direction: column; 73 | border-bottom: 1px solid white; 74 | font-size: 12px; 75 | color: grey; 76 | margin-top: 16px; 77 | } 78 | 79 | .formItem input { 80 | background-color: transparent; 81 | font-size: 13px; 82 | color: white; 83 | padding-top: 10px; 84 | padding-bottom: 5px; 85 | } 86 | 87 | ::-webkit-input-placeholder { 88 | color: white; 89 | } 90 | div.formItem { 91 | text-align: middle; 92 | display: flex; 93 | flex-direction: column; 94 | margin-top: 30px; 95 | } 96 | 97 | .formItem button { 98 | background-color: transparent; 99 | color: white; 100 | border: 2px solid white; 101 | border-radius: 20px; 102 | padding: 3%; 103 | vertical-align: center; 104 | font-family: 'ProximaNova', sans-serif; 105 | font-size: 14px; 106 | } 107 | 108 | .features { 109 | padding: 45px; 110 | min-width: 585px; 111 | border-left: 1px solid white; 112 | } 113 | 114 | .features h1 { 115 | font-size: 60px; 116 | color: #07d159; 117 | font-weight: bold; 118 | } 119 | 120 | .features h2 { 121 | font-size: 28px; 122 | font-weight: 100; 123 | margin-top: 35px; 124 | margin-bottom: 35px; 125 | } 126 | 127 | .features h3 { 128 | font-size: 16px; 129 | font-weight: 100; 130 | margin-bottom: 10px; 131 | } 132 | 133 | a.newSession { 134 | color: white; 135 | font-family: 'ProximaNova', sans-serif; 136 | font-size: 12px; 137 | margin: auto; 138 | margin-top: 20px; 139 | text-decoration: none; 140 | } 141 | 142 | a.newSession:hover { 143 | text-decoration: underline; 144 | } 145 | 146 | .errors { 147 | text-align: center; 148 | color: crimson; 149 | padding: 20px; 150 | } 151 | 152 | .demo { 153 | margin-top: 10px; 154 | cursor: pointer; 155 | } 156 | 157 | // html { 158 | // background-color: black; 159 | // } 160 | html { 161 | // height: 100%; 162 | } 163 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/artists.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Artists 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::after { 6 | // background-color: red; 7 | // } 8 | .artist { 9 | // display: flex; 10 | // justify-content: center; 11 | // flex-direction: column; 12 | // align-items: center; 13 | width: 100%; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | } 18 | // .artist h1 { 19 | // font-size: 42px; 20 | // color: #fff; 21 | // // margin-top: 20px; 22 | // margin-bottom: 15px; 23 | // font-family: 'Proxima Nova', sans-serif; 24 | // font-weight: bold; 25 | // letter-spacing: 1px; 26 | // } 27 | #artist-info { 28 | color: #fff; 29 | font-size: 30px; 30 | padding-top: 10px; 31 | } 32 | 33 | #popular { 34 | margin-top: 10px; 35 | font-size: 20px; 36 | } 37 | 38 | .artist-track-info { 39 | text-align: left; 40 | flex: 1; 41 | flex-grow: 1; 42 | flex-shrink: 1; 43 | font-size: 16px; 44 | } 45 | 46 | .artist-tracks h1 { 47 | color: #fff; 48 | font-size: 17px; 49 | font-family: 'ProximaNova', sans-serif; 50 | font-weight: 100; 51 | margin-bottom: 5px; 52 | } 53 | .artist-tracks h2 { 54 | font-size: 14px; 55 | font-family: 'ProximaNova', sans-serif; 56 | font-weight: 100; 57 | } 58 | 59 | .artist-tracks-left { 60 | text-align: right; 61 | padding-right: 20px; 62 | width: 25px; 63 | 64 | } 65 | 66 | .artist-tracks { 67 | padding-top: 20px; 68 | width: 90%; 69 | padding-bottom: 30px; 70 | // width: 100%; 71 | } 72 | 73 | .artist-tracks li { 74 | display: flex; 75 | justify-content: space-between; 76 | padding-bottom: 10px; 77 | font-weight: 100; 78 | padding-top: 19px; 79 | width: 100%; 80 | transition: background-color .2s linear; 81 | box-sizing: border-box; 82 | &:hover { 83 | background-color: black; 84 | background: transparentize(black, .5%); 85 | // opacity: .4; 86 | z-index: 100; 87 | } 88 | // flex-direction: row; 89 | } 90 | 91 | .artist-tracks li:hover .num { 92 | display: none; 93 | } 94 | 95 | .artist-tracks li:hover .artist-tracks-btn:before { 96 | content: '►'; 97 | color: #fff; 98 | cursor: pointer; 99 | color: #fff; 100 | } 101 | 102 | .artist-image { 103 | // width: 100%; 104 | padding-top: 20px; 105 | border-radius: 50%; 106 | // z-index: 0001; 107 | // margin: 0 auto; 108 | // display: block; 109 | max-width: 400px; 110 | &:hover { 111 | cursor: pointer; 112 | content: '►' 113 | } 114 | } 115 | 116 | 117 | // .artist h2 { 118 | // font-size: 30px; 119 | // color: #fff; 120 | // margin-top: 15px; 121 | // margin-bottom: 15px; 122 | // font-family: 'Proxima Nova', sans-serif; 123 | // font-weight: bold; 124 | // letter-spacing: 1px; 125 | // } 126 | .artists-image { 127 | object-fit: cover; 128 | height: 250px; 129 | width: auto; 130 | border-radius: 180px; 131 | } 132 | 133 | // .artist-image:hover { 134 | // // display: none; 135 | // } 136 | // 137 | // .artist-image img:hover { 138 | // content: '►'; 139 | // z-index: 9998; 140 | // color: #fff; 141 | // cursor: pointer; 142 | // color: #fff; 143 | // } 144 | 145 | @media only screen and (max-width: 1065px) { 146 | .artists-image { 147 | height: 225px; 148 | width: auto; 149 | } 150 | } 151 | 152 | @media only screen and (max-width: 1000px) { 153 | .artists-image { 154 | height: 200px; 155 | width: auto; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /frontend/components/home/artists/artists.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import Modal from '../modal/modal'; 5 | import { createPlaylist } from '../../../actions/playlist_actions' 6 | // import { fetchArtists } from '../../../artists/playlist_artists' 7 | 8 | 9 | class Artists extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | modal: false, 14 | playlistName: '' 15 | }; 16 | this.handleChange = this.handleChange.bind(this); 17 | this.handleSubmit = this.handleSubmit.bind(this); 18 | } 19 | 20 | // componentDidMount() { 21 | // this.props.fetchArtists(); 22 | // } 23 | 24 | render() { 25 | // if (this.props.artsts) 26 | const artists = this.props.artists 27 | 28 | return ( 29 |
    30 |
    31 | 32 | this.closeModal()}> 33 | 34 |

    Create new playlist

    35 | 41 |
    42 | 43 | 44 |
    45 |
    46 |
    47 | 48 |
    49 | { 50 | artists.map(artist => 51 |
    52 | 53 | 54 |

    {artist.name}

    55 | 56 |
    ) 57 | } 58 |
    59 | 60 |
    61 | ); 62 | // } else { 63 | // return null; 64 | // } 65 | } 66 | 67 | openModal() { 68 | this.setState({ modal: true }); 69 | } 70 | 71 | closeModal() { 72 | this.setState({ modal: false }); 73 | } 74 | 75 | handleChange() { 76 | return e => { 77 | this.setState({playlistName: e.currentTarget.value}); 78 | }; 79 | } 80 | 81 | handleSubmit(e) { 82 | e.preventDefault(); 83 | if (!!this.state.playlistName) { 84 | const playlist = {user_id: this.props.user.id, name: this.state.playlistName}; 85 | this.props.createPlaylist(playlist) 86 | .then(({playlist}) => { 87 | return this.props.history.push(`playlists/${playlist.id}`) 88 | } 89 | ); 90 | } 91 | } 92 | } 93 | 94 | const mapStateToProps = (state, ownProps) => { 95 | const user = state.session.currentUser; 96 | return { 97 | artists: user.artist_ids.map(id => state.artists[id]), 98 | user: state.session.currentUser 99 | }; 100 | }; 101 | const mapDispatchToProps = (dispatch, ownProps) => { 102 | return { 103 | createPlaylist: playlist => dispatch(createPlaylist(playlist)), 104 | // fetchArtists: artists => dispatch(fetchArtists()); 105 | }; 106 | }; 107 | export default connect( 108 | mapStateToProps, 109 | mapDispatchToProps) 110 | (Artists); 111 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/modal.scss: -------------------------------------------------------------------------------- 1 | 2 | .new-playlist-form { 3 | display: flex; 4 | flex-direction: column; 5 | align-content: center; 6 | align-items: center; 7 | background-color: transparent; 8 | width: 100%; 9 | justify-content: center; 10 | min-height: 415px; 11 | } 12 | .new-playlist-form h1 { 13 | font-size: 54px; 14 | font-weight: bold; 15 | letter-spacing: 1px; 16 | height: auto; 17 | color: #fff; 18 | margin: 30px 90px 30px 90px; 19 | text-align: center; 20 | } 21 | 22 | .new-playlist-name { 23 | background-color: #242424; 24 | width: 100%; 25 | height: 155px; 26 | vertical-align: middle; 27 | display: flex; 28 | align-items: center; 29 | // padding-left: 40px; 30 | justify-content: center; 31 | } 32 | 33 | .new-playlist-name input { 34 | background-color: inherit; 35 | width: 60%; 36 | font-size: 54px; 37 | color: #fff; 38 | font-family: 'ProximaNova', sans-serif; 39 | font-weight: bold; 40 | letter-spacing: 2px; 41 | vertical-align: middle; 42 | 43 | } 44 | 45 | .modal-buttons { 46 | padding-top: 40px; 47 | padding-bottom: 40px; 48 | padding-left: 10px; 49 | 50 | } 51 | 52 | .new-playlist-name ::-webkit-input-placeholder { 53 | color: #666666; 54 | } 55 | 56 | // input, textarea { 57 | // cursor: url(cursor.cur); 58 | // } 59 | 60 | 61 | #m1, #m2 { 62 | height: 45px; 63 | width: 165px; 64 | font-size: 16px; 65 | border-radius: 35px; 66 | padding: 13px 44px; 67 | line-height: 1.3; 68 | vertical-align: middle; 69 | cursor: pointer; 70 | margin-left: 10px; 71 | margin-right: 10px; 72 | } 73 | 74 | #m1 { 75 | background: hsla(0,0%,9%,.7); 76 | color: #fff; 77 | box-shadow: inset 0 0 0 2px #a0a0a0; 78 | 79 | } 80 | // 81 | #close-button { 82 | background-color: transparent; 83 | cursor: pointer; 84 | height: 32px; 85 | width: 32px; 86 | color: white; 87 | padding: 0; 88 | font-size: 70px; 89 | text-align: center; 90 | vertical-align: middle; 91 | margin-bottom: 40px; 92 | } 93 | 94 | 95 | // #m3, #m4 { 96 | // height: 45px; 97 | // width: 165px; 98 | // font-size: 16px; 99 | // border-radius: 35px; 100 | // padding: 13px 44px; 101 | // line-height: 1.3; 102 | // vertical-align: middle; 103 | // cursor: pointer; 104 | // margin-left: 10px; 105 | // margin-right: 10px; 106 | // } 107 | 108 | // #m3 { 109 | // background: hsla(0,0%,9%,.7); 110 | // color: #fff; 111 | // box-shadow: inset 0 0 0 2px #a0a0a0; 112 | // left: 550px; 113 | // } 114 | 115 | #close-button2 { 116 | background-color: transparent; 117 | cursor: pointer; 118 | // height: 32px; 119 | // width: 32px; 120 | color: white; 121 | padding: 0; 122 | font-size: 50px; 123 | text-align: center; 124 | vertical-align: middle; 125 | // margin-bottom: 40px; 126 | } 127 | .new-playlist-form2 { 128 | display: flex; 129 | flex-direction: column; 130 | align-content: center; 131 | align-items: center; 132 | background-color: transparent; 133 | width: 100%; 134 | justify-content: center; 135 | min-width: 500px; 136 | } 137 | // .modal-buttons2 { 138 | // padding-top: 40px; 139 | // padding-bottom: 40px; 140 | // padding-left: 10px; 141 | // display: flex; 142 | // flex-direction: row; 143 | // justify-content: space-between; 144 | // } 145 | // .mod-buttons2 { 146 | // padding-top: 40px; 147 | // padding-bottom: 40px; 148 | // padding-left: 10px; 149 | // display: flex; 150 | // flex-direction: row; 151 | // justify-content: space-between; 152 | // } 153 | 154 | .new-playlist-top { 155 | display: flex; 156 | flex-direction: column; 157 | // just 158 | 159 | } 160 | // #m4 { 161 | // position: absolute; 162 | // left: 800px; 163 | // } 164 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20170625011021) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | create_table "albums", force: :cascade do |t| 20 | t.string "name", null: false 21 | t.integer "artist_id", null: false 22 | t.datetime "created_at", null: false 23 | t.datetime "updated_at", null: false 24 | t.string "image_file_name" 25 | t.string "image_content_type" 26 | t.integer "image_file_size" 27 | t.datetime "image_updated_at" 28 | end 29 | 30 | add_index "albums", ["artist_id"], name: "index_albums_on_artist_id", using: :btree 31 | 32 | create_table "artists", force: :cascade do |t| 33 | t.string "name", null: false 34 | t.datetime "created_at", null: false 35 | t.datetime "updated_at", null: false 36 | t.string "image_file_name" 37 | t.string "image_content_type" 38 | t.integer "image_file_size" 39 | t.datetime "image_updated_at" 40 | end 41 | 42 | create_table "playlistings", force: :cascade do |t| 43 | t.integer "track_id", null: false 44 | t.integer "playlist_id", null: false 45 | t.integer "ord", null: false 46 | t.datetime "created_at", null: false 47 | t.datetime "updated_at", null: false 48 | end 49 | 50 | add_index "playlistings", ["playlist_id"], name: "index_playlistings_on_playlist_id", using: :btree 51 | add_index "playlistings", ["track_id"], name: "index_playlistings_on_track_id", using: :btree 52 | 53 | create_table "playlists", force: :cascade do |t| 54 | t.string "name", null: false 55 | t.integer "user_id", null: false 56 | t.datetime "created_at", null: false 57 | t.datetime "updated_at", null: false 58 | t.string "image_file_name" 59 | t.string "image_content_type" 60 | t.integer "image_file_size" 61 | t.datetime "image_updated_at" 62 | end 63 | 64 | add_index "playlists", ["user_id"], name: "index_playlists_on_user_id", using: :btree 65 | 66 | create_table "tracks", force: :cascade do |t| 67 | t.string "title", null: false 68 | t.integer "album_id", null: false 69 | t.integer "ord", null: false 70 | t.datetime "created_at", null: false 71 | t.datetime "updated_at", null: false 72 | t.string "audio_file_name" 73 | t.string "audio_content_type" 74 | t.integer "audio_file_size" 75 | t.datetime "audio_updated_at" 76 | end 77 | 78 | add_index "tracks", ["album_id"], name: "index_tracks_on_album_id", using: :btree 79 | 80 | create_table "users", force: :cascade do |t| 81 | t.string "username", null: false 82 | t.string "password_digest", null: false 83 | t.string "session_token", null: false 84 | t.datetime "created_at", null: false 85 | t.datetime "updated_at", null: false 86 | end 87 | 88 | add_index "users", ["username"], name: "index_users_on_username", unique: true, using: :btree 89 | 90 | end 91 | -------------------------------------------------------------------------------- /frontend/components/home/artists/artist_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { fetchArtist } from '../../../actions/artist_actions'; 4 | import { addTrack } from '../../../actions/playlist_actions'; 5 | import AddTrack from '../playlists/add_track'; 6 | import { receiveSong, receiveSongs } from '../../../actions/audio_actions'; 7 | 8 | class ArtistShow extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.playTrack = this.playTrack.bind(this); 12 | this.playTracks = this.playTracks.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | this.props.fetchArtist(this.props.match.params.id); 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | if (nextProps.match.params.id != this.props.artist.id) { 21 | this.props.fetchArtist(nextProps.match.params.id) 22 | } 23 | } 24 | 25 | playTrack(track) { 26 | return (e) => { 27 | e.preventDefault(); 28 | this.props.playTrack(track); 29 | } 30 | } 31 | 32 | playTracks(e) { 33 | e.preventDefault(); 34 | if (this.props.tracks.length > 0) { 35 | this.props.playTracks(this.props.tracks); 36 | } 37 | } 38 | 39 | render() { 40 | if (this.props.tracks && this.props.artist.name) { 41 | let tracks = this.props.tracks 42 | return ( 43 |
    44 |
    45 | 48 |
    49 |
    {this.props.artist.name}
    50 | 51 | 54 | 55 | 56 | 57 |
    58 |
      59 | { 60 | tracks.map((track, i) => 61 |
    1. 62 |
      63 |

      {i+1}.

      65 |

      66 |
      67 |
      68 |

      {track.title}

      69 |

      {this.props.artist.name}

      70 |
      71 |
      72 |
      73 | 74 |
      75 |
      76 |
    2. ) 77 | } 78 |
    79 |
    80 |
    81 | ); 82 | } else { 83 | return null; 84 | } 85 | } 86 | } 87 | 88 | const mapStateToProps = (state, ownProps) => { 89 | const tracks = Object.values(state.tracks); 90 | let artist = state.artists[ownProps.match.params.id] 91 | if (!artist) { 92 | artist = { name: '', id: null, image_url: ''} 93 | } 94 | return { 95 | artist, 96 | tracks 97 | }; 98 | }; 99 | 100 | const mapDispatchToProps = dispatch => { 101 | return { 102 | fetchArtist: id => dispatch(fetchArtist(id)), 103 | addTrack: playlisting => dispatch(addTrack(playlisting)), 104 | playTrack: track => dispatch(receiveSong(track)), 105 | playTracks: tracks => dispatch(receiveSongs(tracks)) 106 | }; 107 | }; 108 | 109 | export default connect( 110 | mapStateToProps, 111 | mapDispatchToProps) 112 | (ArtistShow); 113 | 114 | // {/*
    115 | //

    Popular

    116 | //

    {this.props.artist.name}

    117 | //
      118 | // { 119 | // this.props.tracks.slice(5).map((track,idx) => 120 | //
    1. {track.title}
    2. ) 121 | // } 122 | // 123 | //
    124 | //
    */} 125 | -------------------------------------------------------------------------------- /frontend/components/home/albums/album_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { fetchAlbum } from '../../../actions/album_actions'; 5 | import { addTrack } from '../../../actions/playlist_actions'; 6 | import AddTrack from '../playlists/add_track'; 7 | import { receiveSong, receiveSongs } from '../../../actions/audio_actions'; 8 | 9 | 10 | class AlbumShow extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.playTrack = this.playTrack.bind(this); 14 | this.playTracks = this.playTracks.bind(this); 15 | } 16 | 17 | componentDidMount() { 18 | this.props.fetchAlbum(this.props.match.params.id); 19 | } 20 | componentWillReceiveProps(nextProps) { 21 | if (nextProps.match.params.id != this.props.album.id) { 22 | this.props.fetchAlbum(nextProps.match.params.id) 23 | } 24 | } 25 | 26 | playTrack(track) { 27 | return (e) => { 28 | e.preventDefault(); 29 | this.props.playTrack(track); 30 | } 31 | } 32 | 33 | playTracks(e) { 34 | if (this.props.tracks.length > 0) { 35 | e.preventDefault(); 36 | this.props.playTracks(this.props.tracks); 37 | } 38 | } 39 | 40 | render() { 41 | 42 | if (this.props.tracks && this.props.album.name) { 43 | let length = this.props.tracks.length; 44 | let songs = length === 1 ? `${length} SONG` : `${length} SONGS`; 45 | return ( 46 |
    47 |
    48 | 51 | 52 |
    53 |

    {this.props.album.name}

    54 |

    55 | 56 | {this.props.album.artist} 57 | 58 |

    59 |

    {songs}

    60 | 63 |
    64 |
    65 | 66 |
    67 |
      68 | { 69 | this.props.tracks.map((track, i) => 70 |
    1. 71 |
      72 |

      {i+1}.

      73 |

      74 |
      75 |
      76 |

      {track.title}

      77 |

      {this.props.album.artist}

      78 |
      79 |
      80 |
      81 | 82 | 83 | 84 |
      85 |
      86 |
    2. ) 87 | } 88 |
    89 |
    90 |
    91 | ); 92 | } else { 93 | return null; 94 | } 95 | } 96 | } 97 | 98 | const mapStateToProps = (state, ownProps) => { 99 | const tracks = Object.values(state.tracks); 100 | let album = state.albums[ownProps.match.params.id] 101 | if (!album) { 102 | album = { name: null, id: null, image_url: '', artist: ''} 103 | } 104 | return { 105 | album: album, 106 | tracks: tracks, 107 | addTrack: playlisting => addTrack(playlisting) 108 | }; 109 | }; 110 | 111 | const mapDispatchToProps = dispatch => { 112 | return { 113 | fetchAlbum: id => dispatch(fetchAlbum(id)), 114 | addTrack: playlisting => dispatch(addTrack(playlisting)), 115 | removeTrack: playlisting => dispatch(removeTrack(playlisting)), 116 | playTrack: track => dispatch(receiveSong(track)), 117 | playTracks: tracks => dispatch(receiveSongs(tracks)) 118 | }; 119 | }; 120 | 121 | export default connect( 122 | mapStateToProps, 123 | mapDispatchToProps) 124 | (AlbumShow); 125 | -------------------------------------------------------------------------------- /frontend/components/home/playlists/playlist_show.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { fetchPlaylist, removeTrack, deletePlaylist } from '../../../actions/playlist_actions'; 5 | import AddTrack from './add_track'; 6 | import { receiveSong, receiveSongs } from '../../../actions/audio_actions'; 7 | 8 | class PlaylistShow extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.deletePlaylist = this.deletePlaylist.bind(this); 12 | this.playTrack = this.playTrack.bind(this); 13 | this.playTracks = this.playTracks.bind(this); 14 | this.state = { play: '' } 15 | } 16 | 17 | componentDidMount() { 18 | this.props.fetchPlaylist(this.props.match.params.id); 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | if (nextProps.match.params.id != this.props.playlist.id) { 23 | this.props.fetchPlaylist(nextProps.match.params.id) 24 | } 25 | } 26 | 27 | deletePlaylist(e) { 28 | e.preventDefault(); 29 | this.props.deletePlaylist(this.props.match.params.id) 30 | .then(this.props.history.push('/playlists')) 31 | } 32 | 33 | removeTrack(track) { 34 | return (e) => { 35 | e.preventDefault(); 36 | const playlisting = { 37 | playlist_id: this.props.playlist.id, 38 | track_id: track.id 39 | } 40 | this.props.removeTrack(playlisting) 41 | } 42 | } 43 | 44 | playTrack(track) { 45 | return (e) => { 46 | e.preventDefault(); 47 | this.props.playTrack(track); 48 | } 49 | } 50 | 51 | playTracks(e) { 52 | e.preventDefault(); 53 | if (this.props.tracks.length > 0) { 54 | this.props.playTracks(this.props.tracks); 55 | } 56 | } 57 | 58 | render() { 59 | if (this.props.tracks && this.props.playlist.name) { 60 | let length = this.props.tracks.length; 61 | let songs = length === 1 ? `${length} SONG` : `${length} SONGS`; 62 | let deleteButton; 63 | if (this.props.currentUser.id === this.props.playlist.user_id) { 64 | deleteButton = 67 | } else { 68 | deleteButton = null; 69 | } 70 | return ( 71 |
    72 | 73 |
    74 | 78 | 79 |
    80 |

    {this.props.playlist.name}

    81 |

    By {this.props.playlist.creator}

    82 |

    {songs}

    83 | 86 | {deleteButton} 87 |
    88 |
    89 | 90 |
      91 | { 92 | this.props.tracks.map((track, i) => 93 |
    1. 94 |
      95 |

      {i+1}.

      97 |

      98 | 99 |
      100 | 101 |
      102 |

      {track.title}

      103 |

      {track.artist}

      104 |
      105 |
      106 |
      107 | 108 | 109 |
      110 |
      111 |
    2. ) 112 | } 113 |
    114 |
    115 | ); 116 | 117 | } else { 118 | return null; 119 | } 120 | } 121 | } 122 | 123 | const mapStateToProps = (state, ownProps) => { 124 | const tracks = Object.values(state.tracks); 125 | let playlist = state.playlists[ownProps.match.params.id] 126 | if (!playlist) { 127 | playlist = { name: null, id: null, image_url: '', artist: ''} 128 | } 129 | return { 130 | playlist: playlist, 131 | tracks: tracks, 132 | currentUser: state.session.currentUser 133 | }; 134 | }; 135 | 136 | const mapDispatchToProps = dispatch => { 137 | return { 138 | fetchPlaylist: id => dispatch(fetchPlaylist(id)), 139 | deletePlaylist: id => dispatch(deletePlaylist(id)), 140 | removeTrack: playlisting => dispatch(removeTrack(playlisting)), 141 | playTrack: track => dispatch(receiveSong(track)), 142 | playTracks: tracks => dispatch(receiveSongs(tracks)) 143 | }; 144 | }; 145 | 146 | export default connect( 147 | mapStateToProps, 148 | mapDispatchToProps) 149 | (PlaylistShow); 150 | -------------------------------------------------------------------------------- /frontend/components/home/search/search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fetchSearch } from '../../../actions/search_actions'; 3 | import { connect } from 'react-redux'; 4 | import { Link, NavLink } from 'react-router-dom'; 5 | import AddTrack from '../playlists/add_track'; 6 | import { receiveSong, receiveSongs } from '../../../actions/audio_actions'; 7 | import { AuthRoute, ProtectedRoute } from '../../../util/route_util'; 8 | import PlaylistResults from './playlist_results'; 9 | 10 | class Search extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { search: '' } 14 | this.handleChange = this.handleChange.bind(this) 15 | this.playTrack = this.playTrack.bind(this); 16 | } 17 | 18 | handleChange() { 19 | return e => { 20 | this.setState({['search']: e.currentTarget.value}) 21 | this.props.fetchSearch(e.currentTarget.value) 22 | } 23 | } 24 | 25 | componentDidMount() { 26 | this.props.fetchSearch(); 27 | } 28 | 29 | playTrack(track) { 30 | return (e) => { 31 | e.preventDefault(); 32 | this.props.playTrack(track); 33 | } 34 | } 35 | 36 | render() { 37 | let playlists = [] 38 | let albums = [] 39 | let artists = [] 40 | let tracks = [] 41 | if (this.props.results.playlists) { 42 | playlists = this.props.results.playlists 43 | albums = this.props.results.albums 44 | artists = this.props.results.artists 45 | tracks = this.props.results.tracks 46 | } 47 | return ( 48 |
    49 |
    50 | 51 | 56 |
    57 |
    58 | 59 |
    60 | { 61 | playlists.map(playlist => 62 |
    63 | 64 |

    {playlist.name}

    65 |

    {playlist.creator}

    66 |
    ) 67 | } 68 |
    69 | 70 | 71 |
    72 | { 73 | artists.map(artist => 74 |
    75 | 76 | 77 |

    {artist.name}

    78 | 79 |
    ) 80 | } 81 |
    82 | 83 |
    84 | { 85 | albums.map(album => 86 |
    87 | 88 | 90 |

    {album.name}

    91 | 92 |

    {album.artist}

    93 |
    ) 94 | } 95 |
    96 | 97 |
    98 |
    99 |
      100 | { 101 | tracks.map((track, i) => 102 |
    1. 103 |
      104 |

      {i+1}.

      105 |

      106 |
      107 |
      108 |

      {track.title}

      109 |

      {track.artist}

      110 |
      111 |
      112 |
      113 | 114 |
      115 |
      116 |
    2. ) 117 | } 118 |
    119 |
    120 |
    121 |
    122 |
    123 | 124 | ); 125 | } 126 | } 127 | 128 | const mapDispatchToProps = (dispatch, ownProp) => { 129 | return { 130 | fetchSearch: search => dispatch(fetchSearch(search)), 131 | playTrack: track => dispatch(receiveSong(track)) 132 | }; 133 | }; 134 | 135 | const mapStateToProps = state => { 136 | const user = state.session.currentUser; 137 | return { 138 | results: state.search 139 | }; 140 | }; 141 | 142 | export default connect( 143 | mapStateToProps, 144 | mapDispatchToProps) 145 | (Search); 146 | // } 147 | -------------------------------------------------------------------------------- /app/assets/stylesheets/api/playlist_show.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the Api::Playlistings 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 | .pl { 7 | // background-color: #181818; 8 | // background-image: linear-gradient(#404959, #060708 85%); 9 | padding-left: 30px; 10 | padding-top: 30px; 11 | padding-right: 30px; 12 | display: flex; 13 | flex-direction: row; 14 | width: 100%; 15 | } 16 | 17 | .pl-image { 18 | display: flex; 19 | // flex-wrap: wrap; 20 | // justify-content: space-around; 21 | // align-items: center; 22 | flex-direction: column; 23 | padding-left: 8px; 24 | padding-right: 8px; 25 | // border: 1px solid red; 26 | } 27 | .pl-info { 28 | display: flex; 29 | flex-direction: column; 30 | // padding: 20px; 31 | width: 100%; 32 | color: #fff; 33 | text-transform: capitalize; 34 | padding-bottom: 10px; 35 | align-items: center; 36 | 37 | } 38 | 39 | .pl-info h1 { 40 | font-size: 36px; 41 | padding-bottom: 10px; 42 | padding-top: 2px; 43 | text-align: center; 44 | } 45 | .pl-info h2, a { 46 | font-size: 16px; 47 | font-weight: 100; 48 | // padding-bottom: 8px; 49 | margin-bottom: 16px; 50 | color: #a0a0a0; 51 | text-decoration: none; 52 | } 53 | 54 | .pl-info h3 { 55 | font-size: 14px; 56 | color: #a0a0a0; 57 | font-weight: 100; 58 | letter-spacing: 1px; 59 | } 60 | 61 | .remove-from-pl { 62 | // margin-top: 10px; 63 | padding-right: 20px; 64 | // padding-left: 20px; 65 | background: transparent; 66 | // box-shadow: inset 0 0 0 2px #a0a0a0; 67 | color: #a0a0a0; 68 | // font-family: 'Proxima-Nova', sans-serif; 69 | // letter-spacing: 1px; 70 | cursor: pointer; 71 | font-weight: 100; 72 | vertical-align: top; 73 | &:hover { 74 | color: #fff; 75 | cursor: pointer; 76 | } 77 | } 78 | 79 | .pl-image img { 80 | padding-bottom: 15px; 81 | cursor: pointer; 82 | } 83 | 84 | .pl-tracks { 85 | margin-bottom: 40px; 86 | padding-left: 8px; 87 | // padding-right: 8px; 88 | width: 100%; 89 | } 90 | 91 | .pl-tracks li { 92 | color: #a0a0a0; 93 | display: flex; 94 | flex-direction: row; 95 | justify-content: space-between; 96 | font-weight: 100; 97 | padding-top: 15px; 98 | padding-bottom: 15px; 99 | width: 100%; 100 | transition: background-color .2s linear; 101 | box-sizing: border-box; 102 | &:hover { 103 | background-color: black; 104 | background: transparentize(black, .5%); 105 | // opacity: .4; 106 | z-index: 100; 107 | } 108 | } 109 | .pl-tracks-left { 110 | text-align: right; 111 | padding-right: 20px; 112 | width: 25px; 113 | 114 | } 115 | .track-info { 116 | text-align: left; 117 | flex: 1; 118 | flex-grow: 1; 119 | flex-shrink: 1; 120 | } 121 | 122 | .track-info h1 { 123 | color: #fff; 124 | font-size: 17px; 125 | font-family: 'ProximaNova', sans-serif; 126 | font-weight: 100; 127 | margin-bottom: 5px; 128 | } 129 | 130 | .track-info h2 { 131 | font-size: 14px; 132 | font-family: 'ProximaNova', sans-serif; 133 | font-weight: 100; 134 | } 135 | 136 | .num { 137 | // padding-right: 20px; 138 | // padding-left: 20px; 139 | font-size: 17px; 140 | font-weight: 100; 141 | cursor: pointer; 142 | &:hover { 143 | color: #fff; 144 | } 145 | } 146 | 147 | .pl-tracks li:hover .num { 148 | display: none; 149 | } 150 | 151 | .pl-tracks li:hover .pl-tracks-btn:before { 152 | content: '►'; 153 | color: #fff; 154 | cursor: pointer; 155 | } 156 | 157 | .play-tracks-btn, .delete-playlist { 158 | height: 35px; 159 | width: 95px; 160 | font-size: 12px; 161 | border-radius: 35px; 162 | line-height: 1.3; 163 | vertical-align: middle; 164 | cursor: pointer; 165 | margin-left: 10px; 166 | margin-right: 10px; 167 | margin-top: 14px; 168 | // margin-bottom: 10px; 169 | letter-spacing: 2px; 170 | background-color: #1db954; 171 | color: white; 172 | display: block; 173 | &:hover { 174 | box-sizing: border-box; 175 | transition: .15s ease; 176 | background-color: #1ed660; 177 | color: white; 178 | } 179 | } 180 | 181 | .delete-playlist { 182 | background: transparent; 183 | color: #fff; 184 | box-shadow: inset 0 0 0 2px #a0a0a0; 185 | &:hover{ 186 | background: transparent; 187 | } 188 | } 189 | 190 | // li:hover .pl-tracks-btn { 191 | // content: '►'; 192 | // } 193 | // .track-info { 194 | // // display: block; 195 | // // justify-content: space-between; 196 | // } 197 | 198 | // .pl-title h1 { 199 | // color: white; 200 | // font-size: 20px; 201 | // 202 | // } 203 | // .pl-title h2 { 204 | // color: white; 205 | // font-size: 16px; 206 | // color: #a0a0a0; 207 | // } 208 | 209 | .pl-tracks-right { 210 | // margin-left: 5px; 211 | display: flex; 212 | flex-direction: column; 213 | } 214 | // .pl-tracks-right h3 { 215 | // margin: 0px; 216 | // padding-left: 5px; 217 | // } 218 | // .pl-tracks-right button{ 219 | // box-shadow: none; 220 | // padding: 5px; 221 | // text-align: left; 222 | // border-radius: 0; 223 | // &:hover { 224 | // background-color: black; 225 | // } 226 | // } 227 | 228 | // .track-dropdown { 229 | // position: fixed; 230 | // opacity: 0; 231 | // pointer-events: none; 232 | // } 233 | .pl-options { 234 | // position: fixed; 235 | // opacity: 1; 236 | // top: 114px; 237 | // left: 814px; 238 | display: flex; 239 | flex-direction: row; 240 | align-items: center; 241 | } 242 | 243 | // .add-to-pl, .remove-from-pl { 244 | // width: 100%; 245 | // padding: 3px 20px; 246 | // clear: both; 247 | // font-weight: 400; 248 | // line-height: 2; 249 | // color: hsla(0,0%,100%,.4); 250 | // text-align: inherit; 251 | // white-space: nowrap; 252 | // background: 0 0; 253 | // border: 0; 254 | // cursor: pointer; 255 | // } 256 | -------------------------------------------------------------------------------- /app/assets/stylesheets/playbar.scss: -------------------------------------------------------------------------------- 1 | .playbar { 2 | width: 100%; 3 | height: 100px; 4 | z-index: 100; 5 | background-color: #282828; 6 | position: fixed; 7 | bottom: 0px; 8 | 9 | // flex-direction: column; 10 | // left: 0px; 11 | } 12 | // 13 | .pb-container{ 14 | // display: flex; 15 | // justify-content: center; 16 | // align-items: center; 17 | flex-direction: column; 18 | // height: 100%; 19 | // width: 100%; 20 | display: flex; 21 | justify-content: space-around; 22 | align-items: center; 23 | height: auto; 24 | // min-width: 620px; 25 | } 26 | 27 | .playbar-items { 28 | display: flex; 29 | flex-direction: row; 30 | align-items: center; 31 | height: 100px; 32 | // flex-direction: column; 33 | justify-content: space-between; 34 | padding: 0 16px; 35 | width: 100%; 36 | } 37 | 38 | .playbar-left { 39 | width: 30%; 40 | min-width: 180px; 41 | margin-right: 50px; 42 | // margin-right: 1px; 43 | } 44 | 45 | .now-playing { 46 | display: flex; 47 | } 48 | 49 | .now-info { 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: center; 53 | margin-right: 10px; 54 | vertical-align: middle; 55 | } 56 | .now-title { 57 | font-size: 14px; 58 | font-weight: 100; 59 | color: #fff; 60 | line-height: 27px; 61 | // vertical-align: middle; 62 | } 63 | .now-artist { 64 | font-size: 12px; 65 | font-weight: 100; 66 | // color: #fff; 67 | line-height: 21px; 68 | white-space: nowrap; 69 | overflow: hidden; 70 | text-overflow: ellipsis; 71 | vertical-align: middle; 72 | } 73 | 74 | .now-artist:hover, .now-title:hover { 75 | color: #fff; 76 | text-decoration: underline; 77 | cursor: pointer; 78 | transition: .2s ease; 79 | } 80 | .album-art { 81 | height: 60px; 82 | width: 60px; 83 | margin-left: 20px; 84 | margin-right: 15px; 85 | box-shadow: 0 0 10px rgba(0,0,0,.3); 86 | } 87 | 88 | 89 | 90 | .playbar-middle { 91 | width: 40%; 92 | max-width: 722px; 93 | display: flex; 94 | flex-direction: column; 95 | align-items: center; 96 | min-width: 200px; 97 | } 98 | 99 | .controls { 100 | display: flex; 101 | justify-content: space-between; 102 | align-items: center; 103 | height: 31px; 104 | // align-content: space-between; 105 | // width: 125px; 106 | // flex-direction: row; 107 | // margin-bottom: 12px; 108 | /* margin-bottom: 12px; */ 109 | cursor: default; 110 | width: 224px; 111 | 112 | justify-content: space-between; 113 | 114 | flex-direction: row; 115 | 116 | flex-wrap: nowrap; 117 | } 118 | .progress-bar { 119 | // border-top: 1px solid red; 120 | // width: 40%; 121 | // border: 1px solid #404040; 122 | background: #404040; 123 | border-radius: 10px; 124 | cursor: pointer; 125 | // height: 12px; 126 | position: relative; 127 | width: 100%; 128 | height: 4px; 129 | // bottom: 15px; 130 | // position: absolute; 131 | } 132 | // .progress-bar__bg { 133 | // background-color: #404040; 134 | // border-radius: 2px; 135 | // // display: -webkit-box; 136 | // // display: -ms-flexbox; 137 | // display: flex; 138 | // height: 4px; 139 | // width: 100%; 140 | // } 141 | .playbar-bottom { 142 | width: 100%; 143 | display: flex; 144 | align-items: center; 145 | margin-top: 8px; 146 | box-sizing: border-box; 147 | } 148 | .progress { 149 | border-radius: 20px; 150 | height: 4px; 151 | background-color: #1db954; 152 | } 153 | .slider { 154 | font-size: 10px; 155 | } 156 | 157 | 158 | #play { 159 | // border: 1px solid; 160 | border-radius: 90px; 161 | padding-bottom: 8px; 162 | padding-top: 10px; 163 | padding-left: 11px; 164 | padding-right: 9px; 165 | background-color: #282828; 166 | text-align: center; 167 | vertical-align: middle; 168 | box-sizing: border-box; 169 | border: 1px solid #a0a0a0; 170 | font-size: 13px; 171 | &:hover { 172 | color: white; 173 | cursor: pointer; 174 | border-color: white; 175 | } 176 | } 177 | #pause { 178 | // border: 1px solid; 179 | border-radius: 90px; 180 | padding: 9px 10px; 181 | background-color: #282828; 182 | text-align: center; 183 | vertical-align: middle; 184 | box-sizing: border-box; 185 | border: 1px solid #a0a0a0; 186 | font-size: 13px; 187 | &:hover { 188 | color: white; 189 | cursor: pointer; 190 | border-color: white; 191 | } 192 | } 193 | 194 | .progress-time, .duration { 195 | font-size: 11px; 196 | margin-left: 2px; 197 | margin-right: 2px; 198 | vertical-align: baseline; 199 | } 200 | #previous:hover, #skip:hover { 201 | color: white; 202 | cursor: pointer; 203 | } 204 | .playbar-right { 205 | width: 30%; 206 | min-width: 180px; 207 | display: flex; 208 | flex-direction: row; 209 | justify-content: flex-end; 210 | margin-left: 50px; 211 | // vertical-align: middle; 212 | // align-content: center; 213 | } 214 | 215 | .vol-controls { 216 | display: flex; 217 | vertical-align: middle; 218 | align-items: center; 219 | width: 100%; 220 | justify-content: flex-end; 221 | // border-bottom: 1px solid #404040; 222 | } 223 | .vol-bar { 224 | background-color: #404040; 225 | width: 200px; 226 | height: 4px; 227 | margin-right: 30px; 228 | display: flex; 229 | align-items: center; 230 | border-radius: 3px; 231 | cursor: pointer; 232 | 233 | // vertical-align: middle; 234 | // border-bottom: 1px solid #404040; 235 | } 236 | 237 | .vol-progress { 238 | // width: 50%; 239 | background-color: #a0a0a0; 240 | height: 4px; 241 | border-radius: 3px; 242 | cursor: pointer; 243 | &:hover { 244 | background-color: #1db954; 245 | } 246 | // vertical-align: baseline; 247 | } 248 | 249 | .vol-icon { 250 | color: #a0a0a0; 251 | padding-right: 5px; 252 | 253 | } 254 | 255 | .slider { 256 | position: relative; 257 | right: 3px; 258 | color: #fff; 259 | 260 | } 261 | // .fa, .fa-2x 262 | // sidbar: z-index 100 263 | // artist-image: width 100vw; 264 | // z-index: 0001 265 | // top 0 266 | // left 0 267 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.8) 5 | actionpack (= 4.2.8) 6 | actionview (= 4.2.8) 7 | activejob (= 4.2.8) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.8) 11 | actionview (= 4.2.8) 12 | activesupport (= 4.2.8) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 17 | actionview (4.2.8) 18 | activesupport (= 4.2.8) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 23 | activejob (4.2.8) 24 | activesupport (= 4.2.8) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.8) 27 | activesupport (= 4.2.8) 28 | builder (~> 3.1) 29 | activerecord (4.2.8) 30 | activemodel (= 4.2.8) 31 | activesupport (= 4.2.8) 32 | arel (~> 6.0) 33 | activesupport (4.2.8) 34 | i18n (~> 0.7) 35 | minitest (~> 5.1) 36 | thread_safe (~> 0.3, >= 0.3.4) 37 | tzinfo (~> 1.1) 38 | arel (6.0.4) 39 | aws-sdk (2.10.3) 40 | aws-sdk-resources (= 2.10.3) 41 | aws-sdk-core (2.10.3) 42 | aws-sigv4 (~> 1.0) 43 | jmespath (~> 1.0) 44 | aws-sdk-resources (2.10.3) 45 | aws-sdk-core (= 2.10.3) 46 | aws-sigv4 (1.0.0) 47 | bcrypt (3.1.11) 48 | better_errors (2.1.1) 49 | coderay (>= 1.0.0) 50 | erubis (>= 2.6.6) 51 | rack (>= 0.9.0) 52 | binding_of_caller (0.7.2) 53 | debug_inspector (>= 0.0.1) 54 | builder (3.2.3) 55 | byebug (9.0.6) 56 | climate_control (0.2.0) 57 | cocaine (0.5.8) 58 | climate_control (>= 0.0.3, < 1.0) 59 | coderay (1.1.1) 60 | coffee-rails (4.1.1) 61 | coffee-script (>= 2.2.0) 62 | railties (>= 4.0.0, < 5.1.x) 63 | coffee-script (2.4.1) 64 | coffee-script-source 65 | execjs 66 | coffee-script-source (1.12.2) 67 | concurrent-ruby (1.0.5) 68 | debug_inspector (0.0.3) 69 | erubis (2.7.0) 70 | execjs (2.7.0) 71 | faker (1.7.3) 72 | i18n (~> 0.5) 73 | figaro (1.1.1) 74 | thor (~> 0.14) 75 | font-awesome-sass (4.7.0) 76 | sass (>= 3.2) 77 | globalid (0.4.0) 78 | activesupport (>= 4.2.0) 79 | i18n (0.8.4) 80 | jbuilder (2.7.0) 81 | activesupport (>= 4.2.0) 82 | multi_json (>= 1.2) 83 | jmespath (1.3.1) 84 | jquery-rails (4.3.1) 85 | rails-dom-testing (>= 1, < 3) 86 | railties (>= 4.2.0) 87 | thor (>= 0.14, < 2.0) 88 | json (1.8.6) 89 | loofah (2.0.3) 90 | nokogiri (>= 1.5.9) 91 | mail (2.6.6) 92 | mime-types (>= 1.16, < 4) 93 | method_source (0.8.2) 94 | mime-types (3.1) 95 | mime-types-data (~> 3.2015) 96 | mime-types-data (3.2016.0521) 97 | mimemagic (0.3.2) 98 | mini_portile2 (2.2.0) 99 | minitest (5.10.2) 100 | multi_json (1.12.1) 101 | nokogiri (1.8.0) 102 | mini_portile2 (~> 2.2.0) 103 | paperclip (5.0.0.beta1) 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.21.0) 110 | pry (0.10.4) 111 | coderay (~> 1.1.0) 112 | method_source (~> 0.8.1) 113 | slop (~> 3.4) 114 | pry-rails (0.3.6) 115 | pry (>= 0.10.4) 116 | quiet_assets (1.1.0) 117 | railties (>= 3.1, < 5.0) 118 | rack (1.6.8) 119 | rack-test (0.6.3) 120 | rack (>= 1.0) 121 | rails (4.2.8) 122 | actionmailer (= 4.2.8) 123 | actionpack (= 4.2.8) 124 | actionview (= 4.2.8) 125 | activejob (= 4.2.8) 126 | activemodel (= 4.2.8) 127 | activerecord (= 4.2.8) 128 | activesupport (= 4.2.8) 129 | bundler (>= 1.3.0, < 2.0) 130 | railties (= 4.2.8) 131 | sprockets-rails 132 | rails-deprecated_sanitizer (1.0.3) 133 | activesupport (>= 4.2.0.alpha) 134 | rails-dom-testing (1.0.8) 135 | activesupport (>= 4.2.0.beta, < 5.0) 136 | nokogiri (~> 1.6) 137 | rails-deprecated_sanitizer (>= 1.0.1) 138 | rails-html-sanitizer (1.0.3) 139 | loofah (~> 2.0) 140 | rails_12factor (0.0.3) 141 | rails_serve_static_assets 142 | rails_stdout_logging 143 | rails_serve_static_assets (0.0.5) 144 | rails_stdout_logging (0.0.5) 145 | railties (4.2.8) 146 | actionpack (= 4.2.8) 147 | activesupport (= 4.2.8) 148 | rake (>= 0.8.7) 149 | thor (>= 0.18.1, < 2.0) 150 | rake (12.0.0) 151 | rdoc (4.3.0) 152 | sass (3.4.24) 153 | sass-rails (5.0.6) 154 | railties (>= 4.0.0, < 6) 155 | sass (~> 3.1) 156 | sprockets (>= 2.8, < 4.0) 157 | sprockets-rails (>= 2.0, < 4.0) 158 | tilt (>= 1.1, < 3) 159 | sdoc (0.4.2) 160 | json (~> 1.7, >= 1.7.7) 161 | rdoc (~> 4.0) 162 | slop (3.6.0) 163 | spring (2.0.2) 164 | activesupport (>= 4.2) 165 | sprockets (3.7.1) 166 | concurrent-ruby (~> 1.0) 167 | rack (> 1, < 3) 168 | sprockets-rails (3.2.0) 169 | actionpack (>= 4.0) 170 | activesupport (>= 4.0) 171 | sprockets (>= 3.0.0) 172 | thor (0.19.4) 173 | thread_safe (0.3.6) 174 | tilt (2.0.7) 175 | tzinfo (1.2.3) 176 | thread_safe (~> 0.1) 177 | uglifier (3.2.0) 178 | execjs (>= 0.3.0, < 3) 179 | web-console (2.3.0) 180 | activemodel (>= 4.0) 181 | binding_of_caller (>= 0.7.2) 182 | railties (>= 4.0) 183 | sprockets-rails (>= 2.0, < 4.0) 184 | 185 | PLATFORMS 186 | ruby 187 | 188 | DEPENDENCIES 189 | aws-sdk 190 | bcrypt 191 | better_errors 192 | binding_of_caller 193 | byebug 194 | coffee-rails (~> 4.1.0) 195 | faker (~> 1.6, >= 1.6.6) 196 | figaro 197 | font-awesome-sass 198 | jbuilder (~> 2.0) 199 | jquery-rails 200 | paperclip 201 | pg (~> 0.15) 202 | pry-rails 203 | quiet_assets 204 | rails (= 4.2.8) 205 | rails_12factor 206 | sass-rails (~> 5.0) 207 | sdoc (~> 0.4.0) 208 | spring 209 | uglifier (>= 1.3.0) 210 | web-console (~> 2.0) 211 | 212 | BUNDLED WITH 213 | 1.15.1 214 | --------------------------------------------------------------------------------