diff --git a/app/assets/stylesheets/components/show_box_content.scss b/app/assets/stylesheets/components/show_box_content.scss index 820bbb4..22fbb6f 100644 --- a/app/assets/stylesheets/components/show_box_content.scss +++ b/app/assets/stylesheets/components/show_box_content.scss @@ -33,6 +33,19 @@ font-family: Circular,Helvetica,Arial,sans-serif; } +.show-follow { + display: flex; + justify-content: center; +} + +.follow-button { + padding-top: 10px; + color: lightgray; + font-weight: 100; + font-size: 14px; + font-family: Circular,Helvetica,Arial,sans-serif; +} + .loading { width: 60px; margin: auto; diff --git a/app/controllers/api/follows_controller.rb b/app/controllers/api/follows_controller.rb index 6a4655e..6c6f3e7 100644 --- a/app/controllers/api/follows_controller.rb +++ b/app/controllers/api/follows_controller.rb @@ -1,2 +1,33 @@ class Api::FollowsController < ApplicationController + def create + + @follow = Follow.new(follow_params) + if @follow.save + render 'api/follows/show' + else + render json: ["Could not process request"], status: 401 + end + end + + def destroy + + @follow = Follow.find_by( + user_id: follow_params[:user_id], + followable_id: follow_params[:followable_id], + followable_type: follow_params[:followable_type], + ) + if @follow + f = @follow.dup + @follow.destroy + render json: f + else + render json: ["Connection could not be found"], status: 404 + end + end + + private + + def follow_params + params.require(:follow).permit(:user_id, :followable_id, :followable_type) + end end diff --git a/app/models/artist.rb b/app/models/artist.rb index 04c84b9..3b7e4a7 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -24,4 +24,7 @@ class Artist < ApplicationRecord has_many :albums, through: :songs, source: :album + + has_many :follows, as: :followable, dependent: :destroy + has_many :followers, through: :follows, source: :user end diff --git a/app/models/follow.rb b/app/models/follow.rb index 7ba32c5..800f0c1 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -7,8 +7,13 @@ # followeable_type :string # created_at :datetime not null # updated_at :datetime not null +# user_id :integer # class Follow < ApplicationRecord - belongs_to :followeable_id + validates :user_id, :followable_id, :followable_type, presence: true + validates :user_id, uniqueness: { :scope => [:followable_type, :followable_id] } + + belongs_to :user + belongs_to :followable, polymorphic: true end diff --git a/app/models/playlist.rb b/app/models/playlist.rb index a6f9cc8..6ed92a5 100644 --- a/app/models/playlist.rb +++ b/app/models/playlist.rb @@ -30,4 +30,7 @@ class Playlist < ApplicationRecord has_one_attached :photo has_one_attached :track + + has_many :follows, as: :followable, dependent: :destroy + has_many :followers, through: :follows, source: :user end diff --git a/app/models/user.rb b/app/models/user.rb index 1d2af85..5c784f7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,6 +38,10 @@ class User < ApplicationRecord through: :songs, source: :artists + has_many :follows + has_many :followed_artists, through: :follows, source: :followable, source_type: 'Artist' + has_many :followed_playlists, through: :follows, source: :followable, source_type: 'Playlist' + def password=(password) @password = password self.password_digest = BCrypt::Password.create(password) diff --git a/app/views/api/follows/show.json.jbuilder b/app/views/api/follows/show.json.jbuilder new file mode 100644 index 0000000..ded9ece --- /dev/null +++ b/app/views/api/follows/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @follow, :id, :user_id, :followable_id, :followable_type diff --git a/app/views/api/users/_user.json.jbuilder b/app/views/api/users/_user.json.jbuilder index 99a82f5..4a5f648 100644 --- a/app/views/api/users/_user.json.jbuilder +++ b/app/views/api/users/_user.json.jbuilder @@ -1 +1 @@ -json.extract! user, :id, :username +json.extract! user, :id, :username, :followed_artist_ids, :followed_playlist_ids diff --git a/app/views/api/users/show.json.jbuilder b/app/views/api/users/show.json.jbuilder index 6474d4f..155147a 100644 --- a/app/views/api/users/show.json.jbuilder +++ b/app/views/api/users/show.json.jbuilder @@ -1,3 +1,3 @@ -json.extract! @user, :id, :username, :song_ids, :album_ids +json.extract! @user, :id, :username, :song_ids, :album_ids, :followed_artist_ids, :followed_playlist_ids # json.artist_ids @artists.pluck(:id) diff --git a/db/migrate/20181229235133_add_userid_to_follows.rb b/db/migrate/20181229235133_add_userid_to_follows.rb new file mode 100644 index 0000000..dceab9a --- /dev/null +++ b/db/migrate/20181229235133_add_userid_to_follows.rb @@ -0,0 +1,5 @@ +class AddUseridToFollows < ActiveRecord::Migration[5.2] + def change + add_column :follows, :user_id, :integer + end +end diff --git a/db/migrate/20181229235944_change_follows_column_names.rb b/db/migrate/20181229235944_change_follows_column_names.rb new file mode 100644 index 0000000..1e762e2 --- /dev/null +++ b/db/migrate/20181229235944_change_follows_column_names.rb @@ -0,0 +1,6 @@ +class ChangeFollowsColumnNames < ActiveRecord::Migration[5.2] + def change + rename_column :follows, :followeable_id, :followable_id + rename_column :follows, :followeable_type, :followable_type + end +end diff --git a/db/schema.rb b/db/schema.rb index 9312314..52dae8c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_11_18_235623) do +ActiveRecord::Schema.define(version: 2018_12_29_235944) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -50,10 +50,11 @@ end create_table "follows", force: :cascade do |t| - t.integer "followeable_id" - t.string "followeable_type" + t.integer "followable_id" + t.string "followable_type" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "user_id" end create_table "playlists", force: :cascade do |t| diff --git a/frontend/actions/follow_actions.js b/frontend/actions/follow_actions.js new file mode 100644 index 0000000..8941420 --- /dev/null +++ b/frontend/actions/follow_actions.js @@ -0,0 +1,30 @@ +import * as FollowAPIUtil from '../util/follow_api_util'; + +export const RECEIVE_FOLLOW = 'RECEIVE_FOLLOW'; +export const REMOVE_FOLLOW = 'REMOVE_FOLLOW'; + +const receiveFollow = follow => { + return { + type: RECEIVE_FOLLOW, + follow + }; +}; + +const removeFollow = follow => { + return { + type: REMOVE_FOLLOW, + follow + }; +}; + +export const createFollow = follow => { + return dispatch => FollowAPIUtil.createFollow(follow).then( + res => dispatch(receiveFollow(res)) + ); +}; + +export const deleteFollow = follow => { + return dispatch => FollowAPIUtil.deleteFollow(follow).then( + res => dispatch(removeFollow(res)) + ); +}; diff --git a/frontend/actions/search_actions.js b/frontend/actions/search_actions.js index af2f3af..47431c8 100644 --- a/frontend/actions/search_actions.js +++ b/frontend/actions/search_actions.js @@ -4,7 +4,7 @@ export const RECEIVE_ALL_ALBUMS = 'RECEIVE_ALL_ALBUMS'; export const RECEIVE_ALBUM = 'RECEIVE_ALBUM'; export const requestAllAlbums = (searchQuery) => { - debugger + return (dispatch) => { return SearchApiUtil.fetchAllAlbums(searchQuery) .then(null, diff --git a/frontend/components/main/header/album_index_container.js b/frontend/components/main/header/album_index_container.js index 5a9fbe3..b5d1d51 100644 --- a/frontend/components/main/header/album_index_container.js +++ b/frontend/components/main/header/album_index_container.js @@ -6,7 +6,7 @@ import { requestAllAlbums } from '../../../actions/search_actions'; import { selectRandomAlbums } from '../../../reducers/selectors'; const mapStateToProps = (state, ownProps) => { - debugger + return { path: "album", navpath: ownProps.navpath, diff --git a/frontend/components/main/header/artist_show_container.js b/frontend/components/main/header/artist_show_container.js index fd15730..afb634f 100644 --- a/frontend/components/main/header/artist_show_container.js +++ b/frontend/components/main/header/artist_show_container.js @@ -4,24 +4,31 @@ import { connect } from 'react-redux'; import GridShow from './grid_show'; import { logout } from '../../../actions/session_actions'; import { selectArtistSongs } from '../../../reducers/selectors'; +import { createFollow, deleteFollow } from '../../../actions/follow_actions'; const mapStateToProps = (state, ownProps) => { - debugger + // const artist = Object.values(state.entities.remoteArtists)[ownProps.match.params.artistId] || ownProps.artists || [], const artist = state.entities.artists[ownProps.match.params.artistId] || { name: "", song_ids: [], photoUrl: "" }; const songs = selectArtistSongs(state, artist); + + // const currentUser = state.entities.users[state.session.currentUserId]; + const currentUser = state.session.currentUser; return { artist, artistId: ownProps.match.params.artistId, - songs + songs, + currentUser }; }; const mapDipatchToProps = (dispatch) => { return { fetchArtist: (id) => dispatch(fetchArtist(id)), - logout: () => dispatch(logout()) + logout: () => dispatch(logout()), + createFollow: (follow) => dispatch(createFollow(follow)), + deleteFollow: (follow) => dispatch(deleteFollow(follow)) }; }; diff --git a/frontend/components/main/header/grid_index.jsx b/frontend/components/main/header/grid_index.jsx index ba362db..167ca6b 100644 --- a/frontend/components/main/header/grid_index.jsx +++ b/frontend/components/main/header/grid_index.jsx @@ -11,7 +11,7 @@ class GridIndex extends React.Component { } componentDidMount(){ - debugger + this.fetchElements( {search_term: this.props.searchTerm} ); @@ -26,7 +26,7 @@ class GridIndex extends React.Component { } render(){ - debugger + const {playlists, artists, albums, navpath, path} = this.props; let property = "title"; let gridElements = []; diff --git a/frontend/components/main/header/grid_show.jsx b/frontend/components/main/header/grid_show.jsx index a3c3fe1..e4d116b 100644 --- a/frontend/components/main/header/grid_show.jsx +++ b/frontend/components/main/header/grid_show.jsx @@ -14,6 +14,8 @@ class GridShow extends React.Component { this.elementId = this.props.playlistId || this.props.albumId || this.props.artistId; this.fetchElement = this.fetchElement.bind(this); this.timeout = this.timeout.bind(this); + this.handleFollow = this.handleFollow.bind(this); + } timeout(){ @@ -25,6 +27,18 @@ class GridShow extends React.Component { .then( this.timeout ); } + handleFollow(e) { + + const following = this.props.currentUser.followed_artist_ids.includes(this.props.artist.id); + const follow = { + user_id: this.props.currentUser.id, + followable_id: this.props.artist.id, + followable_type: 'Artist' + }; + + following ? this.props.deleteFollow(follow) : this.props.createFollow(follow); + } + render () { function isDefined(song){ @@ -66,6 +80,16 @@ class GridShow extends React.Component { element.artworkUrl100 = element.artworkUrl100.replace('100x100', '600x600'); } + let follows = ""; + if (this.props.artist) { + follows = ( + + ); + } + return (
@@ -78,6 +102,9 @@ class GridShow extends React.Component {

{element.title || element.name || element.collectionName}

{element.author || element.artists || element.artistName}
+
+ {follows} +
{tracks} diff --git a/frontend/components/main/header/search/search_results.jsx b/frontend/components/main/header/search/search_results.jsx index e4f8b12..df5853d 100644 --- a/frontend/components/main/header/search/search_results.jsx +++ b/frontend/components/main/header/search/search_results.jsx @@ -48,7 +48,7 @@ class SearchResults extends React.Component { // // //
- debugger + return (
diff --git a/frontend/components/main/header/songs_index_item.jsx b/frontend/components/main/header/songs_index_item.jsx index 1c84f94..a138fc2 100644 --- a/frontend/components/main/header/songs_index_item.jsx +++ b/frontend/components/main/header/songs_index_item.jsx @@ -52,7 +52,7 @@ class SongsIndexItem extends React.Component { this.props.pauseCurrentSong(); this.setState({ playing: false }); } else { - debugger + this.props.receiveCurrentSong(songId, elementId, elementType); this.setState({ playing: true }); } diff --git a/frontend/components/main/navbar/browse_container.js b/frontend/components/main/navbar/browse_container.js index bac85e0..9494765 100644 --- a/frontend/components/main/navbar/browse_container.js +++ b/frontend/components/main/navbar/browse_container.js @@ -6,6 +6,7 @@ import { selectAllUnauthoredPlaylists, selectRandomAlbums, selectRandomArtists} import MainContent from '../main_content'; const mapStateToProps = (state, ownProps) => { + return { playlists: selectAllUnauthoredPlaylists(state), albums: selectRandomAlbums(state), diff --git a/frontend/components/main/playbar/music_player.jsx b/frontend/components/main/playbar/music_player.jsx index d3abe9e..fe9d24c 100644 --- a/frontend/components/main/playbar/music_player.jsx +++ b/frontend/components/main/playbar/music_player.jsx @@ -90,7 +90,7 @@ class MusicPlayer extends React.Component { } nextSong(currentSongId) { - debugger + let songList = this.props.songList; songList = songList.map((el) => { return parseInt(el); diff --git a/frontend/reducers/entities/entities_reducer.js b/frontend/reducers/entities/entities_reducer.js index 18556df..eaedbb3 100644 --- a/frontend/reducers/entities/entities_reducer.js +++ b/frontend/reducers/entities/entities_reducer.js @@ -15,5 +15,6 @@ export default combineReducers({ artists: artistsReducer, albums: albumsReducer, remoteAlbums: searchAlbumsReducer, - remoteSongs: searchSongsReducer + remoteSongs: searchSongsReducer, + users: usersReducer }); diff --git a/frontend/reducers/entities/users_reducer.js b/frontend/reducers/entities/users_reducer.js index 02721d2..9781931 100644 --- a/frontend/reducers/entities/users_reducer.js +++ b/frontend/reducers/entities/users_reducer.js @@ -1,12 +1,15 @@ import { RECEIVE_CURRENT_USER, RECEIVE_USER} from '../../actions/session_actions'; +import { RECEIVE_FOLLOW, REMOVE_FOLLOW } from '../../actions/follow_actions'; +import { merge } from 'lodash'; -const usersReducer = (state = {}, action) => { - Object.freeze(state); - switch(action.type) { +const usersReducer = ( state = {}, action ) => { + + Object.freeze(state); + switch (action.type) { case RECEIVE_CURRENT_USER: - const currentUser = {[action.user.id]: action.user}; - return Object.assign({}, state, currentUser); + const currentUser = action.currentUser; + return merge({}, state, { currentUser }); default: return state; } diff --git a/frontend/reducers/selectors.js b/frontend/reducers/selectors.js index 5573333..cf30ab7 100644 --- a/frontend/reducers/selectors.js +++ b/frontend/reducers/selectors.js @@ -106,7 +106,7 @@ export const selectAlbumSongs = (state, album) => { export const selectAllSongs = state => Object.values(state.entities.songs); export const getSongList = (state, currentSong) => { - debugger + if (!currentSong.id) { return []; } diff --git a/frontend/reducers/session/session_reducer.js b/frontend/reducers/session/session_reducer.js index b953ced..539cb4d 100644 --- a/frontend/reducers/session/session_reducer.js +++ b/frontend/reducers/session/session_reducer.js @@ -3,18 +3,39 @@ import merge from 'lodash/merge'; import { RECEIVE_CURRENT_USER, } from '../../actions/session_actions'; +import { RECEIVE_FOLLOW, REMOVE_FOLLOW } from '../../actions/follow_actions'; -const _nullUser = Object.freeze({ + +const _nullUser = { currentUser: null -}); +}; const sessionReducer = (state = _nullUser, action) => { - + Object.freeze(state); switch(action.type) { case RECEIVE_CURRENT_USER: - const currentUser = action.currentUser; + let currentUser = action.currentUser; return merge({}, state, { currentUser }); + case RECEIVE_FOLLOW: + const followedState = merge({}, state); + if (action.follow.followable_type === 'Artist') { + followedState["currentUser"].followed_artist_ids.push(action.follow.followable_id); + } else if (action.follow.followable_type === 'Playlist') { + followedState["currentUser"].followed_playlist_ids.push(action.follow.followable_id); + } + return followedState; + case REMOVE_FOLLOW: + const removedFollowState = merge({}, state); + currentUser = action.currentUser; + if (action.follow.followable_type === 'Artist') { + const artistIdx = state["currentUser"].followed_artist_ids.indexOf(action.follow.followable_id); + removedFollowState["currentUser"].followed_artist_ids.splice(artistIdx,1); + } else if (action.follow.followable_type === 'Playlist') { + const playlistIdx = state["currentUser"].followed_playlist_ids.indexOf(action.follow.followable_id); + removedFollowState["currentUser"].followed_playlist_ids.splice(playlistIdx,1); + } + return removedFollowState; default: return state; } diff --git a/frontend/util/follow_api_util.js b/frontend/util/follow_api_util.js new file mode 100644 index 0000000..5b9dac1 --- /dev/null +++ b/frontend/util/follow_api_util.js @@ -0,0 +1,17 @@ +export const createFollow = follow => { + + return $.ajax({ + method: 'POST', + url: `api/follows`, + data: { follow } + }); +}; + +export const deleteFollow = follow => { + + return $.ajax({ + method: 'DELETE', + url: `api/follows/${follow.followable_id}`, + data: { follow } + }); +}; diff --git a/frontend/util/search_api_util.jsx b/frontend/util/search_api_util.jsx index 898779c..94cb9ba 100644 --- a/frontend/util/search_api_util.jsx +++ b/frontend/util/search_api_util.jsx @@ -1,5 +1,5 @@ export const fetchAllAlbums = (searchQuery) => { - debugger + if (!searchQuery.search_term) { searchQuery.search_term = randomizeQuery(); }