├── 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 | // this.music = el}>
8 | //
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 |
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 | 
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 | 
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 | 
19 | 
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 |
this.openModal()}>NEW PLAYLIST
25 |
this.closeModal()}>
26 |
27 | this.closeModal()}>✕
28 |
Create new playlist
29 |
30 |
31 |
35 |
36 |
37 | this.closeModal()}>CANCEL
38 | this.handleSubmit(e)}>CREATE
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 |
this.openModal()}>+
32 |
this.closeModal()}>
33 |
34 | this.closeModal()}>×
35 |
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 |
83 |
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 |
29 | Spitfire
30 |
31 |
32 | Search
33 |
34 |
35 |
36 | Browse
37 |
38 |
39 | Your Music
40 |
41 |
42 |
43 |
44 | {this.props.currentUser.username}
45 |
46 | Logout
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 |
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 |
this.openModal()}>NEW PLAYLIST
25 |
this.closeModal()}>
26 | this.closeModal()}>✕
27 | Create new playlist
28 |
29 |
33 |
34 |
35 | this.closeModal()}>CANCEL
36 | this.handleSubmit(e)}>CREATE
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 |
this.openModal()}>NEW PLAYLIST
32 |
this.closeModal()}>
33 | this.closeModal()}>✕
34 | Create new playlist
35 |
36 |
40 |
41 |
42 | this.closeModal()}>CANCEL
43 | this.handleSubmit(e)}>CREATE
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 | Popular Tracks
51 | PLAY
54 |
55 |
56 |
57 |
58 |
59 | {
60 | tracks.map((track, i) =>
61 |
62 |
67 |
68 |
{track.title}
69 | {this.props.artist.name}
70 |
71 |
76 | )
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 | // {track.title}
)
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 | PLAY
63 |
64 |
65 |
66 |
67 |
68 | {
69 | this.props.tracks.map((track, i) =>
70 |
71 |
75 |
76 |
{track.title}
77 | {this.props.album.artist}
78 |
79 |
86 | )
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 = this.deletePlaylist(e)}>DELETE
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 | PLAY
86 | {deleteButton}
87 |
88 |
89 |
90 |
91 | {
92 | this.props.tracks.map((track, i) =>
93 |
94 |
95 |
{i+1}.
97 |
98 |
99 |
100 |
101 |
102 |
{track.title}
103 | {track.artist}
104 |
105 |
111 | )
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 | Search for an Artist, Song, Album or Playlist
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 |
103 |
107 |
108 |
{track.title}
109 | {track.artist}
110 |
111 |
116 | )
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 |
--------------------------------------------------------------------------------